From 0e71a7434a1e3c887d566abed147dde3905c95f3 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Wed, 18 Mar 2026 16:17:25 +0900 Subject: [PATCH 01/14] feat(common): editor core types --- packages/common/src/options/index.ts | 9 ++++- .../common/src/options/types/options.type.ts | 21 ++++++++++ .../common/src/options/types/save.type.ts | 40 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 packages/common/src/options/types/save.type.ts diff --git a/packages/common/src/options/index.ts b/packages/common/src/options/index.ts index 32ab2502..1bd44e31 100644 --- a/packages/common/src/options/index.ts +++ b/packages/common/src/options/index.ts @@ -1 +1,8 @@ -export type { IRunClientOptions, IRunOptions, IRunServerOptions } from "./types/options.type"; +export type { + IRunClientOptions, + IRunOptions, + IRunServerOptions, + IEditorRunClientOptions, + IEditorRunOptions, + IEditorRunServerOptions, +} from "./types/options.type"; diff --git a/packages/common/src/options/types/options.type.ts b/packages/common/src/options/types/options.type.ts index 3ed47674..5cab0ac1 100644 --- a/packages/common/src/options/types/options.type.ts +++ b/packages/common/src/options/types/options.type.ts @@ -1,3 +1,5 @@ +import { type Save } from "./save.type"; + export type IRunOptions = IRunClientOptions | IRunServerOptions; export interface IRunClientOptions { @@ -10,3 +12,22 @@ export interface IRunServerOptions { files: Map; env: Record; } + +export type IEditorRunOptions = IEditorRunClientOptions | IEditorRunServerOptions; + +export interface IEditorRunClientOptions { + canvas: HTMLCanvasElement; + files: Map; + env: Record; + editor: { + save: Save; + }; +} +export interface IEditorRunServerOptions { + canvas: HTMLCanvasElement; + files: Map; + env: Record; + editor: { + save: Save; + }; +} diff --git a/packages/common/src/options/types/save.type.ts b/packages/common/src/options/types/save.type.ts new file mode 100644 index 00000000..94371ad1 --- /dev/null +++ b/packages/common/src/options/types/save.type.ts @@ -0,0 +1,40 @@ +export enum SaveLibraryTypeEnum { + COMPONENT_SYSTEM = "component-system", + GRAPHICS = "graphics", + ASSET_MANAGER = "asset-manager", + NETWORK = "network", + INPUT = "input", + SOUND = "sound", +} + +export interface SaveLibrary { + id: string; + type: SaveLibraryTypeEnum | string; + name: string; + path: string; +} + +export interface SaveComponent { + name: string; + path: string; +} + +export interface SaveSystem { + name: string; + path: string; +} + +export interface SaveEntity { + id: string; + components: { + name: string; + params: string[]; + }[]; +} + +export interface Save { + libraries: SaveLibrary[]; + components: SaveComponent[]; + systems: SaveSystem[]; + entities: SaveEntity[]; +} From 151811457a3a88f64ade2141acfbcaa074b176cf Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Wed, 18 Mar 2026 16:17:48 +0900 Subject: [PATCH 02/14] feat(common): editor core types --- packages/core-editor/.cliff-jumperrc.json | 7 + packages/core-editor/.gitignore | 231 ++++++++++++++++++ packages/core-editor/.idea/.name | 1 + .../.idea/[NanoForge] Engine Core.iml | 12 + .../core-editor/.idea/codeStyles/Project.xml | 57 +++++ .../.idea/codeStyles/codeStyleConfig.xml | 5 + .../core-editor/.idea/git_toolbox_blame.xml | 6 + .../core-editor/.idea/git_toolbox_prj.xml | 15 ++ .../inspectionProfiles/Project_Default.xml | 6 + .../core-editor/.idea/jsLinters/eslint.xml | 6 + packages/core-editor/.idea/modules.xml | 8 + packages/core-editor/.idea/prettier.xml | 7 + packages/core-editor/.idea/vcs.xml | 6 + packages/core-editor/.nvmrc | 1 + packages/core-editor/.prettierignore | 11 + packages/core-editor/CHANGELOG.md | 59 +++++ packages/core-editor/LICENSE | 21 ++ packages/core-editor/README.md | 68 ++++++ packages/core-editor/cliff.toml | 79 ++++++ packages/core-editor/eslint.config.js | 3 + packages/core-editor/package.json | 91 +++++++ packages/core-editor/prettier.config.js | 3 + .../src/application/application-config.ts | 89 +++++++ .../application/application-options.type.ts | 3 + .../src/application/nanoforge-application.ts | 57 +++++ .../src/application/nanoforge-client.ts | 21 ++ .../src/application/nanoforge-factory.ts | 15 ++ .../src/application/nanoforge-server.ts | 3 + .../contexts/application.editable-context.ts | 20 ++ .../executions/clear.editable-context.ts | 3 + .../executions/execution.editable-context.ts | 3 + .../executions/init.editable-context.ts | 3 + .../contexts/library.editable-context.ts | 7 + .../common/library/manager/library.manager.ts | 106 ++++++++ .../common/library/relationship-functions.ts | 122 +++++++++ .../core-editor/src/config/config-registry.ts | 19 ++ packages/core-editor/src/core/core.ts | 104 ++++++++ packages/core-editor/src/index.ts | 4 + .../core-editor/test/config-registry.spec.ts | 54 ++++ .../test/editable-library-manager.spec.ts | 182 ++++++++++++++ .../core-editor/test/relationship.spec.ts | 135 ++++++++++ packages/core-editor/tsconfig.json | 6 + packages/core-editor/tsconfig.spec.json | 10 + packages/core-editor/tsup.config.ts | 3 + packages/core-editor/typedoc.json | 5 + 45 files changed, 1677 insertions(+) create mode 100644 packages/core-editor/.cliff-jumperrc.json create mode 100644 packages/core-editor/.gitignore create mode 100644 packages/core-editor/.idea/.name create mode 100644 packages/core-editor/.idea/[NanoForge] Engine Core.iml create mode 100644 packages/core-editor/.idea/codeStyles/Project.xml create mode 100644 packages/core-editor/.idea/codeStyles/codeStyleConfig.xml create mode 100644 packages/core-editor/.idea/git_toolbox_blame.xml create mode 100644 packages/core-editor/.idea/git_toolbox_prj.xml create mode 100644 packages/core-editor/.idea/inspectionProfiles/Project_Default.xml create mode 100644 packages/core-editor/.idea/jsLinters/eslint.xml create mode 100644 packages/core-editor/.idea/modules.xml create mode 100644 packages/core-editor/.idea/prettier.xml create mode 100644 packages/core-editor/.idea/vcs.xml create mode 100644 packages/core-editor/.nvmrc create mode 100644 packages/core-editor/.prettierignore create mode 100644 packages/core-editor/CHANGELOG.md create mode 100644 packages/core-editor/LICENSE create mode 100644 packages/core-editor/README.md create mode 100644 packages/core-editor/cliff.toml create mode 100644 packages/core-editor/eslint.config.js create mode 100644 packages/core-editor/package.json create mode 100644 packages/core-editor/prettier.config.js create mode 100644 packages/core-editor/src/application/application-config.ts create mode 100644 packages/core-editor/src/application/application-options.type.ts create mode 100644 packages/core-editor/src/application/nanoforge-application.ts create mode 100644 packages/core-editor/src/application/nanoforge-client.ts create mode 100644 packages/core-editor/src/application/nanoforge-factory.ts create mode 100644 packages/core-editor/src/application/nanoforge-server.ts create mode 100644 packages/core-editor/src/common/context/contexts/application.editable-context.ts create mode 100644 packages/core-editor/src/common/context/contexts/executions/clear.editable-context.ts create mode 100644 packages/core-editor/src/common/context/contexts/executions/execution.editable-context.ts create mode 100644 packages/core-editor/src/common/context/contexts/executions/init.editable-context.ts create mode 100644 packages/core-editor/src/common/context/contexts/library.editable-context.ts create mode 100644 packages/core-editor/src/common/library/manager/library.manager.ts create mode 100644 packages/core-editor/src/common/library/relationship-functions.ts create mode 100644 packages/core-editor/src/config/config-registry.ts create mode 100644 packages/core-editor/src/core/core.ts create mode 100644 packages/core-editor/src/index.ts create mode 100644 packages/core-editor/test/config-registry.spec.ts create mode 100644 packages/core-editor/test/editable-library-manager.spec.ts create mode 100644 packages/core-editor/test/relationship.spec.ts create mode 100644 packages/core-editor/tsconfig.json create mode 100644 packages/core-editor/tsconfig.spec.json create mode 100644 packages/core-editor/tsup.config.ts create mode 100644 packages/core-editor/typedoc.json diff --git a/packages/core-editor/.cliff-jumperrc.json b/packages/core-editor/.cliff-jumperrc.json new file mode 100644 index 00000000..fe6d9d4a --- /dev/null +++ b/packages/core-editor/.cliff-jumperrc.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json", + "name": "core", + "org": "nanoforge-dev", + "packagePath": "packages/core", + "identifierBase": false +} diff --git a/packages/core-editor/.gitignore b/packages/core-editor/.gitignore new file mode 100644 index 00000000..f7652f89 --- /dev/null +++ b/packages/core-editor/.gitignore @@ -0,0 +1,231 @@ +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Turbo +.turbo/ + +# Compiled files +src/**/*.js +src/**/*.d.ts diff --git a/packages/core-editor/.idea/.name b/packages/core-editor/.idea/.name new file mode 100644 index 00000000..3d3e7caa --- /dev/null +++ b/packages/core-editor/.idea/.name @@ -0,0 +1 @@ +[NanoForge] Engine Core \ No newline at end of file diff --git a/packages/core-editor/.idea/[NanoForge] Engine Core.iml b/packages/core-editor/.idea/[NanoForge] Engine Core.iml new file mode 100644 index 00000000..24643cc3 --- /dev/null +++ b/packages/core-editor/.idea/[NanoForge] Engine Core.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/core-editor/.idea/codeStyles/Project.xml b/packages/core-editor/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..b70d7533 --- /dev/null +++ b/packages/core-editor/.idea/codeStyles/Project.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/core-editor/.idea/codeStyles/codeStyleConfig.xml b/packages/core-editor/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/packages/core-editor/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/packages/core-editor/.idea/git_toolbox_blame.xml b/packages/core-editor/.idea/git_toolbox_blame.xml new file mode 100644 index 00000000..7dc12496 --- /dev/null +++ b/packages/core-editor/.idea/git_toolbox_blame.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/packages/core-editor/.idea/git_toolbox_prj.xml b/packages/core-editor/.idea/git_toolbox_prj.xml new file mode 100644 index 00000000..02b915b8 --- /dev/null +++ b/packages/core-editor/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/packages/core-editor/.idea/inspectionProfiles/Project_Default.xml b/packages/core-editor/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..03d9549e --- /dev/null +++ b/packages/core-editor/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/packages/core-editor/.idea/jsLinters/eslint.xml b/packages/core-editor/.idea/jsLinters/eslint.xml new file mode 100644 index 00000000..541945bb --- /dev/null +++ b/packages/core-editor/.idea/jsLinters/eslint.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/packages/core-editor/.idea/modules.xml b/packages/core-editor/.idea/modules.xml new file mode 100644 index 00000000..99922e22 --- /dev/null +++ b/packages/core-editor/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/core-editor/.idea/prettier.xml b/packages/core-editor/.idea/prettier.xml new file mode 100644 index 00000000..0c83ac4e --- /dev/null +++ b/packages/core-editor/.idea/prettier.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/packages/core-editor/.idea/vcs.xml b/packages/core-editor/.idea/vcs.xml new file mode 100644 index 00000000..b2bdec2d --- /dev/null +++ b/packages/core-editor/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/core-editor/.nvmrc b/packages/core-editor/.nvmrc new file mode 100644 index 00000000..c519bf5b --- /dev/null +++ b/packages/core-editor/.nvmrc @@ -0,0 +1 @@ +v24.11.0 diff --git a/packages/core-editor/.prettierignore b/packages/core-editor/.prettierignore new file mode 100644 index 00000000..64b127c9 --- /dev/null +++ b/packages/core-editor/.prettierignore @@ -0,0 +1,11 @@ +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock +bun.lock + +.turbo/ +node_modules/ +dist/ +coverage/ +CHANGELOG.md diff --git a/packages/core-editor/CHANGELOG.md b/packages/core-editor/CHANGELOG.md new file mode 100644 index 00000000..1d7fb605 --- /dev/null +++ b/packages/core-editor/CHANGELOG.md @@ -0,0 +1,59 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +# [@nanoforge-dev/core@1.0.1](https://github.com/NanoForge-dev/Engine/compare/@nanoforge-dev/core@1.0.0...@nanoforge-dev/core@1.0.1) - (2026-02-16) + +## Documentation + +- Setup typedoc (#192) ([fa908e7](https://github.com/NanoForge-dev/Engine/commit/fa908e7e268fa1770be58fc62a0257f3760480b2)) by @MartinFillon +- Fix readme badges (#186) ([fd8d93d](https://github.com/NanoForge-dev/Engine/commit/fd8d93d13a0fbad95ef9952acd10faad9e112c78)) by @Exeloo + +# [@nanoforge-dev/core@1.0.0](https://github.com/NanoForge-dev/Engine/tree/@nanoforge-dev/core@1.0.0) - (2026-01-09) + +## Bug Fixes + +- **graphics:** Game loop ([53329d2](https://github.com/NanoForge-dev/Engine/commit/53329d28c47bfac9fe86259e9fc6f42b206062a8)) by @Exeloo +- **graphics:** Fix display ([d8522e5](https://github.com/NanoForge-dev/Engine/commit/d8522e56678f3bd136733f7941c1d917c18b1400)) by @Exeloo +- **ecs:** Fix tests ([d33ada5](https://github.com/NanoForge-dev/Engine/commit/d33ada5d9c37e331b8178aa1fc0daee88b07131c)) by @Exeloo +- **ecs:** Change type handling on lib ecs ([580192d](https://github.com/NanoForge-dev/Engine/commit/580192d5038f386c965434f78aacdf3d1e399ff8)) by @Exeloo + +## Documentation + +- Update README files with new structure and detailed usage examples for all packages (#157) ([63fab73](https://github.com/NanoForge-dev/Engine/commit/63fab7326bd9c7e6b00f950694ab16c9d9190c53)) by @Exeloo +- Add funding (#147) ([7301fad](https://github.com/NanoForge-dev/Engine/commit/7301fad10f59b7e1f7fa788f8a2f6fc81d0db72e)) by @Exeloo +- Add a basic introduction readme ([b240964](https://github.com/NanoForge-dev/Engine/commit/b240964a265b31769a8c5422e23e20156ba56192)) by @MartinFillon +- Add building and dependency docs to every readme ([2d4785b](https://github.com/NanoForge-dev/Engine/commit/2d4785bdcb455e83337b37540f9ab6b3394c0850)) by @MartinFillon + +## Features + +- **packages/network:** Client and server for tcp/udp and networked pong as example (#156) ([839fb95](https://github.com/NanoForge-dev/Engine/commit/839fb95449f6ae0ee66d7f7e279374268b743f65)) by @Tchips46 +- **core:** Add client/server distinction and update rendering logic (#119) ([5271432](https://github.com/NanoForge-dev/Engine/commit/5271432710031396d7e433bfdfb015e3871f69d0)) by @Exeloo +- Add schematics used types (#102) ([b992306](https://github.com/NanoForge-dev/Engine/commit/b9923064ba1da3164b1739fcdec5a819734c4ba2)) by @Exeloo +- **core:** Introduce `EditableApplicationContext` for managing sound libraries ([6c7bac2](https://github.com/NanoForge-dev/Engine/commit/6c7bac261eeb7ad79203d5695d5ad76dc9e9e9f5)) by @Exeloo +- **core:** Add Context that admit a ClientLibraryManager ([3835bc8](https://github.com/NanoForge-dev/Engine/commit/3835bc8a6e6d039f11a513b7fe54c353f90e9fe1)) by @Exeloo +- **music:** Finish music library and add an interface for mutable libraries ([8e00c5d](https://github.com/NanoForge-dev/Engine/commit/8e00c5d00f2901ada86f59667eff7e5d3446076b)) by @MartinFillon +- **core:** Add `class-transformer` and `class-validator` dependencies for validation utilities ([fd94fe7](https://github.com/NanoForge-dev/Engine/commit/fd94fe7755999c5529335666720899792a691a36)) by @Exeloo +- **common, core, config:** Introduce configuration registry and validation system ([4fafb82](https://github.com/NanoForge-dev/Engine/commit/4fafb82576fec6866fc281ad5b10321d2ac430df)) by @Exeloo +- **core:** Enhance type safety and execution context handling ([d986030](https://github.com/NanoForge-dev/Engine/commit/d986030a333bc08d2e37291d1a023cf8d7a6e1d6)) by @Exeloo +- **app:** Add the ability to mute and unmute sounds ([947bdc0](https://github.com/NanoForge-dev/Engine/commit/947bdc00784a4c3313fe08feb4f91fc91b3ac7b7)) by @MartinFillon +- **sound:** Add basic sound playing to example ([7335814](https://github.com/NanoForge-dev/Engine/commit/7335814fc532ee92a5f9d776f409c5faa4d56423)) by @MartinFillon +- **core:** Add default libraries to constructor ([7d9da69](https://github.com/NanoForge-dev/Engine/commit/7d9da69be4301875020176656276236b88b737f1)) by @Exeloo +- Add dependencies handling ([e51dd3b](https://github.com/NanoForge-dev/Engine/commit/e51dd3bdb5e2e3de21339bf6218e85f935efb9d5)) by @Exeloo +- **common:** Add dependencies handler ([edb098a](https://github.com/NanoForge-dev/Engine/commit/edb098a65fb932ba9a9532a9b1eee7d64a7a8f0d)) by @Exeloo +- **core:** Add tickrate and fix runner ([1dba5bd](https://github.com/NanoForge-dev/Engine/commit/1dba5bd89ffa20dfd29b079f93c3eb923ffbdbbc)) by @Exeloo +- **input:** Add input library ([387e97d](https://github.com/NanoForge-dev/Engine/commit/387e97d7c3015a869947af4acecf48e8e1b0e2b8)) by @Exeloo +- **game:** Create pong example game ([4b66674](https://github.com/NanoForge-dev/Engine/commit/4b66674c750f345e860d225384054423433beb07)) by @bill-h4rper +- **game:** Add width and height ([c93c985](https://github.com/NanoForge-dev/Engine/commit/c93c985665bd99c09bc410f1499d11aeaffe3c4c)) by @Exeloo +- **game:** Add graphics factory ([0f4453c](https://github.com/NanoForge-dev/Engine/commit/0f4453ced908b39e953a672324e97eba82bfeaa3)) by @Exeloo +- **asset-manager:** Add asset manager ([1774a26](https://github.com/NanoForge-dev/Engine/commit/1774a26593099b4faa0a2527d1684de35211d5d2)) by @Exeloo +- Add asset manager default in core ([26cc5a9](https://github.com/NanoForge-dev/Engine/commit/26cc5a99e014fbc8669a43cc4aa4d78ecc1dee14)) by @Exeloo +- Add core and common ([1755c79](https://github.com/NanoForge-dev/Engine/commit/1755c799c143513d72b28edaac875267d484a44f)) by @Exeloo +- Initial commit ([c9bb59e](https://github.com/NanoForge-dev/Engine/commit/c9bb59ee963e7b444e8668db55597915e9ef0e4b)) by @Exeloo + +## Refactor + +- **core:** Remove default libs in factory (#118) ([fa893c7](https://github.com/NanoForge-dev/Engine/commit/fa893c71616f151343c2f52a4723a64cca65814a)) by @Exeloo +- Migrate namespaces to `@nanoforge-dev` and update related imports ([c84c927](https://github.com/NanoForge-dev/Engine/commit/c84c927ead941d914e5a9fd752fd3a5ac969f981)) by @Exeloo +- **libraries:** Implement initialization validation and standardize nullable fields ([8b04575](https://github.com/NanoForge-dev/Engine/commit/8b04575cf7f649a440b8f40ad6114414406b0c1a)) by @Exeloo + diff --git a/packages/core-editor/LICENSE b/packages/core-editor/LICENSE new file mode 100644 index 00000000..62c6400b --- /dev/null +++ b/packages/core-editor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2025 NanoForge + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/core-editor/README.md b/packages/core-editor/README.md new file mode 100644 index 00000000..f114fe2e --- /dev/null +++ b/packages/core-editor/README.md @@ -0,0 +1,68 @@ +
+
+

+ NanoForge +

+
+

+ npm version + npm downloads + Tests status + Documentation status + Last commit + Contributors +

+
+ +## About + +`@nanoforge-dev/core` is a core package that contains game main loop. It is used to initialize the game and run it. + +## Installation + +**Node.js 24.11.0 or newer is required.** + +```sh +npm install @nanoforge-dev/core +yarn add @nanoforge-dev/core +pnpm add @nanoforge-dev/core +bun add @nanoforge-dev/core +``` + +## Example usage + +Initialize the game in your main file. + +```ts +import { type IRunOptions } from "@nanoforge-dev/common"; +import { NanoforgeFactory } from "@nanoforge-dev/core"; + +export async function main(options: IRunClientOptions) { + const app = NanoforgeFactory.createClient(); + + await app.init(options); + + await app.run(); +} +``` + +## Links + +- [GitHub][source] +- [npm][npm] + +## Contributing + +Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the +[documentation][documentation]. +See [the contribution guide][contributing] if you'd like to submit a PR. + +## Help + +If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle nudge in the right direction, please don't hesitate to ask questions in [discussions][discussions]. + +[documentation]: https://github.com/NanoForge-dev/Engine +[discussions]: https://github.com/NanoForge-dev/Engine/discussions +[source]: https://github.com/NanoForge-dev/Engine/tree/main/packages/core +[npm]: https://www.npmjs.com/package/@nanoforge-dev/core +[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md diff --git a/packages/core-editor/cliff.toml b/packages/core-editor/cliff.toml new file mode 100644 index 00000000..59978d4c --- /dev/null +++ b/packages/core-editor/cliff.toml @@ -0,0 +1,79 @@ +[changelog] +header = """ +# Changelog + +All notable changes to this project will be documented in this file.\n +""" +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} +{% if version %}\ + # [{{ version | trim_start_matches(pat="v") }}]\ + {% if previous %}\ + {% if previous.version %}\ + ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\ + {% else %}\ + ({{ self::remote_url() }}/tree/{{ version }})\ + {% endif %}\ + {% endif %} \ + - ({{ timestamp | date(format="%Y-%m-%d") }}) +{% else %}\ + # [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ## {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}\ + **{{commit.scope}}:** \ + {% endif %}\ + {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ + {% if commit.breaking %}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ + {% endfor %}\ + {% endif %}\ + {% endfor %} +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + - @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = true +commit_parsers = [ + { message = "^feat", group = "Features"}, + { message = "^fix", group = "Bug Fixes"}, + { message = "^docs", group = "Documentation"}, + { message = "^perf", group = "Performance"}, + { message = "^refactor", group = "Refactor"}, + { message = "^types", group = "Typings"}, + { message = ".*deprecated", body = ".*deprecated", group = "Deprecation"}, + { message = "^revert", skip = true}, + { message = "^style", group = "Styling"}, + { message = "^test", group = "Testing"}, + { message = "^chore", skip = true}, + { message = "^ci", skip = true}, + { message = "^build", skip = true}, + { body = ".*security", group = "Security"}, +] +filter_commits = true +protect_breaking_commits = true +tag_pattern = "@nanoforge-dev/core@[0-9]*" +ignore_tags = "" +topo_order = false +sort_commits = "newest" + +[remote.github] +owner = "NanoForge-dev" +repo = "Engine" diff --git a/packages/core-editor/eslint.config.js b/packages/core-editor/eslint.config.js new file mode 100644 index 00000000..62ec06dc --- /dev/null +++ b/packages/core-editor/eslint.config.js @@ -0,0 +1,3 @@ +import config from "@nanoforge-dev/utils-eslint-config"; + +export default config; diff --git a/packages/core-editor/package.json b/packages/core-editor/package.json new file mode 100644 index 00000000..90e0540a --- /dev/null +++ b/packages/core-editor/package.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@nanoforge-dev/core-editor", + "version": "1.0", + "description": "NanoForge Engine - Core Editor", + "keywords": [ + "nanoforge", + "game", + "engine" + ], + "homepage": "https://github.com/NanoForge-dev/Engine#readme", + "bugs": "https://github.com/NanoForge-dev/Engine/issues", + "license": "MIT", + "contributors": [ + "Bill ", + "Exelo ", + "Fexkoser ", + "Tchips " + ], + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.cts", + "exports": { + ".": { + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./package.json": "./package.json" + }, + "type": "module", + "directories": { + "lib": "src" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NanoForge-dev/Engine.git", + "directory": "packages/core-editor" + }, + "funding": "https://github.com/NanoForge-dev/Engine?sponsor", + "scripts": { + "build": "tsc --noEmit && tsup", + "lint": "prettier --check . && eslint --format=pretty src", + "format": "prettier --write . && eslint --fix --format=pretty src", + "test:unit": "vitest run --config ../../vitest.config.ts", + "prepack": "pnpm run build && pnpm run lint", + "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/core-editor/*'", + "release": "cliff-jumper" + }, + "dependencies": { + "@nanoforge-dev/asset-manager": "workspace:*", + "@nanoforge-dev/common": "workspace:*", + "@nanoforge-dev/input": "workspace:*", + "class-transformer": "catalog:config", + "class-validator": "catalog:config" + }, + "devDependencies": { + "@favware/cliff-jumper": "catalog:ci", + "@nanoforge-dev/utils-eslint-config": "workspace:*", + "@nanoforge-dev/utils-prettier-config": "workspace:*", + "@trivago/prettier-plugin-sort-imports": "catalog:lint", + "eslint": "catalog:lint", + "prettier": "catalog:lint", + "tsup": "catalog:build", + "typescript": "catalog:core", + "vitest": "catalog:test" + }, + "packageManager": "pnpm@10.29.3", + "engines": { + "node": "25" + }, + "publishConfig": { + "access": "public" + }, + "lint-staged": { + "**": [ + "prettier --ignore-unknown --write" + ], + "src/**/*.ts": [ + "eslint --fix" + ] + } +} diff --git a/packages/core-editor/prettier.config.js b/packages/core-editor/prettier.config.js new file mode 100644 index 00000000..27d0e269 --- /dev/null +++ b/packages/core-editor/prettier.config.js @@ -0,0 +1,3 @@ +import config from "@nanoforge-dev/utils-prettier-config"; + +export default config; diff --git a/packages/core-editor/src/application/application-config.ts b/packages/core-editor/src/application/application-config.ts new file mode 100644 index 00000000..82d28bed --- /dev/null +++ b/packages/core-editor/src/application/application-config.ts @@ -0,0 +1,89 @@ +import { + type IAssetManagerLibrary, + type IComponentSystemLibrary, + type IGraphicsLibrary, + type IInputLibrary, + type ILibrary, + type IMusicLibrary, + type INetworkLibrary, + type ISoundLibrary, + type LibraryHandle, +} from "@nanoforge-dev/common"; + +import { EditableLibraryManager } from "../common/library/manager/library.manager"; + +export class ApplicationConfig { + private readonly _libraryManager: EditableLibraryManager; + + constructor() { + this._libraryManager = new EditableLibraryManager(); + } + + get libraryManager(): EditableLibraryManager { + return this._libraryManager; + } + + public getLibrary(sym: symbol): LibraryHandle { + return this._libraryManager.get(sym); + } + + public useLibrary(sym: symbol, library: ILibrary): void { + this._libraryManager.set(sym, library); + } + + public getComponentSystemLibrary() { + return this._libraryManager.getComponentSystem(); + } + + public useComponentSystemLibrary(library: IComponentSystemLibrary) { + this._libraryManager.setComponentSystem(library); + } + + public getGraphicsLibrary() { + return this._libraryManager.getGraphics(); + } + + public useGraphicsLibrary(library: IGraphicsLibrary) { + this._libraryManager.setGraphics(library); + } + + public getNetworkLibrary() { + return this._libraryManager.getNetwork(); + } + + public useNetworkLibrary(library: INetworkLibrary) { + this._libraryManager.setNetwork(library); + } + + public getAssetManagerLibrary() { + return this._libraryManager.getAssetManager(); + } + + public useAssetManagerLibrary(library: IAssetManagerLibrary) { + this._libraryManager.setAssetManager(library); + } + + public getInputLibrary() { + return this._libraryManager.getInput(); + } + + public useInputLibrary(library: IInputLibrary) { + this._libraryManager.setInput(library); + } + + public getSoundLibrary() { + return this._libraryManager.getSound(); + } + + public useSoundLibrary(library: ISoundLibrary) { + this._libraryManager.setSound(library); + } + + public getMusicLibrary() { + return this._libraryManager.getMusic(); + } + + public useMusicLibrary(library: IMusicLibrary) { + this._libraryManager.setMusic(library); + } +} diff --git a/packages/core-editor/src/application/application-options.type.ts b/packages/core-editor/src/application/application-options.type.ts new file mode 100644 index 00000000..57ecc833 --- /dev/null +++ b/packages/core-editor/src/application/application-options.type.ts @@ -0,0 +1,3 @@ +export interface IApplicationOptions { + tickRate: number; +} diff --git a/packages/core-editor/src/application/nanoforge-application.ts b/packages/core-editor/src/application/nanoforge-application.ts new file mode 100644 index 00000000..b35db5ae --- /dev/null +++ b/packages/core-editor/src/application/nanoforge-application.ts @@ -0,0 +1,57 @@ +import { + type IAssetManagerLibrary, + type IComponentSystemLibrary, + type IEditorRunOptions, + type ILibrary, + type INetworkLibrary, + NfNotInitializedException, +} from "@nanoforge-dev/common"; + +import { EditableApplicationContext } from "../common/context/contexts/application.editable-context"; +import { Core } from "../core/core"; +import { ApplicationConfig } from "./application-config"; +import type { IApplicationOptions } from "./application-options.type"; + +export abstract class NanoforgeApplication { + protected applicationConfig: ApplicationConfig; + private _core?: Core; + private readonly _options: IApplicationOptions; + + constructor(options?: Partial) { + this.applicationConfig = new ApplicationConfig(); + + this._options = { + tickRate: 60, + ...(options ?? {}), + }; + } + + public use(sym: symbol, library: ILibrary): void { + this.applicationConfig.useLibrary(sym, library); + } + + public useComponentSystem(library: IComponentSystemLibrary) { + this.applicationConfig.useComponentSystemLibrary(library); + } + + public useNetwork(library: INetworkLibrary) { + this.applicationConfig.useNetworkLibrary(library); + } + + public useAssetManager(library: IAssetManagerLibrary) { + this.applicationConfig.useAssetManagerLibrary(library); + } + + public init(options: IEditorRunOptions): Promise { + this._core = new Core( + this.applicationConfig, + new EditableApplicationContext(this.applicationConfig.libraryManager), + ); + return this._core.init(options, this._options); + } + + public run() { + if (!this._core) throw new NfNotInitializedException("Core"); + return this._core?.run(); + } +} diff --git a/packages/core-editor/src/application/nanoforge-client.ts b/packages/core-editor/src/application/nanoforge-client.ts new file mode 100644 index 00000000..1ddfe4ac --- /dev/null +++ b/packages/core-editor/src/application/nanoforge-client.ts @@ -0,0 +1,21 @@ +import { + type IGraphicsLibrary, + type IInputLibrary, + type ISoundLibrary, +} from "@nanoforge-dev/common"; + +import { NanoforgeApplication } from "./nanoforge-application"; + +export class NanoforgeClient extends NanoforgeApplication { + public useGraphics(library: IGraphicsLibrary) { + this.applicationConfig.useGraphicsLibrary(library); + } + + public useInput(library: IInputLibrary) { + this.applicationConfig.useInputLibrary(library); + } + + public useSound(library: ISoundLibrary) { + this.applicationConfig.useSoundLibrary(library); + } +} diff --git a/packages/core-editor/src/application/nanoforge-factory.ts b/packages/core-editor/src/application/nanoforge-factory.ts new file mode 100644 index 00000000..84711a6d --- /dev/null +++ b/packages/core-editor/src/application/nanoforge-factory.ts @@ -0,0 +1,15 @@ +import { type IApplicationOptions } from "./application-options.type"; +import { NanoforgeClient } from "./nanoforge-client"; +import { NanoforgeServer } from "./nanoforge-server"; + +class NanoforgeFactoryStatic { + createClient(options?: Partial): NanoforgeClient { + return new NanoforgeClient(options); + } + + createServer(options?: Partial): NanoforgeServer { + return new NanoforgeServer(options); + } +} + +export const NanoforgeFactory = new NanoforgeFactoryStatic(); diff --git a/packages/core-editor/src/application/nanoforge-server.ts b/packages/core-editor/src/application/nanoforge-server.ts new file mode 100644 index 00000000..74cca8a8 --- /dev/null +++ b/packages/core-editor/src/application/nanoforge-server.ts @@ -0,0 +1,3 @@ +import { NanoforgeApplication } from "./nanoforge-application"; + +export class NanoforgeServer extends NanoforgeApplication {} diff --git a/packages/core-editor/src/common/context/contexts/application.editable-context.ts b/packages/core-editor/src/common/context/contexts/application.editable-context.ts new file mode 100644 index 00000000..492b797e --- /dev/null +++ b/packages/core-editor/src/common/context/contexts/application.editable-context.ts @@ -0,0 +1,20 @@ +import { ApplicationContext } from "@nanoforge-dev/common"; + +import { type EditableLibraryManager } from "../../library/manager/library.manager"; + +export class EditableApplicationContext extends ApplicationContext { + private _libraryManager: EditableLibraryManager; + + constructor(libraryManager: EditableLibraryManager) { + super(); + this._libraryManager = libraryManager; + } + + setDelta(delta: number) { + this._delta = delta; + } + + muteSoundLibraries(): void { + this._libraryManager.getMutableLibraries().forEach((lib) => lib.library.mute()); + } +} diff --git a/packages/core-editor/src/common/context/contexts/executions/clear.editable-context.ts b/packages/core-editor/src/common/context/contexts/executions/clear.editable-context.ts new file mode 100644 index 00000000..1081686d --- /dev/null +++ b/packages/core-editor/src/common/context/contexts/executions/clear.editable-context.ts @@ -0,0 +1,3 @@ +import { ClearContext } from "@nanoforge-dev/common"; + +export class EditableClearContext extends ClearContext {} diff --git a/packages/core-editor/src/common/context/contexts/executions/execution.editable-context.ts b/packages/core-editor/src/common/context/contexts/executions/execution.editable-context.ts new file mode 100644 index 00000000..e9b5b3de --- /dev/null +++ b/packages/core-editor/src/common/context/contexts/executions/execution.editable-context.ts @@ -0,0 +1,3 @@ +import { ExecutionContext } from "@nanoforge-dev/common"; + +export class EditableExecutionContext extends ExecutionContext {} diff --git a/packages/core-editor/src/common/context/contexts/executions/init.editable-context.ts b/packages/core-editor/src/common/context/contexts/executions/init.editable-context.ts new file mode 100644 index 00000000..7ce44e10 --- /dev/null +++ b/packages/core-editor/src/common/context/contexts/executions/init.editable-context.ts @@ -0,0 +1,3 @@ +import { InitContext } from "@nanoforge-dev/common"; + +export class EditableInitContext extends InitContext {} diff --git a/packages/core-editor/src/common/context/contexts/library.editable-context.ts b/packages/core-editor/src/common/context/contexts/library.editable-context.ts new file mode 100644 index 00000000..48f942e8 --- /dev/null +++ b/packages/core-editor/src/common/context/contexts/library.editable-context.ts @@ -0,0 +1,7 @@ +import { LibraryContext, type LibraryStatusEnum } from "@nanoforge-dev/common"; + +export class EditableLibraryContext extends LibraryContext { + setStatus(status: LibraryStatusEnum) { + this._status = status; + } +} diff --git a/packages/core-editor/src/common/library/manager/library.manager.ts b/packages/core-editor/src/common/library/manager/library.manager.ts new file mode 100644 index 00000000..6e17b321 --- /dev/null +++ b/packages/core-editor/src/common/library/manager/library.manager.ts @@ -0,0 +1,106 @@ +import { + ASSET_MANAGER_LIBRARY, + COMPONENT_SYSTEM_LIBRARY, + DefaultLibrariesEnum, + GRAPHICS_LIBRARY, + type IAssetManagerLibrary, + type IComponentSystemLibrary, + type IGraphicsLibrary, + type IInputLibrary, + type ILibrary, + type IMusicLibrary, + type IMutableLibrary, + INPUT_LIBRARY, + type INetworkLibrary, + type IRunnerLibrary, + type ISoundLibrary, + type LibraryHandle, + LibraryManager, + MUSIC_LIBRARY, + NETWORK_LIBRARY, + SOUND_LIBRARY, +} from "@nanoforge-dev/common"; + +import { EditableLibraryContext } from "../../context/contexts/library.editable-context"; +import { Relationship } from "../relationship-functions"; + +const hasMethod = (obj: any, method: string) => { + return typeof obj[method] === "function"; +}; + +export class EditableLibraryManager extends LibraryManager { + public set(sym: symbol, library: ILibrary) { + this.setNewLibrary(sym, library, new EditableLibraryContext()); + } + + public setComponentSystem(library: IComponentSystemLibrary): void { + this._set( + DefaultLibrariesEnum.COMPONENT_SYSTEM, + COMPONENT_SYSTEM_LIBRARY, + library, + new EditableLibraryContext(), + ); + } + + public setGraphics(library: IGraphicsLibrary): void { + this._set( + DefaultLibrariesEnum.GRAPHICS, + GRAPHICS_LIBRARY, + library, + new EditableLibraryContext(), + ); + } + + public setAssetManager(library: IAssetManagerLibrary): void { + this._set( + DefaultLibrariesEnum.ASSET_MANAGER, + ASSET_MANAGER_LIBRARY, + library, + new EditableLibraryContext(), + ); + } + + public setNetwork(library: INetworkLibrary): void { + this._set(DefaultLibrariesEnum.NETWORK, NETWORK_LIBRARY, library, new EditableLibraryContext()); + } + + public setInput(library: IInputLibrary): void { + this._set(DefaultLibrariesEnum.INPUT, INPUT_LIBRARY, library, new EditableLibraryContext()); + } + + public setSound(library: ISoundLibrary): void { + this._set(DefaultLibrariesEnum.SOUND, SOUND_LIBRARY, library, new EditableLibraryContext()); + } + + public setMusic(library: IMusicLibrary): void { + this._set(DefaultLibrariesEnum.MUSIC, MUSIC_LIBRARY, library, new EditableLibraryContext()); + } + + public getLibraries(): LibraryHandle[] { + return this._libraries; + } + + public getInitLibraries(): LibraryHandle[] { + return Relationship.getLibrariesByDependencies(this._libraries); + } + + public getExecutionLibraries(): LibraryHandle[] { + return Relationship.getLibrariesByRun(this._getRunnerLibraries()); + } + + public getClearLibraries(): LibraryHandle[] { + return Relationship.getLibrariesByDependencies(this._libraries, true); + } + + public getMutableLibraries(): LibraryHandle[] { + return this._libraries.filter( + (handle) => handle && hasMethod(handle.library, "mute"), + ) as LibraryHandle[]; + } + + private _getRunnerLibraries(): LibraryHandle[] { + return this._libraries.filter( + (handle) => handle && hasMethod(handle.library, "__run"), + ) as LibraryHandle[]; + } +} diff --git a/packages/core-editor/src/common/library/relationship-functions.ts b/packages/core-editor/src/common/library/relationship-functions.ts new file mode 100644 index 00000000..d9ff0f2b --- /dev/null +++ b/packages/core-editor/src/common/library/relationship-functions.ts @@ -0,0 +1,122 @@ +import { type ILibrary, type LibraryHandle } from "@nanoforge-dev/common"; + +class RelationshipStatic { + getLibrariesByDependencies(libraries: LibraryHandle[], reverse: boolean = false) { + let response: LibraryHandle[] = []; + for (const library of libraries) { + if (!library) continue; + response = this._pushLibraryWithDependencies(library, response, [], libraries); + } + + if (reverse) return response.reverse(); + return response; + } + + getLibrariesByRun(libraries: LibraryHandle[]) { + let response: LibraryHandle[] = []; + const dependencies = new Map>( + libraries.map((library) => [library.symbol, new Set()]), + ); + + for (const handle of libraries) { + const key = handle.symbol; + + for (const before of handle.library.__relationship.runBefore) { + this._pushToDependencies(key, before, dependencies); + } + for (const after of handle.library.__relationship.runAfter) { + this._pushToDependencies(after, key, dependencies); + } + } + + for (const library of libraries) { + response = this._pushLibraryWithDependenciesRun( + library, + dependencies, + response, + [], + libraries, + ); + } + return response; + } + + private _pushToDependencies( + key: symbol, + value: symbol, + dependencies: Map>, + ): void { + let curr = dependencies.get(key); + if (!curr) curr = new Set(); + curr.add(value); + dependencies.set(key, curr); + } + + private _pushLibraryWithDependenciesRun( + handle: LibraryHandle, + dependencies: Map>, + response: LibraryHandle[], + cache: symbol[], + libraries: LibraryHandle[], + ): LibraryHandle[] { + const key = handle.symbol; + if (this._symbolIsInList(key, response)) return response; + + if (cache.includes(key)) throw new Error("Circular dependencies !"); + + cache.push(key); + + const deps = dependencies.get(key); + if (!deps) throw new Error("Dependencies not found"); + + for (const dep of deps) { + if (this._symbolIsInList(dep, response)) continue; + + const depHandle = libraries.find((lib) => lib?.symbol === dep) as LibraryHandle; + if (!depHandle) throw new Error(`Cannot find library ${dep.toString()}`); + + response = this._pushLibraryWithDependenciesRun( + depHandle, + dependencies, + response, + cache, + libraries, + ); + } + cache.pop(); + + response.push(handle); + return response; + } + + private _pushLibraryWithDependencies( + handle: LibraryHandle, + response: LibraryHandle[], + cache: symbol[], + libraries: LibraryHandle[], + ): LibraryHandle[] { + if (this._symbolIsInList(handle.symbol, response)) return response; + + if (cache.includes(handle.symbol)) throw new Error("Circular dependencies !"); + + cache.push(handle.symbol); + for (const dep of handle.library.__relationship.dependencies) { + if (this._symbolIsInList(dep, response)) continue; + + const depHandle = libraries.find((lib) => lib?.symbol === dep) as LibraryHandle; + if (!depHandle) throw new Error(`Cannot find library ${dep.toString()}`); + + response = this._pushLibraryWithDependencies(depHandle, response, cache, libraries); + } + cache.pop(); + + response.push(handle); + return response; + } + + private _symbolIsInList(sym: symbol, libraries: LibraryHandle[]): boolean { + return libraries.some((lib) => lib.symbol === sym); + } +} + +export const Relationship = new RelationshipStatic(); diff --git a/packages/core-editor/src/config/config-registry.ts b/packages/core-editor/src/config/config-registry.ts new file mode 100644 index 00000000..946ed1dc --- /dev/null +++ b/packages/core-editor/src/config/config-registry.ts @@ -0,0 +1,19 @@ +import { plainToInstance } from "class-transformer"; +import { validate } from "class-validator"; + +export class ConfigRegistry { + private readonly _env: Record; + + constructor(env: Record) { + this._env = env; + } + + async registerConfig(config: new () => T): Promise { + const data = plainToInstance(config, this._env, { excludeExtraneousValues: true }); + const errors = await validate(data); + if (errors.length > 0) { + throw new Error(errors.toString()); + } + return data; + } +} diff --git a/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts new file mode 100644 index 00000000..3d9d15d1 --- /dev/null +++ b/packages/core-editor/src/core/core.ts @@ -0,0 +1,104 @@ +import { + ClearContext, + ClientLibraryManager, + Context, + type IEditorRunOptions, + type IRunnerLibrary, + InitContext, + type LibraryHandle, + LibraryStatusEnum, + NfNotInitializedException, +} from "@nanoforge-dev/common"; + +import { type ApplicationConfig } from "../application/application-config"; +import type { IApplicationOptions } from "../application/application-options.type"; +import { type EditableApplicationContext } from "../common/context/contexts/application.editable-context"; +import { EditableExecutionContext } from "../common/context/contexts/executions/execution.editable-context"; +import { type EditableLibraryContext } from "../common/context/contexts/library.editable-context"; +import { ConfigRegistry } from "../config/config-registry"; + +export class Core { + private readonly config: ApplicationConfig; + private readonly context: EditableApplicationContext; + private options?: IApplicationOptions; + private _configRegistry?: ConfigRegistry; + + constructor(config: ApplicationConfig, context: EditableApplicationContext) { + this.config = config; + this.context = context; + } + + public async init(options: IEditorRunOptions, appOptions: IApplicationOptions): Promise { + this.options = appOptions; + this._configRegistry = new ConfigRegistry(options.env); + await this.runInit(this.getInitContext(options)); + } + + public async run(): Promise { + if (!this.options) throw new NfNotInitializedException("Core"); + + const context = this.getExecutionContext(); + const clientContext = this.getClientContext(); + const libraries = this.config.libraryManager.getExecutionLibraries(); + + const runner = async (delta: number) => { + this.context.setDelta(delta); + await this.runExecute(clientContext, libraries); + }; + + const tickLengthMs = 1000 / this.options.tickRate; + let previousTick = Date.now(); + + const render = async () => { + if (!context.application.isRunning) { + await this.runClear(this.getClearContext()); + return; + } + const tickStart = Date.now(); + await runner(tickStart - previousTick); + previousTick = tickStart; + setTimeout(render, tickLengthMs + tickStart - Date.now()); + }; + + context.application.setIsRunning(true); + setTimeout(render); + } + + private getInitContext(options: IEditorRunOptions): InitContext { + if (!this._configRegistry) throw new NfNotInitializedException("Core"); + + return new InitContext(this.context, this.config.libraryManager, this._configRegistry, options); + } + + private getExecutionContext(): EditableExecutionContext { + return new EditableExecutionContext(this.context, this.config.libraryManager); + } + + private getClearContext(): ClearContext { + return new ClearContext(this.context, this.config.libraryManager); + } + + private getClientContext(): Context { + return new Context(this.context, new ClientLibraryManager(this.config.libraryManager)); + } + + private async runInit(context: InitContext): Promise { + for (const handle of this.config.libraryManager.getInitLibraries()) { + await handle.library.__init(context); + (handle.context as EditableLibraryContext).setStatus(LibraryStatusEnum.LOADED); + } + } + + private async runExecute(context: Context, libraries: LibraryHandle[]) { + for (const handle of libraries) { + await handle.library.__run(context); + } + } + + private async runClear(context: ClearContext) { + for (const handle of this.config.libraryManager.getClearLibraries()) { + await handle.library.__clear(context); + (handle.context as EditableLibraryContext).setStatus(LibraryStatusEnum.CLEAR); + } + } +} diff --git a/packages/core-editor/src/index.ts b/packages/core-editor/src/index.ts new file mode 100644 index 00000000..4273d62e --- /dev/null +++ b/packages/core-editor/src/index.ts @@ -0,0 +1,4 @@ +export * from "./application/nanoforge-factory"; + +export type { NanoforgeClient } from "./application/nanoforge-client"; +export type { NanoforgeServer } from "./application/nanoforge-server"; diff --git a/packages/core-editor/test/config-registry.spec.ts b/packages/core-editor/test/config-registry.spec.ts new file mode 100644 index 00000000..0a1e2844 --- /dev/null +++ b/packages/core-editor/test/config-registry.spec.ts @@ -0,0 +1,54 @@ +import { Expose } from "class-transformer"; +import { IsString } from "class-validator"; +import { describe, expect, it } from "vitest"; + +import { ConfigRegistry } from "../src/config/config-registry"; + +class ValidConfig { + @Expose() + @IsString() + name!: string; +} + +class OptionalConfig { + @Expose() + @IsString() + name!: string; + + @Expose() + host?: string; +} + +describe("ConfigRegistry", () => { + describe("registerConfig", () => { + it("should return a transformed config instance when env is valid", async () => { + const registry = new ConfigRegistry({ name: "hello" }); + const config = await registry.registerConfig(ValidConfig); + expect(config).toBeInstanceOf(ValidConfig); + expect(config.name).toBe("hello"); + }); + + it("should exclude values not decorated with @Expose", async () => { + const registry = new ConfigRegistry({ name: "hello", extra: "ignored" }); + const config = await registry.registerConfig(ValidConfig); + expect((config as any)["extra"]).toBeUndefined(); + }); + + it("should throw when a required field is missing", async () => { + const registry = new ConfigRegistry({}); + await expect(registry.registerConfig(ValidConfig)).rejects.toThrow(); + }); + + it("should throw when a field has the wrong type", async () => { + const registry = new ConfigRegistry({ name: 42 }); + await expect(registry.registerConfig(ValidConfig)).rejects.toThrow(); + }); + + it("should map multiple env fields correctly", async () => { + const registry = new ConfigRegistry({ name: "world", host: "localhost" }); + const config = await registry.registerConfig(OptionalConfig); + expect(config.name).toBe("world"); + expect(config.host).toBe("localhost"); + }); + }); +}); diff --git a/packages/core-editor/test/editable-library-manager.spec.ts b/packages/core-editor/test/editable-library-manager.spec.ts new file mode 100644 index 00000000..0e808baa --- /dev/null +++ b/packages/core-editor/test/editable-library-manager.spec.ts @@ -0,0 +1,182 @@ +import { COMPONENT_SYSTEM_LIBRARY, type ILibrary, LibraryStatusEnum } from "@nanoforge-dev/common"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { Library } from "../../common/src/library/libraries/library"; +import { EditableLibraryManager } from "../src/common/library/manager/library.manager"; + +class StubLibrary extends Library { + private readonly _name: string; + + constructor(name: string, options?: ConstructorParameters[0]) { + super(options); + this._name = name; + } + + get __name(): string { + return this._name; + } +} + +class StubRunnerLibrary extends StubLibrary { + async __run(): Promise {} +} + +class StubMutableLibrary extends StubLibrary { + mute(): void {} +} + +describe("EditableLibraryManager", () => { + let manager: EditableLibraryManager; + + beforeEach(() => { + manager = new EditableLibraryManager(); + }); + + describe("typed setters and getters", () => { + it("should store and retrieve a component system library", () => { + const lib = new StubLibrary("ComponentSystem"); + manager.setComponentSystem(lib as any); + expect(manager.getComponentSystem().library).toBe(lib); + }); + + it("should store and retrieve a graphics library", () => { + const lib = new StubLibrary("Graphics"); + manager.setGraphics(lib as any); + expect(manager.getGraphics().library).toBe(lib); + }); + + it("should store and retrieve an asset manager library", () => { + const lib = new StubLibrary("AssetManager"); + manager.setAssetManager(lib as any); + expect(manager.getAssetManager().library).toBe(lib); + }); + + it("should store and retrieve a network library", () => { + const lib = new StubLibrary("Network"); + manager.setNetwork(lib as any); + expect(manager.getNetwork().library).toBe(lib); + }); + + it("should store and retrieve an input library", () => { + const lib = new StubLibrary("Input"); + manager.setInput(lib as any); + expect(manager.getInput().library).toBe(lib); + }); + + it("should store and retrieve a sound library", () => { + const lib = new StubLibrary("Sound"); + manager.setSound(lib as any); + expect(manager.getSound().library).toBe(lib); + }); + + it("should store and retrieve a music library", () => { + const lib = new StubLibrary("Music"); + manager.setMusic(lib as any); + expect(manager.getMusic().library).toBe(lib); + }); + + it("should throw when getting a typed library that was not set", () => { + expect(() => manager.getComponentSystem()).toThrow(); + }); + }); + + describe("set and get (custom symbol)", () => { + it("should store and retrieve a library by Symbol.for key", () => { + const sym = Symbol.for("customLib"); + const lib = new StubLibrary("Custom"); + + manager.setAssetManager(new StubLibrary("Asset") as any); + manager.set(sym, lib as unknown as ILibrary); + + expect(manager.get(sym).library).toBe(lib); + }); + }); + + describe("getLibraries", () => { + it("should return the list of all set libraries", () => { + const lib = new StubLibrary("ComponentSystem"); + manager.setComponentSystem(lib as any); + const libs = manager.getLibraries().filter(Boolean); + expect(libs.some((h) => h.library === (lib as unknown as ILibrary))).toBe(true); + }); + }); + + describe("getInitLibraries", () => { + it("should return libraries in dependency order", () => { + const libA = new StubLibrary("A", { dependencies: [COMPONENT_SYSTEM_LIBRARY] }); + const libB = new StubLibrary("B"); + + manager.setComponentSystem(libB as any); + manager.setGraphics(libA as any); + + const order = manager.getInitLibraries().map((h) => h.library.__name); + const idxB = order.indexOf("B"); + const idxA = order.indexOf("A"); + + expect(idxB).toBeLessThan(idxA); + }); + + it("should return all set libraries", () => { + manager.setAssetManager(new StubLibrary("Asset") as any); + manager.setGraphics(new StubLibrary("Graphics") as any); + + expect(manager.getInitLibraries().length).toBe(2); + }); + }); + + describe("getClearLibraries", () => { + it("should return libraries in reverse dependency order", () => { + const libA = new StubLibrary("A", { dependencies: [COMPONENT_SYSTEM_LIBRARY] }); + const libB = new StubLibrary("B"); + + manager.setComponentSystem(libB as any); + manager.setGraphics(libA as any); + + const order = manager.getClearLibraries().map((h) => h.library.__name); + const idxA = order.indexOf("A"); + const idxB = order.indexOf("B"); + + expect(idxA).toBeLessThan(idxB); + }); + }); + + describe("getExecutionLibraries", () => { + it("should only return libraries that implement __run", () => { + manager.setComponentSystem(new StubLibrary("NotARunner") as any); + manager.setGraphics(new StubRunnerLibrary("Runner") as any); + + const runners = manager.getExecutionLibraries(); + expect(runners.every((h) => typeof (h.library as any).__run === "function")).toBe(true); + expect(runners.some((h) => h.library.__name === "Runner")).toBe(true); + expect(runners.some((h) => h.library.__name === "NotARunner")).toBe(false); + }); + + it("should return empty when no runner libraries are set", () => { + manager.setComponentSystem(new StubLibrary("Static") as any); + expect(manager.getExecutionLibraries()).toHaveLength(0); + }); + }); + + describe("getMutableLibraries", () => { + it("should only return libraries that implement mute", () => { + manager.setSound(new StubMutableLibrary("MutableSound") as any); + manager.setGraphics(new StubLibrary("NonMutableGraphics") as any); + + const mutable = manager.getMutableLibraries(); + expect(mutable.some((h) => h.library.__name === "MutableSound")).toBe(true); + expect(mutable.some((h) => h.library.__name === "NonMutableGraphics")).toBe(false); + }); + + it("should return empty when no mutable libraries are set", () => { + manager.setGraphics(new StubLibrary("Graphics") as any); + expect(manager.getMutableLibraries()).toHaveLength(0); + }); + }); + + describe("library context status", () => { + it("should start with UNLOADED status", () => { + manager.setComponentSystem(new StubLibrary("Comp") as any); + expect(manager.getComponentSystem().context.status).toBe(LibraryStatusEnum.UNLOADED); + }); + }); +}); diff --git a/packages/core-editor/test/relationship.spec.ts b/packages/core-editor/test/relationship.spec.ts new file mode 100644 index 00000000..776fa493 --- /dev/null +++ b/packages/core-editor/test/relationship.spec.ts @@ -0,0 +1,135 @@ +import { type ILibrary, LibraryContext, LibraryHandle } from "@nanoforge-dev/common"; +import { describe, expect, it } from "vitest"; + +import { Library } from "../../common/src/library/libraries/library"; +import { Relationship } from "../src/common/library/relationship-functions"; + +class StubLibrary extends Library { + private readonly _name: string; + + constructor(name: string, options?: ConstructorParameters[0]) { + super(options); + this._name = name; + } + + get __name(): string { + return this._name; + } +} + +const makeHandle = ( + sym: symbol, + name: string, + options?: ConstructorParameters[0], +): LibraryHandle => { + return new LibraryHandle( + sym, + new StubLibrary(name, options) as unknown as ILibrary, + new LibraryContext(), + ); +}; + +describe("Relationship.getLibrariesByDependencies", () => { + it("should return libraries in same order when no dependencies are declared", () => { + const symA = Symbol("A"); + const symB = Symbol("B"); + const handleA = makeHandle(symA, "A"); + const handleB = makeHandle(symB, "B"); + + const result = Relationship.getLibrariesByDependencies([handleA, handleB]); + expect(result.map((h) => h.library.__name)).toEqual(["A", "B"]); + }); + + it("should put a dependency before the library that depends on it", () => { + const symA = Symbol("A"); + const symB = Symbol("B"); + const handleB = makeHandle(symB, "B"); + const handleA = makeHandle(symA, "A", { dependencies: [symB] }); + + const result = Relationship.getLibrariesByDependencies([handleA, handleB]); + const names = result.map((h) => h.library.__name); + expect(names.indexOf("B")).toBeLessThan(names.indexOf("A")); + }); + + it("should not duplicate a shared dependency", () => { + const symDep = Symbol("Dep"); + const symA = Symbol("A"); + const symB = Symbol("B"); + const handleDep = makeHandle(symDep, "Dep"); + const handleA = makeHandle(symA, "A", { dependencies: [symDep] }); + const handleB = makeHandle(symB, "B", { dependencies: [symDep] }); + + const result = Relationship.getLibrariesByDependencies([handleA, handleB, handleDep]); + const names = result.map((h) => h.library.__name); + expect(names.filter((n) => n === "Dep")).toHaveLength(1); + }); + + it("should return libraries in reverse dependency order when reverse=true", () => { + const symA = Symbol("A"); + const symB = Symbol("B"); + const handleB = makeHandle(symB, "B"); + const handleA = makeHandle(symA, "A", { dependencies: [symB] }); + + const result = Relationship.getLibrariesByDependencies([handleA, handleB], true); + const names = result.map((h) => h.library.__name); + expect(names.indexOf("A")).toBeLessThan(names.indexOf("B")); + }); + + it("should throw on circular dependencies", () => { + const symA = Symbol("A"); + const symB = Symbol("B"); + const handleA = makeHandle(symA, "A", { dependencies: [symB] }); + const handleB = makeHandle(symB, "B", { dependencies: [symA] }); + + expect(() => Relationship.getLibrariesByDependencies([handleA, handleB])).toThrow( + /[Cc]ircular/, + ); + }); +}); + +describe("Relationship.getLibrariesByRun", () => { + it("should return all runner libraries when no ordering is specified", () => { + const symA = Symbol("A"); + const symB = Symbol("B"); + const handleA = makeHandle(symA, "A"); + const handleB = makeHandle(symB, "B"); + + const result = Relationship.getLibrariesByRun([handleA, handleB]); + expect(result).toHaveLength(2); + }); + + it("should place a library before another when runBefore is set", () => { + const symA = Symbol("A"); + const symB = Symbol("B"); + const handleA = makeHandle(symA, "A", { runBefore: [symB] }); + const handleB = makeHandle(symB, "B"); + + const result = Relationship.getLibrariesByRun([handleA, handleB]); + const names = result.map((h) => h.library.__name); + expect(names.indexOf("B")).toBeLessThan(names.indexOf("A")); + }); + + it("should place a library after another when runAfter is set", () => { + const symA = Symbol("A"); + const symB = Symbol("B"); + const handleA = makeHandle(symA, "A"); + const handleB = makeHandle(symB, "B", { runAfter: [symA] }); + + const result = Relationship.getLibrariesByRun([handleA, handleB]); + const names = result.map((h) => h.library.__name); + expect(names.indexOf("B")).toBeLessThan(names.indexOf("A")); + }); + + it("should throw on circular run dependencies", () => { + const symA = Symbol("A"); + const symB = Symbol("B"); + const handleA = makeHandle(symA, "A", { runBefore: [symB] }); + const handleB = makeHandle(symB, "B", { runBefore: [symA] }); + + expect(() => Relationship.getLibrariesByRun([handleA, handleB])).toThrow(/[Cc]ircular/); + }); + + it("should return empty array for empty input", () => { + expect(Relationship.getLibrariesByRun([])).toHaveLength(0); + }); +}); diff --git a/packages/core-editor/tsconfig.json b/packages/core-editor/tsconfig.json new file mode 100644 index 00000000..9e6d724b --- /dev/null +++ b/packages/core-editor/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/core-editor/tsconfig.spec.json b/packages/core-editor/tsconfig.spec.json new file mode 100644 index 00000000..8270caba --- /dev/null +++ b/packages/core-editor/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "skipLibCheck": true + }, + "include": ["test/**/*.spec.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/core-editor/tsup.config.ts b/packages/core-editor/tsup.config.ts new file mode 100644 index 00000000..f3b6e6ce --- /dev/null +++ b/packages/core-editor/tsup.config.ts @@ -0,0 +1,3 @@ +import { createTsupConfig } from "../../tsup.config.js"; + +export default [createTsupConfig()]; diff --git a/packages/core-editor/typedoc.json b/packages/core-editor/typedoc.json new file mode 100644 index 00000000..7e2ec1a3 --- /dev/null +++ b/packages/core-editor/typedoc.json @@ -0,0 +1,5 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": ["src/index.ts"], + "skipErrorChecking": true +} From 8e21bc9c01f7ba53cf0f3557b0a7d1ebc3e02945 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Wed, 25 Mar 2026 15:43:47 +0900 Subject: [PATCH 03/14] feat(core-editor): hot reload from save --- packages/common/src/options/index.ts | 9 +--- .../common/src/options/types/options.type.ts | 21 -------- packages/core-editor/package.json | 2 + .../src/application/application-config.ts | 4 +- .../src/application/nanoforge-application.ts | 2 +- .../src/common/context/options.type.ts | 20 +++++++ .../src/common/context}/save.type.ts | 6 +-- packages/core-editor/src/core/core.ts | 10 +++- .../core-editor/src/editor/core-editor.ts | 37 +++++++++++++ .../core-editor/test/editor-feature.spec.ts | 54 +++++++++++++++++++ pnpm-lock.yaml | 52 ++++++++++++++++++ 11 files changed, 180 insertions(+), 37 deletions(-) create mode 100644 packages/core-editor/src/common/context/options.type.ts rename packages/{common/src/options/types => core-editor/src/common/context}/save.type.ts (89%) create mode 100644 packages/core-editor/src/editor/core-editor.ts create mode 100644 packages/core-editor/test/editor-feature.spec.ts diff --git a/packages/common/src/options/index.ts b/packages/common/src/options/index.ts index 1bd44e31..32ab2502 100644 --- a/packages/common/src/options/index.ts +++ b/packages/common/src/options/index.ts @@ -1,8 +1 @@ -export type { - IRunClientOptions, - IRunOptions, - IRunServerOptions, - IEditorRunClientOptions, - IEditorRunOptions, - IEditorRunServerOptions, -} from "./types/options.type"; +export type { IRunClientOptions, IRunOptions, IRunServerOptions } from "./types/options.type"; diff --git a/packages/common/src/options/types/options.type.ts b/packages/common/src/options/types/options.type.ts index 5cab0ac1..3ed47674 100644 --- a/packages/common/src/options/types/options.type.ts +++ b/packages/common/src/options/types/options.type.ts @@ -1,5 +1,3 @@ -import { type Save } from "./save.type"; - export type IRunOptions = IRunClientOptions | IRunServerOptions; export interface IRunClientOptions { @@ -12,22 +10,3 @@ export interface IRunServerOptions { files: Map; env: Record; } - -export type IEditorRunOptions = IEditorRunClientOptions | IEditorRunServerOptions; - -export interface IEditorRunClientOptions { - canvas: HTMLCanvasElement; - files: Map; - env: Record; - editor: { - save: Save; - }; -} -export interface IEditorRunServerOptions { - canvas: HTMLCanvasElement; - files: Map; - env: Record; - editor: { - save: Save; - }; -} diff --git a/packages/core-editor/package.json b/packages/core-editor/package.json index 90e0540a..cf532b87 100644 --- a/packages/core-editor/package.json +++ b/packages/core-editor/package.json @@ -58,6 +58,8 @@ "dependencies": { "@nanoforge-dev/asset-manager": "workspace:*", "@nanoforge-dev/common": "workspace:*", + "@nanoforge-dev/ecs-client": "workspace:*", + "@nanoforge-dev/ecs-server": "workspace:*", "@nanoforge-dev/input": "workspace:*", "class-transformer": "catalog:config", "class-validator": "catalog:config" diff --git a/packages/core-editor/src/application/application-config.ts b/packages/core-editor/src/application/application-config.ts index 82d28bed..7eeede0c 100644 --- a/packages/core-editor/src/application/application-config.ts +++ b/packages/core-editor/src/application/application-config.ts @@ -31,8 +31,8 @@ export class ApplicationConfig { this._libraryManager.set(sym, library); } - public getComponentSystemLibrary() { - return this._libraryManager.getComponentSystem(); + public getComponentSystemLibrary() { + return this._libraryManager.getComponentSystem(); } public useComponentSystemLibrary(library: IComponentSystemLibrary) { diff --git a/packages/core-editor/src/application/nanoforge-application.ts b/packages/core-editor/src/application/nanoforge-application.ts index b35db5ae..81f6ea6e 100644 --- a/packages/core-editor/src/application/nanoforge-application.ts +++ b/packages/core-editor/src/application/nanoforge-application.ts @@ -1,13 +1,13 @@ import { type IAssetManagerLibrary, type IComponentSystemLibrary, - type IEditorRunOptions, type ILibrary, type INetworkLibrary, NfNotInitializedException, } from "@nanoforge-dev/common"; import { EditableApplicationContext } from "../common/context/contexts/application.editable-context"; +import { type IEditorRunOptions } from "../common/context/options.type"; import { Core } from "../core/core"; import { ApplicationConfig } from "./application-config"; import type { IApplicationOptions } from "./application-options.type"; diff --git a/packages/core-editor/src/common/context/options.type.ts b/packages/core-editor/src/common/context/options.type.ts new file mode 100644 index 00000000..4aa66560 --- /dev/null +++ b/packages/core-editor/src/common/context/options.type.ts @@ -0,0 +1,20 @@ +import { type Save } from "./save.type"; + +export type IEditorRunOptions = IEditorRunClientOptions | IEditorRunServerOptions; + +export interface IEditorRunClientOptions { + canvas: HTMLCanvasElement; + files: Map; + env: Record; + editor: { + save: Save; + }; +} +export interface IEditorRunServerOptions { + canvas: HTMLCanvasElement; + files: Map; + env: Record; + editor: { + save: Save; + }; +} diff --git a/packages/common/src/options/types/save.type.ts b/packages/core-editor/src/common/context/save.type.ts similarity index 89% rename from packages/common/src/options/types/save.type.ts rename to packages/core-editor/src/common/context/save.type.ts index 94371ad1..36372a33 100644 --- a/packages/common/src/options/types/save.type.ts +++ b/packages/core-editor/src/common/context/save.type.ts @@ -17,6 +17,7 @@ export interface SaveLibrary { export interface SaveComponent { name: string; path: string; + paramsNames: string[]; } export interface SaveSystem { @@ -26,10 +27,7 @@ export interface SaveSystem { export interface SaveEntity { id: string; - components: { - name: string; - params: string[]; - }[]; + components: Record>; } export interface Save { diff --git a/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts index 3d9d15d1..437da6b7 100644 --- a/packages/core-editor/src/core/core.ts +++ b/packages/core-editor/src/core/core.ts @@ -2,25 +2,29 @@ import { ClearContext, ClientLibraryManager, Context, - type IEditorRunOptions, type IRunnerLibrary, InitContext, type LibraryHandle, LibraryStatusEnum, NfNotInitializedException, } from "@nanoforge-dev/common"; +import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; +import { type ECSServerLibrary } from "@nanoforge-dev/ecs-server"; import { type ApplicationConfig } from "../application/application-config"; import type { IApplicationOptions } from "../application/application-options.type"; import { type EditableApplicationContext } from "../common/context/contexts/application.editable-context"; import { EditableExecutionContext } from "../common/context/contexts/executions/execution.editable-context"; import { type EditableLibraryContext } from "../common/context/contexts/library.editable-context"; +import { type IEditorRunOptions } from "../common/context/options.type"; import { ConfigRegistry } from "../config/config-registry"; +import { CoreEditor } from "../editor/core-editor"; export class Core { private readonly config: ApplicationConfig; private readonly context: EditableApplicationContext; private options?: IApplicationOptions; + public editor?: CoreEditor; private _configRegistry?: ConfigRegistry; constructor(config: ApplicationConfig, context: EditableApplicationContext) { @@ -30,6 +34,10 @@ export class Core { public async init(options: IEditorRunOptions, appOptions: IApplicationOptions): Promise { this.options = appOptions; + this.editor = new CoreEditor( + options.editor, + this.config.getComponentSystemLibrary().library, + ); this._configRegistry = new ConfigRegistry(options.env); await this.runInit(this.getInitContext(options)); } diff --git a/packages/core-editor/src/editor/core-editor.ts b/packages/core-editor/src/editor/core-editor.ts new file mode 100644 index 00000000..23fed3b8 --- /dev/null +++ b/packages/core-editor/src/editor/core-editor.ts @@ -0,0 +1,37 @@ +import { NfNotFound } from "@nanoforge-dev/common"; +import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; +import { type ECSServerLibrary } from "@nanoforge-dev/ecs-server"; + +import type { IEditorRunOptions } from "../common/context/options.type"; +import type { SaveComponent, SaveEntity } from "../common/context/save.type"; + +export class CoreEditor { + private editor: IEditorRunOptions["editor"]; + private ecsLibrary: ECSClientLibrary | ECSServerLibrary; + constructor( + editor: IEditorRunOptions["editor"], + ecsLibrary: ECSClientLibrary | ECSServerLibrary, + ) { + this.editor = editor; + this.ecsLibrary = ecsLibrary; + } + + public askEntityHotReload(saveComponents: SaveComponent[], entityToReload: SaveEntity[]): void { + const reg = this.ecsLibrary.registry; + entityToReload.forEach(({ id, components }) => { + Object.entries(components).forEach(([componentName, params]) => { + const ogComponent = saveComponents.find(({ name: paramName }) => { + if (!ogComponent) { + throw new NfNotFound("Component: " + componentName + " not found in saved components"); + } + return paramName == componentName; + }); + const ecsComponent = reg.getComponents({ name: componentName }).get(Number(id)); + Object.entries(params).forEach(([paramName, paramValue]) => { + ecsComponent[paramName] = paramValue; + }); + reg.getComponents({ name: componentName }).set(Number(id), ecsComponent); + }); + }); + } +} diff --git a/packages/core-editor/test/editor-feature.spec.ts b/packages/core-editor/test/editor-feature.spec.ts new file mode 100644 index 00000000..0a1e2844 --- /dev/null +++ b/packages/core-editor/test/editor-feature.spec.ts @@ -0,0 +1,54 @@ +import { Expose } from "class-transformer"; +import { IsString } from "class-validator"; +import { describe, expect, it } from "vitest"; + +import { ConfigRegistry } from "../src/config/config-registry"; + +class ValidConfig { + @Expose() + @IsString() + name!: string; +} + +class OptionalConfig { + @Expose() + @IsString() + name!: string; + + @Expose() + host?: string; +} + +describe("ConfigRegistry", () => { + describe("registerConfig", () => { + it("should return a transformed config instance when env is valid", async () => { + const registry = new ConfigRegistry({ name: "hello" }); + const config = await registry.registerConfig(ValidConfig); + expect(config).toBeInstanceOf(ValidConfig); + expect(config.name).toBe("hello"); + }); + + it("should exclude values not decorated with @Expose", async () => { + const registry = new ConfigRegistry({ name: "hello", extra: "ignored" }); + const config = await registry.registerConfig(ValidConfig); + expect((config as any)["extra"]).toBeUndefined(); + }); + + it("should throw when a required field is missing", async () => { + const registry = new ConfigRegistry({}); + await expect(registry.registerConfig(ValidConfig)).rejects.toThrow(); + }); + + it("should throw when a field has the wrong type", async () => { + const registry = new ConfigRegistry({ name: 42 }); + await expect(registry.registerConfig(ValidConfig)).rejects.toThrow(); + }); + + it("should map multiple env fields correctly", async () => { + const registry = new ConfigRegistry({ name: "world", host: "localhost" }); + const config = await registry.registerConfig(OptionalConfig); + expect(config.name).toBe("world"); + expect(config.host).toBe("localhost"); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ed1b563..2913b6c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,6 +420,58 @@ importers: specifier: catalog:test version: 4.0.18(@types/node@25.3.5)(jiti@2.6.1)(yaml@2.8.2) + packages/core-editor: + dependencies: + '@nanoforge-dev/asset-manager': + specifier: workspace:* + version: link:../asset-manager + '@nanoforge-dev/common': + specifier: workspace:* + version: link:../common + '@nanoforge-dev/ecs-client': + specifier: workspace:* + version: link:../ecs-client + '@nanoforge-dev/ecs-server': + specifier: workspace:* + version: link:../ecs-server + '@nanoforge-dev/input': + specifier: workspace:* + version: link:../input + class-transformer: + specifier: catalog:config + version: 0.5.1 + class-validator: + specifier: catalog:config + version: 0.14.4 + devDependencies: + '@favware/cliff-jumper': + specifier: catalog:ci + version: 6.0.0 + '@nanoforge-dev/utils-eslint-config': + specifier: workspace:* + version: link:../../utils/eslint-config + '@nanoforge-dev/utils-prettier-config': + specifier: workspace:* + version: link:../../utils/prettier-config + '@trivago/prettier-plugin-sort-imports': + specifier: catalog:lint + version: 6.0.2(prettier@3.8.1) + eslint: + specifier: catalog:lint + version: 10.0.2(jiti@2.6.1) + prettier: + specifier: catalog:lint + version: 3.8.1 + tsup: + specifier: catalog:build + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: catalog:core + version: 5.9.3 + vitest: + specifier: catalog:test + version: 4.0.18(@types/node@25.3.5)(jiti@2.6.1)(yaml@2.8.2) + packages/ecs-client: dependencies: '@nanoforge-dev/common': From 9096b78a43b05fe7a5ce98674c9788d3779ce52c Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Wed, 25 Mar 2026 16:01:28 +0900 Subject: [PATCH 04/14] feat(core-editor): hot reload from save --- packages/core-editor/src/core/core.ts | 1 - packages/core-editor/src/editor/core-editor.ts | 8 +------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts index 437da6b7..9ea80077 100644 --- a/packages/core-editor/src/core/core.ts +++ b/packages/core-editor/src/core/core.ts @@ -35,7 +35,6 @@ export class Core { public async init(options: IEditorRunOptions, appOptions: IApplicationOptions): Promise { this.options = appOptions; this.editor = new CoreEditor( - options.editor, this.config.getComponentSystemLibrary().library, ); this._configRegistry = new ConfigRegistry(options.env); diff --git a/packages/core-editor/src/editor/core-editor.ts b/packages/core-editor/src/editor/core-editor.ts index 23fed3b8..c739ee34 100644 --- a/packages/core-editor/src/editor/core-editor.ts +++ b/packages/core-editor/src/editor/core-editor.ts @@ -2,17 +2,11 @@ import { NfNotFound } from "@nanoforge-dev/common"; import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; import { type ECSServerLibrary } from "@nanoforge-dev/ecs-server"; -import type { IEditorRunOptions } from "../common/context/options.type"; import type { SaveComponent, SaveEntity } from "../common/context/save.type"; export class CoreEditor { - private editor: IEditorRunOptions["editor"]; private ecsLibrary: ECSClientLibrary | ECSServerLibrary; - constructor( - editor: IEditorRunOptions["editor"], - ecsLibrary: ECSClientLibrary | ECSServerLibrary, - ) { - this.editor = editor; + constructor(ecsLibrary: ECSClientLibrary | ECSServerLibrary) { this.ecsLibrary = ecsLibrary; } From ec6af58068d2a10b8e9138c598c39df88e78d4f4 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Wed, 25 Mar 2026 16:08:47 +0900 Subject: [PATCH 05/14] feat(core-editor): hot reload from save --- packages/core-editor/src/editor/core-editor.ts | 6 +++--- packages/ecs-lib/lib/libecs.d.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core-editor/src/editor/core-editor.ts b/packages/core-editor/src/editor/core-editor.ts index c739ee34..1acf374c 100644 --- a/packages/core-editor/src/editor/core-editor.ts +++ b/packages/core-editor/src/editor/core-editor.ts @@ -15,11 +15,11 @@ export class CoreEditor { entityToReload.forEach(({ id, components }) => { Object.entries(components).forEach(([componentName, params]) => { const ogComponent = saveComponents.find(({ name: paramName }) => { - if (!ogComponent) { - throw new NfNotFound("Component: " + componentName + " not found in saved components"); - } return paramName == componentName; }); + if (!ogComponent) { + throw new NfNotFound("Component: " + componentName + " not found in saved components"); + } const ecsComponent = reg.getComponents({ name: componentName }).get(Number(id)); Object.entries(params).forEach(([paramName, paramValue]) => { ecsComponent[paramName] = paramValue; diff --git a/packages/ecs-lib/lib/libecs.d.ts b/packages/ecs-lib/lib/libecs.d.ts index 6ff945f1..d4d8cf0b 100644 --- a/packages/ecs-lib/lib/libecs.d.ts +++ b/packages/ecs-lib/lib/libecs.d.ts @@ -16,7 +16,7 @@ export interface ClassHandle { [Symbol.dispose](): void; clone(): this; } -export interface container extends ClassHandle { +export interface container extends ClassHandle, Iterable { size(): number; get(_0: number): any | undefined | undefined; push_back(_0?: any): void; From 24a8782b08644fe19f7a0e1a33658979a98908fe Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Thu, 26 Mar 2026 10:19:26 +0900 Subject: [PATCH 06/14] feat(core-editor): ask hot reload --- packages/core-editor/package.json | 1 - packages/core-editor/src/core/core.ts | 5 +- .../core-editor/src/editor/core-editor.ts | 25 ++- .../core-editor/test/editor-feature.spec.ts | 157 +++++++++++++----- packages/ecs-client/src/index.ts | 2 + packages/ecs-lib/src/index.ts | 2 +- packages/ecs-server/src/index.ts | 2 + pnpm-lock.yaml | 3 - 8 files changed, 136 insertions(+), 61 deletions(-) diff --git a/packages/core-editor/package.json b/packages/core-editor/package.json index cf532b87..8f9080bf 100644 --- a/packages/core-editor/package.json +++ b/packages/core-editor/package.json @@ -59,7 +59,6 @@ "@nanoforge-dev/asset-manager": "workspace:*", "@nanoforge-dev/common": "workspace:*", "@nanoforge-dev/ecs-client": "workspace:*", - "@nanoforge-dev/ecs-server": "workspace:*", "@nanoforge-dev/input": "workspace:*", "class-transformer": "catalog:config", "class-validator": "catalog:config" diff --git a/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts index 9ea80077..657d810d 100644 --- a/packages/core-editor/src/core/core.ts +++ b/packages/core-editor/src/core/core.ts @@ -9,7 +9,6 @@ import { NfNotInitializedException, } from "@nanoforge-dev/common"; import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; -import { type ECSServerLibrary } from "@nanoforge-dev/ecs-server"; import { type ApplicationConfig } from "../application/application-config"; import type { IApplicationOptions } from "../application/application-options.type"; @@ -34,9 +33,7 @@ export class Core { public async init(options: IEditorRunOptions, appOptions: IApplicationOptions): Promise { this.options = appOptions; - this.editor = new CoreEditor( - this.config.getComponentSystemLibrary().library, - ); + this.editor = new CoreEditor(this.config.getComponentSystemLibrary().library); this._configRegistry = new ConfigRegistry(options.env); await this.runInit(this.getInitContext(options)); } diff --git a/packages/core-editor/src/editor/core-editor.ts b/packages/core-editor/src/editor/core-editor.ts index 1acf374c..5b0bc74c 100644 --- a/packages/core-editor/src/editor/core-editor.ts +++ b/packages/core-editor/src/editor/core-editor.ts @@ -1,16 +1,15 @@ import { NfNotFound } from "@nanoforge-dev/common"; -import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; -import { type ECSServerLibrary } from "@nanoforge-dev/ecs-server"; +import { type ECSClientLibrary, type Entity } from "@nanoforge-dev/ecs-client"; import type { SaveComponent, SaveEntity } from "../common/context/save.type"; export class CoreEditor { - private ecsLibrary: ECSClientLibrary | ECSServerLibrary; - constructor(ecsLibrary: ECSClientLibrary | ECSServerLibrary) { + private ecsLibrary: ECSClientLibrary; + constructor(ecsLibrary: ECSClientLibrary) { this.ecsLibrary = ecsLibrary; } - public askEntityHotReload(saveComponents: SaveComponent[], entityToReload: SaveEntity[]): void { + public askEntitiesHotReload(saveComponents: SaveComponent[], entityToReload: SaveEntity[]): void { const reg = this.ecsLibrary.registry; entityToReload.forEach(({ id, components }) => { Object.entries(components).forEach(([componentName, params]) => { @@ -20,12 +19,24 @@ export class CoreEditor { if (!ogComponent) { throw new NfNotFound("Component: " + componentName + " not found in saved components"); } - const ecsComponent = reg.getComponents({ name: componentName }).get(Number(id)); + const ecsEntity: Entity = this.getEntityFromEntityId(id); + const ecsComponent = reg.getEntityComponent(ecsEntity, { + name: componentName, + }); Object.entries(params).forEach(([paramName, paramValue]) => { ecsComponent[paramName] = paramValue; }); - reg.getComponents({ name: componentName }).set(Number(id), ecsComponent); + reg.addComponent(ecsEntity, ecsComponent); }); }); } + + private getEntityFromEntityId(entityId: string): Entity { + const reg = this.ecsLibrary.registry; + return reg.entityFromIndex( + reg + .getComponentsConst({ name: "__RESERVED_ENTITY_ID" }) + .getIndex({ name: "__RESERVED_ENTITY_ID", entityId: entityId }), + ); + } } diff --git a/packages/core-editor/test/editor-feature.spec.ts b/packages/core-editor/test/editor-feature.spec.ts index 0a1e2844..b5674dcf 100644 --- a/packages/core-editor/test/editor-feature.spec.ts +++ b/packages/core-editor/test/editor-feature.spec.ts @@ -1,54 +1,121 @@ -import { Expose } from "class-transformer"; -import { IsString } from "class-validator"; -import { describe, expect, it } from "vitest"; +import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; +import { describe, expect, it, vi } from "vitest"; -import { ConfigRegistry } from "../src/config/config-registry"; +import { type SaveComponent, type SaveEntity } from "../src/common/context/save.type"; +import { CoreEditor } from "../src/editor/core-editor"; -class ValidConfig { - @Expose() - @IsString() - name!: string; -} - -class OptionalConfig { - @Expose() - @IsString() - name!: string; - - @Expose() - host?: string; -} - -describe("ConfigRegistry", () => { - describe("registerConfig", () => { - it("should return a transformed config instance when env is valid", async () => { - const registry = new ConfigRegistry({ name: "hello" }); - const config = await registry.registerConfig(ValidConfig); - expect(config).toBeInstanceOf(ValidConfig); - expect(config.name).toBe("hello"); - }); - - it("should exclude values not decorated with @Expose", async () => { - const registry = new ConfigRegistry({ name: "hello", extra: "ignored" }); - const config = await registry.registerConfig(ValidConfig); - expect((config as any)["extra"]).toBeUndefined(); - }); +const getIndex = vi.fn((component) => { + return Number(component.entityId.slice(-1)); +}); - it("should throw when a required field is missing", async () => { - const registry = new ConfigRegistry({}); - await expect(registry.registerConfig(ValidConfig)).rejects.toThrow(); +const FakeRegistry = vi.fn( + class { + addComponent = vi.fn(); + getComponentsConst = vi.fn(() => ({ getIndex })); + getEntityComponent = vi.fn((entity: number, component) => { + return ( + { + 2: { + Position: { + name: "Position", + x: 3, + y: 4, + }, + Bullets: { + name: "Bullets", + number: 4, + bulletTypes: ["9mm"], + }, + __RESERVED_ENTITY_ID: { + entityId: "ent2", + }, + }, + 3: { + Position: { + name: "Position", + x: 7, + y: 8, + }, + __RESERVED_ENTITY_ID: { + entityId: "ent3", + }, + }, + } as Record> + )[entity]?.[component.name]; }); - - it("should throw when a field has the wrong type", async () => { - const registry = new ConfigRegistry({ name: 42 }); - await expect(registry.registerConfig(ValidConfig)).rejects.toThrow(); + entityFromIndex = vi.fn((index) => { + return index; }); + }, +); - it("should map multiple env fields correctly", async () => { - const registry = new ConfigRegistry({ name: "world", host: "localhost" }); - const config = await registry.registerConfig(OptionalConfig); - expect(config.name).toBe("world"); - expect(config.host).toBe("localhost"); +describe("EditorFeatures", () => { + describe("askEntitiesHotReload", () => { + it("should reload entities with new save variables", async () => { + const components: SaveComponent[] = [ + { + name: "Position", + path: "/tmp/pos", + paramsNames: ["x", "y"], + }, + { + name: "Bullets", + path: "/tmp/pos", + paramsNames: ["bulletTypes", "number"], + }, + ]; + const entities: SaveEntity[] = [ + { + id: "ent2", + components: { + Position: { + x: 1, + y: 2, + }, + Bullets: { + bulletTypes: ["fire", "water", "rocket"], + number: 1000, + }, + }, + }, + { + id: "ent3", + components: { + Position: { + x: 5, + y: 6, + }, + }, + }, + ]; + const fakeReg = new FakeRegistry(); + new CoreEditor({ registry: fakeReg } as any as ECSClientLibrary).askEntitiesHotReload( + components, + entities, + ); + expect(fakeReg.getComponentsConst).toHaveBeenCalledWith({ name: "__RESERVED_ENTITY_ID" }); + expect(getIndex).toHaveBeenNthCalledWith(1, { + entityId: "ent2", + name: "__RESERVED_ENTITY_ID", + }); + expect(getIndex).toHaveBeenNthCalledWith(2, { + entityId: "ent2", + name: "__RESERVED_ENTITY_ID", + }); + expect(getIndex).toHaveBeenNthCalledWith(3, { + entityId: "ent3", + name: "__RESERVED_ENTITY_ID", + }); + expect(fakeReg.getEntityComponent).toHaveBeenNthCalledWith(1, 2, { name: "Position" }); + expect(fakeReg.getEntityComponent).toHaveBeenNthCalledWith(2, 2, { name: "Bullets" }); + expect(fakeReg.getEntityComponent).toHaveBeenNthCalledWith(3, 3, { name: "Position" }); + expect(fakeReg.addComponent).toHaveBeenNthCalledWith(1, 2, { name: "Position", x: 1, y: 2 }); + expect(fakeReg.addComponent).toHaveBeenNthCalledWith(2, 2, { + name: "Bullets", + bulletTypes: ["fire", "water", "rocket"], + number: 1000, + }); + expect(fakeReg.addComponent).toHaveBeenNthCalledWith(3, 3, { name: "Position", x: 5, y: 6 }); }); }); }); diff --git a/packages/ecs-client/src/index.ts b/packages/ecs-client/src/index.ts index 5e93dd98..8759c383 100644 --- a/packages/ecs-client/src/index.ts +++ b/packages/ecs-client/src/index.ts @@ -6,6 +6,8 @@ export type { EditorSystemManifest, System, Registry, + Entity, + SparseArray, } from "@nanoforge-dev/ecs-lib"; export { ECSClientLibrary } from "./ecs-client-library"; diff --git a/packages/ecs-lib/src/index.ts b/packages/ecs-lib/src/index.ts index ab84137a..a0d20643 100644 --- a/packages/ecs-lib/src/index.ts +++ b/packages/ecs-lib/src/index.ts @@ -1,3 +1,3 @@ export { AbstractECSLibrary } from "./ecs-library.abstract"; -export type { Component, System, Registry } from "../lib/libecs"; +export type { Component, System, Registry, SparseArray, Entity } from "../lib/libecs"; export type * from "./editor-manifest.type"; diff --git a/packages/ecs-server/src/index.ts b/packages/ecs-server/src/index.ts index 0c14ef42..045ba79b 100644 --- a/packages/ecs-server/src/index.ts +++ b/packages/ecs-server/src/index.ts @@ -6,6 +6,8 @@ export type { EditorSystemManifest, System, Registry, + Entity, + SparseArray, } from "@nanoforge-dev/ecs-lib"; export { ECSServerLibrary } from "./ecs-server-library"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2913b6c7..0abe178c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -431,9 +431,6 @@ importers: '@nanoforge-dev/ecs-client': specifier: workspace:* version: link:../ecs-client - '@nanoforge-dev/ecs-server': - specifier: workspace:* - version: link:../ecs-server '@nanoforge-dev/input': specifier: workspace:* version: link:../input From 7cc1bc5349a3fd6d2a4d94e79e96652909e6a579 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Thu, 26 Mar 2026 10:23:49 +0900 Subject: [PATCH 07/14] feat: merge main --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61290906..35a1e681 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -455,7 +455,7 @@ importers: version: 6.0.2(prettier@3.8.1) eslint: specifier: catalog:lint - version: 10.0.2(jiti@2.6.1) + version: 10.0.3(jiti@2.6.1) prettier: specifier: catalog:lint version: 3.8.1 From c1dbfd6a18f70e89133540068cb9bda7fe0162ac Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Thu, 26 Mar 2026 11:26:35 +0900 Subject: [PATCH 08/14] feat(core-editor): event executor --- .../src/common/context/event-emitter.type.ts | 8 ++ .../src/common/context/options.type.ts | 3 + packages/core-editor/src/core/core.ts | 6 +- .../core-editor/src/editor/core-editor.ts | 28 +++- .../core-editor/test/editor-feature.spec.ts | 123 +++++++++++------- 5 files changed, 112 insertions(+), 56 deletions(-) create mode 100644 packages/core-editor/src/common/context/event-emitter.type.ts diff --git a/packages/core-editor/src/common/context/event-emitter.type.ts b/packages/core-editor/src/common/context/event-emitter.type.ts new file mode 100644 index 00000000..c9c040d0 --- /dev/null +++ b/packages/core-editor/src/common/context/event-emitter.type.ts @@ -0,0 +1,8 @@ +export enum EventTypeEnum { + HOT_RELOAD = "hot-reload", + HARD_RELOAD = "hard-reload", +} + +export interface EventEmitter { + eventQueue: (EventTypeEnum | string)[]; +} diff --git a/packages/core-editor/src/common/context/options.type.ts b/packages/core-editor/src/common/context/options.type.ts index 4aa66560..3a06c151 100644 --- a/packages/core-editor/src/common/context/options.type.ts +++ b/packages/core-editor/src/common/context/options.type.ts @@ -1,3 +1,4 @@ +import { type EventEmitter } from "./event-emitter.type"; import { type Save } from "./save.type"; export type IEditorRunOptions = IEditorRunClientOptions | IEditorRunServerOptions; @@ -8,6 +9,7 @@ export interface IEditorRunClientOptions { env: Record; editor: { save: Save; + events: EventEmitter; }; } export interface IEditorRunServerOptions { @@ -16,5 +18,6 @@ export interface IEditorRunServerOptions { env: Record; editor: { save: Save; + events: EventEmitter; }; } diff --git a/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts index 657d810d..9857164b 100644 --- a/packages/core-editor/src/core/core.ts +++ b/packages/core-editor/src/core/core.ts @@ -33,9 +33,12 @@ export class Core { public async init(options: IEditorRunOptions, appOptions: IApplicationOptions): Promise { this.options = appOptions; - this.editor = new CoreEditor(this.config.getComponentSystemLibrary().library); this._configRegistry = new ConfigRegistry(options.env); await this.runInit(this.getInitContext(options)); + this.editor = new CoreEditor( + options.editor, + this.config.getComponentSystemLibrary().library, + ); } public async run(): Promise { @@ -47,6 +50,7 @@ export class Core { const runner = async (delta: number) => { this.context.setDelta(delta); + this.editor?.runEvents(); await this.runExecute(clientContext, libraries); }; diff --git a/packages/core-editor/src/editor/core-editor.ts b/packages/core-editor/src/editor/core-editor.ts index 5b0bc74c..2f454410 100644 --- a/packages/core-editor/src/editor/core-editor.ts +++ b/packages/core-editor/src/editor/core-editor.ts @@ -1,19 +1,37 @@ import { NfNotFound } from "@nanoforge-dev/common"; import { type ECSClientLibrary, type Entity } from "@nanoforge-dev/ecs-client"; -import type { SaveComponent, SaveEntity } from "../common/context/save.type"; +import { EventTypeEnum } from "../common/context/event-emitter.type"; +import { type IEditorRunOptions } from "../common/context/options.type"; export class CoreEditor { + private editor: IEditorRunOptions["editor"]; private ecsLibrary: ECSClientLibrary; - constructor(ecsLibrary: ECSClientLibrary) { + constructor(editor: IEditorRunOptions["editor"], ecsLibrary: ECSClientLibrary) { + this.editor = editor; this.ecsLibrary = ecsLibrary; } - public askEntitiesHotReload(saveComponents: SaveComponent[], entityToReload: SaveEntity[]): void { + public runEvents() { + const events: (EventTypeEnum | string)[] = this.editor.events.eventQueue; + while (events.length > 0) { + const event = events.shift(); + switch (event) { + case EventTypeEnum.HOT_RELOAD: + this.askEntitiesHotReload(); + break; + default: + console.warn(`Unknown event type ${event}`); + } + } + } + + public askEntitiesHotReload(): void { const reg = this.ecsLibrary.registry; - entityToReload.forEach(({ id, components }) => { + const save = this.editor.save; + save.entities.forEach(({ id, components }) => { Object.entries(components).forEach(([componentName, params]) => { - const ogComponent = saveComponents.find(({ name: paramName }) => { + const ogComponent = save.components.find(({ name: paramName }) => { return paramName == componentName; }); if (!ogComponent) { diff --git a/packages/core-editor/test/editor-feature.spec.ts b/packages/core-editor/test/editor-feature.spec.ts index b5674dcf..2c67193c 100644 --- a/packages/core-editor/test/editor-feature.spec.ts +++ b/packages/core-editor/test/editor-feature.spec.ts @@ -1,57 +1,75 @@ import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; -import { type SaveComponent, type SaveEntity } from "../src/common/context/save.type"; +import { type EventEmitter, EventTypeEnum } from "../src/common/context/event-emitter.type"; +import type { IEditorRunOptions } from "../src/common/context/options.type"; +import { type Save, type SaveComponent, type SaveEntity } from "../src/common/context/save.type"; import { CoreEditor } from "../src/editor/core-editor"; -const getIndex = vi.fn((component) => { - return Number(component.entityId.slice(-1)); -}); - -const FakeRegistry = vi.fn( - class { - addComponent = vi.fn(); - getComponentsConst = vi.fn(() => ({ getIndex })); - getEntityComponent = vi.fn((entity: number, component) => { - return ( - { - 2: { - Position: { - name: "Position", - x: 3, - y: 4, - }, - Bullets: { - name: "Bullets", - number: 4, - bulletTypes: ["9mm"], - }, - __RESERVED_ENTITY_ID: { - entityId: "ent2", - }, - }, - 3: { - Position: { - name: "Position", - x: 7, - y: 8, - }, - __RESERVED_ENTITY_ID: { - entityId: "ent3", - }, - }, - } as Record> - )[entity]?.[component.name]; - }); - entityFromIndex = vi.fn((index) => { - return index; +describe("EditorFeatures", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + describe("eventEmitter", () => { + it("should execute eventQueue once", async () => { + const events: EventEmitter = { + eventQueue: [EventTypeEnum.HOT_RELOAD, EventTypeEnum.HOT_RELOAD], + }; + const spyHotReload = vi + .spyOn(CoreEditor.prototype, "askEntitiesHotReload") + .mockImplementation(() => {}); + new CoreEditor({ events } as IEditorRunOptions["editor"], {} as ECSClientLibrary).runEvents(); + expect(spyHotReload).toHaveBeenCalledTimes(2); }); - }, -); + }); -describe("EditorFeatures", () => { describe("askEntitiesHotReload", () => { it("should reload entities with new save variables", async () => { + const getIndex = vi.fn((component) => { + return Number(component.entityId.slice(-1)); + }); + + const FakeRegistry = vi.fn( + class { + addComponent = vi.fn(); + getComponentsConst = vi.fn(() => ({ getIndex })); + getEntityComponent = vi.fn((entity: number, component) => { + return ( + { + 2: { + Position: { + name: "Position", + x: 3, + y: 4, + }, + Bullets: { + name: "Bullets", + number: 4, + bulletTypes: ["9mm"], + }, + __RESERVED_ENTITY_ID: { + entityId: "ent2", + }, + }, + 3: { + Position: { + name: "Position", + x: 7, + y: 8, + }, + __RESERVED_ENTITY_ID: { + entityId: "ent3", + }, + }, + } as Record> + )[entity]?.[component.name]; + }); + entityFromIndex = vi.fn((index) => { + return index; + }); + }, + ); + const components: SaveComponent[] = [ { name: "Position", @@ -89,10 +107,15 @@ describe("EditorFeatures", () => { }, ]; const fakeReg = new FakeRegistry(); - new CoreEditor({ registry: fakeReg } as any as ECSClientLibrary).askEntitiesHotReload( - components, - entities, - ); + new CoreEditor( + { + save: { + components, + entities, + } as any as Save, + } as any as IEditorRunOptions["editor"], + { registry: fakeReg } as any as ECSClientLibrary, + ).askEntitiesHotReload(); expect(fakeReg.getComponentsConst).toHaveBeenCalledWith({ name: "__RESERVED_ENTITY_ID" }); expect(getIndex).toHaveBeenNthCalledWith(1, { entityId: "ent2", From 565446fa4b92d15049cdb2dccf70b57fc49b7a4e Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 31 Mar 2026 00:55:20 +0900 Subject: [PATCH 09/14] feat(core-editor): cleaner event manager --- packages/core-editor/.cliff-jumperrc.json | 2 +- packages/core-editor/.idea/.name | 2 +- ...iml => [NanoForge] Engine Core Editor.iml} | 0 packages/core-editor/.idea/modules.xml | 2 +- packages/core-editor/CHANGELOG.md | 45 ----------------- packages/core-editor/README.md | 19 ++++--- packages/core-editor/cliff.toml | 2 +- packages/core-editor/package.json | 2 +- .../src/common/context/event-emitter.type.ts | 20 +++++++- .../src/common/context/options.type.ts | 9 ++-- .../core-editor/src/editor/core-editor.ts | 13 +---- .../src/editor/event-emitter.manager.ts | 50 +++++++++++++++++++ .../core-editor/test/editor-feature.spec.ts | 15 ++++-- 13 files changed, 99 insertions(+), 82 deletions(-) rename packages/core-editor/.idea/{[NanoForge] Engine Core.iml => [NanoForge] Engine Core Editor.iml} (100%) create mode 100644 packages/core-editor/src/editor/event-emitter.manager.ts diff --git a/packages/core-editor/.cliff-jumperrc.json b/packages/core-editor/.cliff-jumperrc.json index fe6d9d4a..05f3e85e 100644 --- a/packages/core-editor/.cliff-jumperrc.json +++ b/packages/core-editor/.cliff-jumperrc.json @@ -2,6 +2,6 @@ "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json", "name": "core", "org": "nanoforge-dev", - "packagePath": "packages/core", + "packagePath": "packages/core-editor", "identifierBase": false } diff --git a/packages/core-editor/.idea/.name b/packages/core-editor/.idea/.name index 3d3e7caa..b5da13ff 100644 --- a/packages/core-editor/.idea/.name +++ b/packages/core-editor/.idea/.name @@ -1 +1 @@ -[NanoForge] Engine Core \ No newline at end of file +[NanoForge] Engine Core Editor \ No newline at end of file diff --git a/packages/core-editor/.idea/[NanoForge] Engine Core.iml b/packages/core-editor/.idea/[NanoForge] Engine Core Editor.iml similarity index 100% rename from packages/core-editor/.idea/[NanoForge] Engine Core.iml rename to packages/core-editor/.idea/[NanoForge] Engine Core Editor.iml diff --git a/packages/core-editor/.idea/modules.xml b/packages/core-editor/.idea/modules.xml index 99922e22..529602fb 100644 --- a/packages/core-editor/.idea/modules.xml +++ b/packages/core-editor/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/packages/core-editor/CHANGELOG.md b/packages/core-editor/CHANGELOG.md index 1d7fb605..53aaa9de 100644 --- a/packages/core-editor/CHANGELOG.md +++ b/packages/core-editor/CHANGELOG.md @@ -2,58 +2,13 @@ All notable changes to this project will be documented in this file. -# [@nanoforge-dev/core@1.0.1](https://github.com/NanoForge-dev/Engine/compare/@nanoforge-dev/core@1.0.0...@nanoforge-dev/core@1.0.1) - (2026-02-16) - ## Documentation -- Setup typedoc (#192) ([fa908e7](https://github.com/NanoForge-dev/Engine/commit/fa908e7e268fa1770be58fc62a0257f3760480b2)) by @MartinFillon -- Fix readme badges (#186) ([fd8d93d](https://github.com/NanoForge-dev/Engine/commit/fd8d93d13a0fbad95ef9952acd10faad9e112c78)) by @Exeloo - -# [@nanoforge-dev/core@1.0.0](https://github.com/NanoForge-dev/Engine/tree/@nanoforge-dev/core@1.0.0) - (2026-01-09) - ## Bug Fixes -- **graphics:** Game loop ([53329d2](https://github.com/NanoForge-dev/Engine/commit/53329d28c47bfac9fe86259e9fc6f42b206062a8)) by @Exeloo -- **graphics:** Fix display ([d8522e5](https://github.com/NanoForge-dev/Engine/commit/d8522e56678f3bd136733f7941c1d917c18b1400)) by @Exeloo -- **ecs:** Fix tests ([d33ada5](https://github.com/NanoForge-dev/Engine/commit/d33ada5d9c37e331b8178aa1fc0daee88b07131c)) by @Exeloo -- **ecs:** Change type handling on lib ecs ([580192d](https://github.com/NanoForge-dev/Engine/commit/580192d5038f386c965434f78aacdf3d1e399ff8)) by @Exeloo - ## Documentation -- Update README files with new structure and detailed usage examples for all packages (#157) ([63fab73](https://github.com/NanoForge-dev/Engine/commit/63fab7326bd9c7e6b00f950694ab16c9d9190c53)) by @Exeloo -- Add funding (#147) ([7301fad](https://github.com/NanoForge-dev/Engine/commit/7301fad10f59b7e1f7fa788f8a2f6fc81d0db72e)) by @Exeloo -- Add a basic introduction readme ([b240964](https://github.com/NanoForge-dev/Engine/commit/b240964a265b31769a8c5422e23e20156ba56192)) by @MartinFillon -- Add building and dependency docs to every readme ([2d4785b](https://github.com/NanoForge-dev/Engine/commit/2d4785bdcb455e83337b37540f9ab6b3394c0850)) by @MartinFillon - ## Features -- **packages/network:** Client and server for tcp/udp and networked pong as example (#156) ([839fb95](https://github.com/NanoForge-dev/Engine/commit/839fb95449f6ae0ee66d7f7e279374268b743f65)) by @Tchips46 -- **core:** Add client/server distinction and update rendering logic (#119) ([5271432](https://github.com/NanoForge-dev/Engine/commit/5271432710031396d7e433bfdfb015e3871f69d0)) by @Exeloo -- Add schematics used types (#102) ([b992306](https://github.com/NanoForge-dev/Engine/commit/b9923064ba1da3164b1739fcdec5a819734c4ba2)) by @Exeloo -- **core:** Introduce `EditableApplicationContext` for managing sound libraries ([6c7bac2](https://github.com/NanoForge-dev/Engine/commit/6c7bac261eeb7ad79203d5695d5ad76dc9e9e9f5)) by @Exeloo -- **core:** Add Context that admit a ClientLibraryManager ([3835bc8](https://github.com/NanoForge-dev/Engine/commit/3835bc8a6e6d039f11a513b7fe54c353f90e9fe1)) by @Exeloo -- **music:** Finish music library and add an interface for mutable libraries ([8e00c5d](https://github.com/NanoForge-dev/Engine/commit/8e00c5d00f2901ada86f59667eff7e5d3446076b)) by @MartinFillon -- **core:** Add `class-transformer` and `class-validator` dependencies for validation utilities ([fd94fe7](https://github.com/NanoForge-dev/Engine/commit/fd94fe7755999c5529335666720899792a691a36)) by @Exeloo -- **common, core, config:** Introduce configuration registry and validation system ([4fafb82](https://github.com/NanoForge-dev/Engine/commit/4fafb82576fec6866fc281ad5b10321d2ac430df)) by @Exeloo -- **core:** Enhance type safety and execution context handling ([d986030](https://github.com/NanoForge-dev/Engine/commit/d986030a333bc08d2e37291d1a023cf8d7a6e1d6)) by @Exeloo -- **app:** Add the ability to mute and unmute sounds ([947bdc0](https://github.com/NanoForge-dev/Engine/commit/947bdc00784a4c3313fe08feb4f91fc91b3ac7b7)) by @MartinFillon -- **sound:** Add basic sound playing to example ([7335814](https://github.com/NanoForge-dev/Engine/commit/7335814fc532ee92a5f9d776f409c5faa4d56423)) by @MartinFillon -- **core:** Add default libraries to constructor ([7d9da69](https://github.com/NanoForge-dev/Engine/commit/7d9da69be4301875020176656276236b88b737f1)) by @Exeloo -- Add dependencies handling ([e51dd3b](https://github.com/NanoForge-dev/Engine/commit/e51dd3bdb5e2e3de21339bf6218e85f935efb9d5)) by @Exeloo -- **common:** Add dependencies handler ([edb098a](https://github.com/NanoForge-dev/Engine/commit/edb098a65fb932ba9a9532a9b1eee7d64a7a8f0d)) by @Exeloo -- **core:** Add tickrate and fix runner ([1dba5bd](https://github.com/NanoForge-dev/Engine/commit/1dba5bd89ffa20dfd29b079f93c3eb923ffbdbbc)) by @Exeloo -- **input:** Add input library ([387e97d](https://github.com/NanoForge-dev/Engine/commit/387e97d7c3015a869947af4acecf48e8e1b0e2b8)) by @Exeloo -- **game:** Create pong example game ([4b66674](https://github.com/NanoForge-dev/Engine/commit/4b66674c750f345e860d225384054423433beb07)) by @bill-h4rper -- **game:** Add width and height ([c93c985](https://github.com/NanoForge-dev/Engine/commit/c93c985665bd99c09bc410f1499d11aeaffe3c4c)) by @Exeloo -- **game:** Add graphics factory ([0f4453c](https://github.com/NanoForge-dev/Engine/commit/0f4453ced908b39e953a672324e97eba82bfeaa3)) by @Exeloo -- **asset-manager:** Add asset manager ([1774a26](https://github.com/NanoForge-dev/Engine/commit/1774a26593099b4faa0a2527d1684de35211d5d2)) by @Exeloo -- Add asset manager default in core ([26cc5a9](https://github.com/NanoForge-dev/Engine/commit/26cc5a99e014fbc8669a43cc4aa4d78ecc1dee14)) by @Exeloo -- Add core and common ([1755c79](https://github.com/NanoForge-dev/Engine/commit/1755c799c143513d72b28edaac875267d484a44f)) by @Exeloo -- Initial commit ([c9bb59e](https://github.com/NanoForge-dev/Engine/commit/c9bb59ee963e7b444e8668db55597915e9ef0e4b)) by @Exeloo - ## Refactor -- **core:** Remove default libs in factory (#118) ([fa893c7](https://github.com/NanoForge-dev/Engine/commit/fa893c71616f151343c2f52a4723a64cca65814a)) by @Exeloo -- Migrate namespaces to `@nanoforge-dev` and update related imports ([c84c927](https://github.com/NanoForge-dev/Engine/commit/c84c927ead941d914e5a9fd752fd3a5ac969f981)) by @Exeloo -- **libraries:** Implement initialization validation and standardize nullable fields ([8b04575](https://github.com/NanoForge-dev/Engine/commit/8b04575cf7f649a440b8f40ad6114414406b0c1a)) by @Exeloo - diff --git a/packages/core-editor/README.md b/packages/core-editor/README.md index f114fe2e..af648541 100644 --- a/packages/core-editor/README.md +++ b/packages/core-editor/README.md @@ -16,17 +16,17 @@ ## About -`@nanoforge-dev/core` is a core package that contains game main loop. It is used to initialize the game and run it. +`@nanoforge-dev/core-editor` is a core package that contains game main loop. It is used to initialize the game and run it. ## Installation **Node.js 24.11.0 or newer is required.** ```sh -npm install @nanoforge-dev/core -yarn add @nanoforge-dev/core -pnpm add @nanoforge-dev/core -bun add @nanoforge-dev/core +npm install @nanoforge-dev/core-editor +yarn add @nanoforge-dev/core-editor +pnpm add @nanoforge-dev/core-editor +bun add @nanoforge-dev/core-editor ``` ## Example usage @@ -34,10 +34,9 @@ bun add @nanoforge-dev/core Initialize the game in your main file. ```ts -import { type IRunOptions } from "@nanoforge-dev/common"; -import { NanoforgeFactory } from "@nanoforge-dev/core"; +import { IEditorRunOptions, NanoforgeFactory } from "@nanoforge-dev/core-editor"; -export async function main(options: IRunClientOptions) { +export async function main(options: IEditorRunOptions) { const app = NanoforgeFactory.createClient(); await app.init(options); @@ -63,6 +62,6 @@ If you don't understand something in the documentation, you are experiencing pro [documentation]: https://github.com/NanoForge-dev/Engine [discussions]: https://github.com/NanoForge-dev/Engine/discussions -[source]: https://github.com/NanoForge-dev/Engine/tree/main/packages/core -[npm]: https://www.npmjs.com/package/@nanoforge-dev/core +[source]: https://github.com/NanoForge-dev/Engine/tree/main/packages/core-editor +[npm]: https://www.npmjs.com/package/@nanoforge-dev/core-editor [contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md diff --git a/packages/core-editor/cliff.toml b/packages/core-editor/cliff.toml index 59978d4c..a9abf0b7 100644 --- a/packages/core-editor/cliff.toml +++ b/packages/core-editor/cliff.toml @@ -69,7 +69,7 @@ commit_parsers = [ ] filter_commits = true protect_breaking_commits = true -tag_pattern = "@nanoforge-dev/core@[0-9]*" +tag_pattern = "@nanoforge-dev/core-editor@[0-9]*" ignore_tags = "" topo_order = false sort_commits = "newest" diff --git a/packages/core-editor/package.json b/packages/core-editor/package.json index 8f9080bf..c2fe4a6a 100644 --- a/packages/core-editor/package.json +++ b/packages/core-editor/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@nanoforge-dev/core-editor", - "version": "1.0", + "version": "0.0", "description": "NanoForge Engine - Core Editor", "keywords": [ "nanoforge", diff --git a/packages/core-editor/src/common/context/event-emitter.type.ts b/packages/core-editor/src/common/context/event-emitter.type.ts index c9c040d0..c90caba3 100644 --- a/packages/core-editor/src/common/context/event-emitter.type.ts +++ b/packages/core-editor/src/common/context/event-emitter.type.ts @@ -3,6 +3,22 @@ export enum EventTypeEnum { HARD_RELOAD = "hard-reload", } -export interface EventEmitter { - eventQueue: (EventTypeEnum | string)[]; +export type ListenerType = (...args: any[]) => void; + +export interface IEventEmitter { + listeners: Record; + eventQueue: { event: EventTypeEnum | string; args: any[] }[]; + + runEvents: () => void; + + emitEvent: (event: EventTypeEnum, ...args: any) => void; + + addListener: (event: EventTypeEnum | string, listener: ListenerType) => void; + on: (event: EventTypeEnum | string, listener: ListenerType) => void; + + removeListener: (event: EventTypeEnum | string, listener: ListenerType) => void; + off: (event: EventTypeEnum | string, listener: ListenerType) => void; + + removeListenersForEvent: (event: EventTypeEnum | string) => void; + removeAllListeners: () => void; } diff --git a/packages/core-editor/src/common/context/options.type.ts b/packages/core-editor/src/common/context/options.type.ts index 3a06c151..1fc49839 100644 --- a/packages/core-editor/src/common/context/options.type.ts +++ b/packages/core-editor/src/common/context/options.type.ts @@ -1,4 +1,4 @@ -import { type EventEmitter } from "./event-emitter.type"; +import { type EventEmitter } from "../../editor/event-emitter.manager"; import { type Save } from "./save.type"; export type IEditorRunOptions = IEditorRunClientOptions | IEditorRunServerOptions; @@ -9,15 +9,16 @@ export interface IEditorRunClientOptions { env: Record; editor: { save: Save; - events: EventEmitter; + coreEvents: EventEmitter; + editorEvents: EventEmitter; }; } export interface IEditorRunServerOptions { - canvas: HTMLCanvasElement; files: Map; env: Record; editor: { save: Save; - events: EventEmitter; + coreEvents: EventEmitter; + editorEvents: EventEmitter; }; } diff --git a/packages/core-editor/src/editor/core-editor.ts b/packages/core-editor/src/editor/core-editor.ts index 2f454410..553638d5 100644 --- a/packages/core-editor/src/editor/core-editor.ts +++ b/packages/core-editor/src/editor/core-editor.ts @@ -10,20 +10,11 @@ export class CoreEditor { constructor(editor: IEditorRunOptions["editor"], ecsLibrary: ECSClientLibrary) { this.editor = editor; this.ecsLibrary = ecsLibrary; + this.editor.coreEvents?.addListener(EventTypeEnum.HOT_RELOAD, this.askEntitiesHotReload); } public runEvents() { - const events: (EventTypeEnum | string)[] = this.editor.events.eventQueue; - while (events.length > 0) { - const event = events.shift(); - switch (event) { - case EventTypeEnum.HOT_RELOAD: - this.askEntitiesHotReload(); - break; - default: - console.warn(`Unknown event type ${event}`); - } - } + this.editor.coreEvents?.runEvents(); } public askEntitiesHotReload(): void { diff --git a/packages/core-editor/src/editor/event-emitter.manager.ts b/packages/core-editor/src/editor/event-emitter.manager.ts new file mode 100644 index 00000000..3498d826 --- /dev/null +++ b/packages/core-editor/src/editor/event-emitter.manager.ts @@ -0,0 +1,50 @@ +import { + type EventTypeEnum, + type IEventEmitter, + type ListenerType, +} from "../common/context/event-emitter.type"; + +export class EventEmitter implements IEventEmitter { + public listeners: Record = {}; + public eventQueue: { event: EventTypeEnum | string; args: any[] }[] = []; + + public runEvents = () => { + this.eventQueue.forEach(({ event, args }) => { + this.listeners[event]?.forEach((listener) => { + listener(...args); + }); + }); + this.eventQueue = []; + }; + + public emitEvent(event: EventTypeEnum | string, ...args: any[]) { + this.eventQueue.push({ event, args }); + } + + public addListener(event: EventTypeEnum | string, listener: ListenerType): void { + if (!this.listeners[event]) this.listeners[event] = []; + this.listeners[event].push(listener); + } + public on(event: EventTypeEnum | string, listener: ListenerType): void { + this.addListener(event, listener); + } + + public removeListener(event: EventTypeEnum | string, listener: ListenerType): void { + if (!this.listeners[event]) return; + const index = this.listeners[event].indexOf(listener); + if (index >= 0) { + this.listeners[event].splice(index, 1); + } + } + public off(event: EventTypeEnum | string, listener: ListenerType): void { + this.removeListener(event, listener); + } + + public removeListenersForEvent(event: EventTypeEnum | string): void { + if (!this.listeners[event]) return; + this.listeners[event] = []; + } + public removeAllListeners(): void { + this.listeners = {}; + } +} diff --git a/packages/core-editor/test/editor-feature.spec.ts b/packages/core-editor/test/editor-feature.spec.ts index 2c67193c..bd442f78 100644 --- a/packages/core-editor/test/editor-feature.spec.ts +++ b/packages/core-editor/test/editor-feature.spec.ts @@ -1,24 +1,29 @@ import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { type EventEmitter, EventTypeEnum } from "../src/common/context/event-emitter.type"; +import { EventTypeEnum } from "../src/common/context/event-emitter.type"; import type { IEditorRunOptions } from "../src/common/context/options.type"; import { type Save, type SaveComponent, type SaveEntity } from "../src/common/context/save.type"; import { CoreEditor } from "../src/editor/core-editor"; +import { EventEmitter } from "../src/editor/event-emitter.manager"; describe("EditorFeatures", () => { afterEach(() => { vi.restoreAllMocks(); }); + describe("eventEmitter", () => { it("should execute eventQueue once", async () => { - const events: EventEmitter = { - eventQueue: [EventTypeEnum.HOT_RELOAD, EventTypeEnum.HOT_RELOAD], - }; + const events = new EventEmitter(); + events.emitEvent(EventTypeEnum.HOT_RELOAD); + events.emitEvent(EventTypeEnum.HOT_RELOAD); const spyHotReload = vi .spyOn(CoreEditor.prototype, "askEntitiesHotReload") .mockImplementation(() => {}); - new CoreEditor({ events } as IEditorRunOptions["editor"], {} as ECSClientLibrary).runEvents(); + new CoreEditor( + { coreEvents: events } as IEditorRunOptions["editor"], + {} as ECSClientLibrary, + ).runEvents(); expect(spyHotReload).toHaveBeenCalledTimes(2); }); }); From c8e9be799e84b7e697a08f4cd0dcc01cf856b38d Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 31 Mar 2026 00:55:58 +0900 Subject: [PATCH 10/14] fix(core-editor): cliffer name --- packages/core-editor/.cliff-jumperrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-editor/.cliff-jumperrc.json b/packages/core-editor/.cliff-jumperrc.json index 05f3e85e..c760745b 100644 --- a/packages/core-editor/.cliff-jumperrc.json +++ b/packages/core-editor/.cliff-jumperrc.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json", - "name": "core", + "name": "core-editor", "org": "nanoforge-dev", "packagePath": "packages/core-editor", "identifierBase": false From b94f0546182ad1449f7a658ac5deb5f47d31dfbd Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 31 Mar 2026 10:47:47 +0900 Subject: [PATCH 11/14] feat(core-editor): use core fonctions --- .../src/application/application-config.ts | 89 --------- .../application/application-options.type.ts | 3 - .../src/application/nanoforge-application.ts | 6 +- .../src/application/nanoforge-factory.ts | 2 +- .../contexts/application.editable-context.ts | 20 -- .../executions/clear.editable-context.ts | 3 - .../executions/execution.editable-context.ts | 3 - .../executions/init.editable-context.ts | 3 - .../contexts/library.editable-context.ts | 7 - .../common/library/manager/library.manager.ts | 106 ---------- .../common/library/relationship-functions.ts | 122 ------------ .../core-editor/src/config/config-registry.ts | 19 -- packages/core-editor/src/core/core.ts | 12 +- .../core-editor/test/config-registry.spec.ts | 54 ------ .../test/editable-library-manager.spec.ts | 182 ------------------ .../core-editor/test/relationship.spec.ts | 135 ------------- packages/core-editor/tsconfig.json | 7 +- .../src/application/application-config.ts | 4 +- 18 files changed, 18 insertions(+), 759 deletions(-) delete mode 100644 packages/core-editor/src/application/application-config.ts delete mode 100644 packages/core-editor/src/application/application-options.type.ts delete mode 100644 packages/core-editor/src/common/context/contexts/application.editable-context.ts delete mode 100644 packages/core-editor/src/common/context/contexts/executions/clear.editable-context.ts delete mode 100644 packages/core-editor/src/common/context/contexts/executions/execution.editable-context.ts delete mode 100644 packages/core-editor/src/common/context/contexts/executions/init.editable-context.ts delete mode 100644 packages/core-editor/src/common/context/contexts/library.editable-context.ts delete mode 100644 packages/core-editor/src/common/library/manager/library.manager.ts delete mode 100644 packages/core-editor/src/common/library/relationship-functions.ts delete mode 100644 packages/core-editor/src/config/config-registry.ts delete mode 100644 packages/core-editor/test/config-registry.spec.ts delete mode 100644 packages/core-editor/test/editable-library-manager.spec.ts delete mode 100644 packages/core-editor/test/relationship.spec.ts diff --git a/packages/core-editor/src/application/application-config.ts b/packages/core-editor/src/application/application-config.ts deleted file mode 100644 index 7eeede0c..00000000 --- a/packages/core-editor/src/application/application-config.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - type IAssetManagerLibrary, - type IComponentSystemLibrary, - type IGraphicsLibrary, - type IInputLibrary, - type ILibrary, - type IMusicLibrary, - type INetworkLibrary, - type ISoundLibrary, - type LibraryHandle, -} from "@nanoforge-dev/common"; - -import { EditableLibraryManager } from "../common/library/manager/library.manager"; - -export class ApplicationConfig { - private readonly _libraryManager: EditableLibraryManager; - - constructor() { - this._libraryManager = new EditableLibraryManager(); - } - - get libraryManager(): EditableLibraryManager { - return this._libraryManager; - } - - public getLibrary(sym: symbol): LibraryHandle { - return this._libraryManager.get(sym); - } - - public useLibrary(sym: symbol, library: ILibrary): void { - this._libraryManager.set(sym, library); - } - - public getComponentSystemLibrary() { - return this._libraryManager.getComponentSystem(); - } - - public useComponentSystemLibrary(library: IComponentSystemLibrary) { - this._libraryManager.setComponentSystem(library); - } - - public getGraphicsLibrary() { - return this._libraryManager.getGraphics(); - } - - public useGraphicsLibrary(library: IGraphicsLibrary) { - this._libraryManager.setGraphics(library); - } - - public getNetworkLibrary() { - return this._libraryManager.getNetwork(); - } - - public useNetworkLibrary(library: INetworkLibrary) { - this._libraryManager.setNetwork(library); - } - - public getAssetManagerLibrary() { - return this._libraryManager.getAssetManager(); - } - - public useAssetManagerLibrary(library: IAssetManagerLibrary) { - this._libraryManager.setAssetManager(library); - } - - public getInputLibrary() { - return this._libraryManager.getInput(); - } - - public useInputLibrary(library: IInputLibrary) { - this._libraryManager.setInput(library); - } - - public getSoundLibrary() { - return this._libraryManager.getSound(); - } - - public useSoundLibrary(library: ISoundLibrary) { - this._libraryManager.setSound(library); - } - - public getMusicLibrary() { - return this._libraryManager.getMusic(); - } - - public useMusicLibrary(library: IMusicLibrary) { - this._libraryManager.setMusic(library); - } -} diff --git a/packages/core-editor/src/application/application-options.type.ts b/packages/core-editor/src/application/application-options.type.ts deleted file mode 100644 index 57ecc833..00000000 --- a/packages/core-editor/src/application/application-options.type.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IApplicationOptions { - tickRate: number; -} diff --git a/packages/core-editor/src/application/nanoforge-application.ts b/packages/core-editor/src/application/nanoforge-application.ts index 81f6ea6e..66bb18eb 100644 --- a/packages/core-editor/src/application/nanoforge-application.ts +++ b/packages/core-editor/src/application/nanoforge-application.ts @@ -6,11 +6,11 @@ import { NfNotInitializedException, } from "@nanoforge-dev/common"; -import { EditableApplicationContext } from "../common/context/contexts/application.editable-context"; +import { ApplicationConfig } from "../../../core/src/application/application-config"; +import type { IApplicationOptions } from "../../../core/src/application/application-options.type"; +import { EditableApplicationContext } from "../../../core/src/common/context/contexts/application.editable-context"; import { type IEditorRunOptions } from "../common/context/options.type"; import { Core } from "../core/core"; -import { ApplicationConfig } from "./application-config"; -import type { IApplicationOptions } from "./application-options.type"; export abstract class NanoforgeApplication { protected applicationConfig: ApplicationConfig; diff --git a/packages/core-editor/src/application/nanoforge-factory.ts b/packages/core-editor/src/application/nanoforge-factory.ts index 84711a6d..98ec685e 100644 --- a/packages/core-editor/src/application/nanoforge-factory.ts +++ b/packages/core-editor/src/application/nanoforge-factory.ts @@ -1,4 +1,4 @@ -import { type IApplicationOptions } from "./application-options.type"; +import { type IApplicationOptions } from "../../../core/src/application/application-options.type"; import { NanoforgeClient } from "./nanoforge-client"; import { NanoforgeServer } from "./nanoforge-server"; diff --git a/packages/core-editor/src/common/context/contexts/application.editable-context.ts b/packages/core-editor/src/common/context/contexts/application.editable-context.ts deleted file mode 100644 index 492b797e..00000000 --- a/packages/core-editor/src/common/context/contexts/application.editable-context.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ApplicationContext } from "@nanoforge-dev/common"; - -import { type EditableLibraryManager } from "../../library/manager/library.manager"; - -export class EditableApplicationContext extends ApplicationContext { - private _libraryManager: EditableLibraryManager; - - constructor(libraryManager: EditableLibraryManager) { - super(); - this._libraryManager = libraryManager; - } - - setDelta(delta: number) { - this._delta = delta; - } - - muteSoundLibraries(): void { - this._libraryManager.getMutableLibraries().forEach((lib) => lib.library.mute()); - } -} diff --git a/packages/core-editor/src/common/context/contexts/executions/clear.editable-context.ts b/packages/core-editor/src/common/context/contexts/executions/clear.editable-context.ts deleted file mode 100644 index 1081686d..00000000 --- a/packages/core-editor/src/common/context/contexts/executions/clear.editable-context.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ClearContext } from "@nanoforge-dev/common"; - -export class EditableClearContext extends ClearContext {} diff --git a/packages/core-editor/src/common/context/contexts/executions/execution.editable-context.ts b/packages/core-editor/src/common/context/contexts/executions/execution.editable-context.ts deleted file mode 100644 index e9b5b3de..00000000 --- a/packages/core-editor/src/common/context/contexts/executions/execution.editable-context.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ExecutionContext } from "@nanoforge-dev/common"; - -export class EditableExecutionContext extends ExecutionContext {} diff --git a/packages/core-editor/src/common/context/contexts/executions/init.editable-context.ts b/packages/core-editor/src/common/context/contexts/executions/init.editable-context.ts deleted file mode 100644 index 7ce44e10..00000000 --- a/packages/core-editor/src/common/context/contexts/executions/init.editable-context.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { InitContext } from "@nanoforge-dev/common"; - -export class EditableInitContext extends InitContext {} diff --git a/packages/core-editor/src/common/context/contexts/library.editable-context.ts b/packages/core-editor/src/common/context/contexts/library.editable-context.ts deleted file mode 100644 index 48f942e8..00000000 --- a/packages/core-editor/src/common/context/contexts/library.editable-context.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { LibraryContext, type LibraryStatusEnum } from "@nanoforge-dev/common"; - -export class EditableLibraryContext extends LibraryContext { - setStatus(status: LibraryStatusEnum) { - this._status = status; - } -} diff --git a/packages/core-editor/src/common/library/manager/library.manager.ts b/packages/core-editor/src/common/library/manager/library.manager.ts deleted file mode 100644 index 6e17b321..00000000 --- a/packages/core-editor/src/common/library/manager/library.manager.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - ASSET_MANAGER_LIBRARY, - COMPONENT_SYSTEM_LIBRARY, - DefaultLibrariesEnum, - GRAPHICS_LIBRARY, - type IAssetManagerLibrary, - type IComponentSystemLibrary, - type IGraphicsLibrary, - type IInputLibrary, - type ILibrary, - type IMusicLibrary, - type IMutableLibrary, - INPUT_LIBRARY, - type INetworkLibrary, - type IRunnerLibrary, - type ISoundLibrary, - type LibraryHandle, - LibraryManager, - MUSIC_LIBRARY, - NETWORK_LIBRARY, - SOUND_LIBRARY, -} from "@nanoforge-dev/common"; - -import { EditableLibraryContext } from "../../context/contexts/library.editable-context"; -import { Relationship } from "../relationship-functions"; - -const hasMethod = (obj: any, method: string) => { - return typeof obj[method] === "function"; -}; - -export class EditableLibraryManager extends LibraryManager { - public set(sym: symbol, library: ILibrary) { - this.setNewLibrary(sym, library, new EditableLibraryContext()); - } - - public setComponentSystem(library: IComponentSystemLibrary): void { - this._set( - DefaultLibrariesEnum.COMPONENT_SYSTEM, - COMPONENT_SYSTEM_LIBRARY, - library, - new EditableLibraryContext(), - ); - } - - public setGraphics(library: IGraphicsLibrary): void { - this._set( - DefaultLibrariesEnum.GRAPHICS, - GRAPHICS_LIBRARY, - library, - new EditableLibraryContext(), - ); - } - - public setAssetManager(library: IAssetManagerLibrary): void { - this._set( - DefaultLibrariesEnum.ASSET_MANAGER, - ASSET_MANAGER_LIBRARY, - library, - new EditableLibraryContext(), - ); - } - - public setNetwork(library: INetworkLibrary): void { - this._set(DefaultLibrariesEnum.NETWORK, NETWORK_LIBRARY, library, new EditableLibraryContext()); - } - - public setInput(library: IInputLibrary): void { - this._set(DefaultLibrariesEnum.INPUT, INPUT_LIBRARY, library, new EditableLibraryContext()); - } - - public setSound(library: ISoundLibrary): void { - this._set(DefaultLibrariesEnum.SOUND, SOUND_LIBRARY, library, new EditableLibraryContext()); - } - - public setMusic(library: IMusicLibrary): void { - this._set(DefaultLibrariesEnum.MUSIC, MUSIC_LIBRARY, library, new EditableLibraryContext()); - } - - public getLibraries(): LibraryHandle[] { - return this._libraries; - } - - public getInitLibraries(): LibraryHandle[] { - return Relationship.getLibrariesByDependencies(this._libraries); - } - - public getExecutionLibraries(): LibraryHandle[] { - return Relationship.getLibrariesByRun(this._getRunnerLibraries()); - } - - public getClearLibraries(): LibraryHandle[] { - return Relationship.getLibrariesByDependencies(this._libraries, true); - } - - public getMutableLibraries(): LibraryHandle[] { - return this._libraries.filter( - (handle) => handle && hasMethod(handle.library, "mute"), - ) as LibraryHandle[]; - } - - private _getRunnerLibraries(): LibraryHandle[] { - return this._libraries.filter( - (handle) => handle && hasMethod(handle.library, "__run"), - ) as LibraryHandle[]; - } -} diff --git a/packages/core-editor/src/common/library/relationship-functions.ts b/packages/core-editor/src/common/library/relationship-functions.ts deleted file mode 100644 index d9ff0f2b..00000000 --- a/packages/core-editor/src/common/library/relationship-functions.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { type ILibrary, type LibraryHandle } from "@nanoforge-dev/common"; - -class RelationshipStatic { - getLibrariesByDependencies(libraries: LibraryHandle[], reverse: boolean = false) { - let response: LibraryHandle[] = []; - for (const library of libraries) { - if (!library) continue; - response = this._pushLibraryWithDependencies(library, response, [], libraries); - } - - if (reverse) return response.reverse(); - return response; - } - - getLibrariesByRun(libraries: LibraryHandle[]) { - let response: LibraryHandle[] = []; - const dependencies = new Map>( - libraries.map((library) => [library.symbol, new Set()]), - ); - - for (const handle of libraries) { - const key = handle.symbol; - - for (const before of handle.library.__relationship.runBefore) { - this._pushToDependencies(key, before, dependencies); - } - for (const after of handle.library.__relationship.runAfter) { - this._pushToDependencies(after, key, dependencies); - } - } - - for (const library of libraries) { - response = this._pushLibraryWithDependenciesRun( - library, - dependencies, - response, - [], - libraries, - ); - } - return response; - } - - private _pushToDependencies( - key: symbol, - value: symbol, - dependencies: Map>, - ): void { - let curr = dependencies.get(key); - if (!curr) curr = new Set(); - curr.add(value); - dependencies.set(key, curr); - } - - private _pushLibraryWithDependenciesRun( - handle: LibraryHandle, - dependencies: Map>, - response: LibraryHandle[], - cache: symbol[], - libraries: LibraryHandle[], - ): LibraryHandle[] { - const key = handle.symbol; - if (this._symbolIsInList(key, response)) return response; - - if (cache.includes(key)) throw new Error("Circular dependencies !"); - - cache.push(key); - - const deps = dependencies.get(key); - if (!deps) throw new Error("Dependencies not found"); - - for (const dep of deps) { - if (this._symbolIsInList(dep, response)) continue; - - const depHandle = libraries.find((lib) => lib?.symbol === dep) as LibraryHandle; - if (!depHandle) throw new Error(`Cannot find library ${dep.toString()}`); - - response = this._pushLibraryWithDependenciesRun( - depHandle, - dependencies, - response, - cache, - libraries, - ); - } - cache.pop(); - - response.push(handle); - return response; - } - - private _pushLibraryWithDependencies( - handle: LibraryHandle, - response: LibraryHandle[], - cache: symbol[], - libraries: LibraryHandle[], - ): LibraryHandle[] { - if (this._symbolIsInList(handle.symbol, response)) return response; - - if (cache.includes(handle.symbol)) throw new Error("Circular dependencies !"); - - cache.push(handle.symbol); - for (const dep of handle.library.__relationship.dependencies) { - if (this._symbolIsInList(dep, response)) continue; - - const depHandle = libraries.find((lib) => lib?.symbol === dep) as LibraryHandle; - if (!depHandle) throw new Error(`Cannot find library ${dep.toString()}`); - - response = this._pushLibraryWithDependencies(depHandle, response, cache, libraries); - } - cache.pop(); - - response.push(handle); - return response; - } - - private _symbolIsInList(sym: symbol, libraries: LibraryHandle[]): boolean { - return libraries.some((lib) => lib.symbol === sym); - } -} - -export const Relationship = new RelationshipStatic(); diff --git a/packages/core-editor/src/config/config-registry.ts b/packages/core-editor/src/config/config-registry.ts deleted file mode 100644 index 946ed1dc..00000000 --- a/packages/core-editor/src/config/config-registry.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { plainToInstance } from "class-transformer"; -import { validate } from "class-validator"; - -export class ConfigRegistry { - private readonly _env: Record; - - constructor(env: Record) { - this._env = env; - } - - async registerConfig(config: new () => T): Promise { - const data = plainToInstance(config, this._env, { excludeExtraneousValues: true }); - const errors = await validate(data); - if (errors.length > 0) { - throw new Error(errors.toString()); - } - return data; - } -} diff --git a/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts index 9857164b..b917ce2e 100644 --- a/packages/core-editor/src/core/core.ts +++ b/packages/core-editor/src/core/core.ts @@ -10,13 +10,13 @@ import { } from "@nanoforge-dev/common"; import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; -import { type ApplicationConfig } from "../application/application-config"; -import type { IApplicationOptions } from "../application/application-options.type"; -import { type EditableApplicationContext } from "../common/context/contexts/application.editable-context"; -import { EditableExecutionContext } from "../common/context/contexts/executions/execution.editable-context"; -import { type EditableLibraryContext } from "../common/context/contexts/library.editable-context"; +import { type ApplicationConfig } from "../../../core/src/application/application-config"; +import type { IApplicationOptions } from "../../../core/src/application/application-options.type"; +import { type EditableApplicationContext } from "../../../core/src/common/context/contexts/application.editable-context"; +import { EditableExecutionContext } from "../../../core/src/common/context/contexts/executions/execution.editable-context"; +import { type EditableLibraryContext } from "../../../core/src/common/context/contexts/library.editable-context"; +import { ConfigRegistry } from "../../../core/src/config/config-registry"; import { type IEditorRunOptions } from "../common/context/options.type"; -import { ConfigRegistry } from "../config/config-registry"; import { CoreEditor } from "../editor/core-editor"; export class Core { diff --git a/packages/core-editor/test/config-registry.spec.ts b/packages/core-editor/test/config-registry.spec.ts deleted file mode 100644 index 0a1e2844..00000000 --- a/packages/core-editor/test/config-registry.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Expose } from "class-transformer"; -import { IsString } from "class-validator"; -import { describe, expect, it } from "vitest"; - -import { ConfigRegistry } from "../src/config/config-registry"; - -class ValidConfig { - @Expose() - @IsString() - name!: string; -} - -class OptionalConfig { - @Expose() - @IsString() - name!: string; - - @Expose() - host?: string; -} - -describe("ConfigRegistry", () => { - describe("registerConfig", () => { - it("should return a transformed config instance when env is valid", async () => { - const registry = new ConfigRegistry({ name: "hello" }); - const config = await registry.registerConfig(ValidConfig); - expect(config).toBeInstanceOf(ValidConfig); - expect(config.name).toBe("hello"); - }); - - it("should exclude values not decorated with @Expose", async () => { - const registry = new ConfigRegistry({ name: "hello", extra: "ignored" }); - const config = await registry.registerConfig(ValidConfig); - expect((config as any)["extra"]).toBeUndefined(); - }); - - it("should throw when a required field is missing", async () => { - const registry = new ConfigRegistry({}); - await expect(registry.registerConfig(ValidConfig)).rejects.toThrow(); - }); - - it("should throw when a field has the wrong type", async () => { - const registry = new ConfigRegistry({ name: 42 }); - await expect(registry.registerConfig(ValidConfig)).rejects.toThrow(); - }); - - it("should map multiple env fields correctly", async () => { - const registry = new ConfigRegistry({ name: "world", host: "localhost" }); - const config = await registry.registerConfig(OptionalConfig); - expect(config.name).toBe("world"); - expect(config.host).toBe("localhost"); - }); - }); -}); diff --git a/packages/core-editor/test/editable-library-manager.spec.ts b/packages/core-editor/test/editable-library-manager.spec.ts deleted file mode 100644 index 0e808baa..00000000 --- a/packages/core-editor/test/editable-library-manager.spec.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { COMPONENT_SYSTEM_LIBRARY, type ILibrary, LibraryStatusEnum } from "@nanoforge-dev/common"; -import { beforeEach, describe, expect, it } from "vitest"; - -import { Library } from "../../common/src/library/libraries/library"; -import { EditableLibraryManager } from "../src/common/library/manager/library.manager"; - -class StubLibrary extends Library { - private readonly _name: string; - - constructor(name: string, options?: ConstructorParameters[0]) { - super(options); - this._name = name; - } - - get __name(): string { - return this._name; - } -} - -class StubRunnerLibrary extends StubLibrary { - async __run(): Promise {} -} - -class StubMutableLibrary extends StubLibrary { - mute(): void {} -} - -describe("EditableLibraryManager", () => { - let manager: EditableLibraryManager; - - beforeEach(() => { - manager = new EditableLibraryManager(); - }); - - describe("typed setters and getters", () => { - it("should store and retrieve a component system library", () => { - const lib = new StubLibrary("ComponentSystem"); - manager.setComponentSystem(lib as any); - expect(manager.getComponentSystem().library).toBe(lib); - }); - - it("should store and retrieve a graphics library", () => { - const lib = new StubLibrary("Graphics"); - manager.setGraphics(lib as any); - expect(manager.getGraphics().library).toBe(lib); - }); - - it("should store and retrieve an asset manager library", () => { - const lib = new StubLibrary("AssetManager"); - manager.setAssetManager(lib as any); - expect(manager.getAssetManager().library).toBe(lib); - }); - - it("should store and retrieve a network library", () => { - const lib = new StubLibrary("Network"); - manager.setNetwork(lib as any); - expect(manager.getNetwork().library).toBe(lib); - }); - - it("should store and retrieve an input library", () => { - const lib = new StubLibrary("Input"); - manager.setInput(lib as any); - expect(manager.getInput().library).toBe(lib); - }); - - it("should store and retrieve a sound library", () => { - const lib = new StubLibrary("Sound"); - manager.setSound(lib as any); - expect(manager.getSound().library).toBe(lib); - }); - - it("should store and retrieve a music library", () => { - const lib = new StubLibrary("Music"); - manager.setMusic(lib as any); - expect(manager.getMusic().library).toBe(lib); - }); - - it("should throw when getting a typed library that was not set", () => { - expect(() => manager.getComponentSystem()).toThrow(); - }); - }); - - describe("set and get (custom symbol)", () => { - it("should store and retrieve a library by Symbol.for key", () => { - const sym = Symbol.for("customLib"); - const lib = new StubLibrary("Custom"); - - manager.setAssetManager(new StubLibrary("Asset") as any); - manager.set(sym, lib as unknown as ILibrary); - - expect(manager.get(sym).library).toBe(lib); - }); - }); - - describe("getLibraries", () => { - it("should return the list of all set libraries", () => { - const lib = new StubLibrary("ComponentSystem"); - manager.setComponentSystem(lib as any); - const libs = manager.getLibraries().filter(Boolean); - expect(libs.some((h) => h.library === (lib as unknown as ILibrary))).toBe(true); - }); - }); - - describe("getInitLibraries", () => { - it("should return libraries in dependency order", () => { - const libA = new StubLibrary("A", { dependencies: [COMPONENT_SYSTEM_LIBRARY] }); - const libB = new StubLibrary("B"); - - manager.setComponentSystem(libB as any); - manager.setGraphics(libA as any); - - const order = manager.getInitLibraries().map((h) => h.library.__name); - const idxB = order.indexOf("B"); - const idxA = order.indexOf("A"); - - expect(idxB).toBeLessThan(idxA); - }); - - it("should return all set libraries", () => { - manager.setAssetManager(new StubLibrary("Asset") as any); - manager.setGraphics(new StubLibrary("Graphics") as any); - - expect(manager.getInitLibraries().length).toBe(2); - }); - }); - - describe("getClearLibraries", () => { - it("should return libraries in reverse dependency order", () => { - const libA = new StubLibrary("A", { dependencies: [COMPONENT_SYSTEM_LIBRARY] }); - const libB = new StubLibrary("B"); - - manager.setComponentSystem(libB as any); - manager.setGraphics(libA as any); - - const order = manager.getClearLibraries().map((h) => h.library.__name); - const idxA = order.indexOf("A"); - const idxB = order.indexOf("B"); - - expect(idxA).toBeLessThan(idxB); - }); - }); - - describe("getExecutionLibraries", () => { - it("should only return libraries that implement __run", () => { - manager.setComponentSystem(new StubLibrary("NotARunner") as any); - manager.setGraphics(new StubRunnerLibrary("Runner") as any); - - const runners = manager.getExecutionLibraries(); - expect(runners.every((h) => typeof (h.library as any).__run === "function")).toBe(true); - expect(runners.some((h) => h.library.__name === "Runner")).toBe(true); - expect(runners.some((h) => h.library.__name === "NotARunner")).toBe(false); - }); - - it("should return empty when no runner libraries are set", () => { - manager.setComponentSystem(new StubLibrary("Static") as any); - expect(manager.getExecutionLibraries()).toHaveLength(0); - }); - }); - - describe("getMutableLibraries", () => { - it("should only return libraries that implement mute", () => { - manager.setSound(new StubMutableLibrary("MutableSound") as any); - manager.setGraphics(new StubLibrary("NonMutableGraphics") as any); - - const mutable = manager.getMutableLibraries(); - expect(mutable.some((h) => h.library.__name === "MutableSound")).toBe(true); - expect(mutable.some((h) => h.library.__name === "NonMutableGraphics")).toBe(false); - }); - - it("should return empty when no mutable libraries are set", () => { - manager.setGraphics(new StubLibrary("Graphics") as any); - expect(manager.getMutableLibraries()).toHaveLength(0); - }); - }); - - describe("library context status", () => { - it("should start with UNLOADED status", () => { - manager.setComponentSystem(new StubLibrary("Comp") as any); - expect(manager.getComponentSystem().context.status).toBe(LibraryStatusEnum.UNLOADED); - }); - }); -}); diff --git a/packages/core-editor/test/relationship.spec.ts b/packages/core-editor/test/relationship.spec.ts deleted file mode 100644 index 776fa493..00000000 --- a/packages/core-editor/test/relationship.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { type ILibrary, LibraryContext, LibraryHandle } from "@nanoforge-dev/common"; -import { describe, expect, it } from "vitest"; - -import { Library } from "../../common/src/library/libraries/library"; -import { Relationship } from "../src/common/library/relationship-functions"; - -class StubLibrary extends Library { - private readonly _name: string; - - constructor(name: string, options?: ConstructorParameters[0]) { - super(options); - this._name = name; - } - - get __name(): string { - return this._name; - } -} - -const makeHandle = ( - sym: symbol, - name: string, - options?: ConstructorParameters[0], -): LibraryHandle => { - return new LibraryHandle( - sym, - new StubLibrary(name, options) as unknown as ILibrary, - new LibraryContext(), - ); -}; - -describe("Relationship.getLibrariesByDependencies", () => { - it("should return libraries in same order when no dependencies are declared", () => { - const symA = Symbol("A"); - const symB = Symbol("B"); - const handleA = makeHandle(symA, "A"); - const handleB = makeHandle(symB, "B"); - - const result = Relationship.getLibrariesByDependencies([handleA, handleB]); - expect(result.map((h) => h.library.__name)).toEqual(["A", "B"]); - }); - - it("should put a dependency before the library that depends on it", () => { - const symA = Symbol("A"); - const symB = Symbol("B"); - const handleB = makeHandle(symB, "B"); - const handleA = makeHandle(symA, "A", { dependencies: [symB] }); - - const result = Relationship.getLibrariesByDependencies([handleA, handleB]); - const names = result.map((h) => h.library.__name); - expect(names.indexOf("B")).toBeLessThan(names.indexOf("A")); - }); - - it("should not duplicate a shared dependency", () => { - const symDep = Symbol("Dep"); - const symA = Symbol("A"); - const symB = Symbol("B"); - const handleDep = makeHandle(symDep, "Dep"); - const handleA = makeHandle(symA, "A", { dependencies: [symDep] }); - const handleB = makeHandle(symB, "B", { dependencies: [symDep] }); - - const result = Relationship.getLibrariesByDependencies([handleA, handleB, handleDep]); - const names = result.map((h) => h.library.__name); - expect(names.filter((n) => n === "Dep")).toHaveLength(1); - }); - - it("should return libraries in reverse dependency order when reverse=true", () => { - const symA = Symbol("A"); - const symB = Symbol("B"); - const handleB = makeHandle(symB, "B"); - const handleA = makeHandle(symA, "A", { dependencies: [symB] }); - - const result = Relationship.getLibrariesByDependencies([handleA, handleB], true); - const names = result.map((h) => h.library.__name); - expect(names.indexOf("A")).toBeLessThan(names.indexOf("B")); - }); - - it("should throw on circular dependencies", () => { - const symA = Symbol("A"); - const symB = Symbol("B"); - const handleA = makeHandle(symA, "A", { dependencies: [symB] }); - const handleB = makeHandle(symB, "B", { dependencies: [symA] }); - - expect(() => Relationship.getLibrariesByDependencies([handleA, handleB])).toThrow( - /[Cc]ircular/, - ); - }); -}); - -describe("Relationship.getLibrariesByRun", () => { - it("should return all runner libraries when no ordering is specified", () => { - const symA = Symbol("A"); - const symB = Symbol("B"); - const handleA = makeHandle(symA, "A"); - const handleB = makeHandle(symB, "B"); - - const result = Relationship.getLibrariesByRun([handleA, handleB]); - expect(result).toHaveLength(2); - }); - - it("should place a library before another when runBefore is set", () => { - const symA = Symbol("A"); - const symB = Symbol("B"); - const handleA = makeHandle(symA, "A", { runBefore: [symB] }); - const handleB = makeHandle(symB, "B"); - - const result = Relationship.getLibrariesByRun([handleA, handleB]); - const names = result.map((h) => h.library.__name); - expect(names.indexOf("B")).toBeLessThan(names.indexOf("A")); - }); - - it("should place a library after another when runAfter is set", () => { - const symA = Symbol("A"); - const symB = Symbol("B"); - const handleA = makeHandle(symA, "A"); - const handleB = makeHandle(symB, "B", { runAfter: [symA] }); - - const result = Relationship.getLibrariesByRun([handleA, handleB]); - const names = result.map((h) => h.library.__name); - expect(names.indexOf("B")).toBeLessThan(names.indexOf("A")); - }); - - it("should throw on circular run dependencies", () => { - const symA = Symbol("A"); - const symB = Symbol("B"); - const handleA = makeHandle(symA, "A", { runBefore: [symB] }); - const handleB = makeHandle(symB, "B", { runBefore: [symA] }); - - expect(() => Relationship.getLibrariesByRun([handleA, handleB])).toThrow(/[Cc]ircular/); - }); - - it("should return empty array for empty input", () => { - expect(Relationship.getLibrariesByRun([])).toHaveLength(0); - }); -}); diff --git a/packages/core-editor/tsconfig.json b/packages/core-editor/tsconfig.json index 9e6d724b..2a9635c8 100644 --- a/packages/core-editor/tsconfig.json +++ b/packages/core-editor/tsconfig.json @@ -2,5 +2,10 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.json", "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "paths": { + "@core/*": ["../core/src"] + } + } } diff --git a/packages/core/src/application/application-config.ts b/packages/core/src/application/application-config.ts index 82d28bed..7eeede0c 100644 --- a/packages/core/src/application/application-config.ts +++ b/packages/core/src/application/application-config.ts @@ -31,8 +31,8 @@ export class ApplicationConfig { this._libraryManager.set(sym, library); } - public getComponentSystemLibrary() { - return this._libraryManager.getComponentSystem(); + public getComponentSystemLibrary() { + return this._libraryManager.getComponentSystem(); } public useComponentSystemLibrary(library: IComponentSystemLibrary) { From e985ead67fa301750d5b51fdee09e4ef993e269e Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 31 Mar 2026 11:47:43 +0900 Subject: [PATCH 12/14] fix(core-editor): version as str --- packages/core-editor/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-editor/package.json b/packages/core-editor/package.json index c2fe4a6a..74bd0ece 100644 --- a/packages/core-editor/package.json +++ b/packages/core-editor/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@nanoforge-dev/core-editor", - "version": "0.0", + "version": "0.0.0", "description": "NanoForge Engine - Core Editor", "keywords": [ "nanoforge", From be0ce854e3cb60d051b7c0c36ec535f61623e4c7 Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 31 Mar 2026 15:04:26 +0900 Subject: [PATCH 13/14] fix(core-editor): review --- packages/core-editor/CHANGELOG.md | 14 -------------- packages/core-editor/README.md | 26 ++++++-------------------- packages/core-editor/tsconfig.json | 7 +------ 3 files changed, 7 insertions(+), 40 deletions(-) diff --git a/packages/core-editor/CHANGELOG.md b/packages/core-editor/CHANGELOG.md index 53aaa9de..e69de29b 100644 --- a/packages/core-editor/CHANGELOG.md +++ b/packages/core-editor/CHANGELOG.md @@ -1,14 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -## Documentation - -## Bug Fixes - -## Documentation - -## Features - -## Refactor - diff --git a/packages/core-editor/README.md b/packages/core-editor/README.md index af648541..7daa7a82 100644 --- a/packages/core-editor/README.md +++ b/packages/core-editor/README.md @@ -5,18 +5,20 @@


- npm version - npm downloads + npm version + npm downloads Tests status Documentation status - Last commit + Last commit Contributors

## About -`@nanoforge-dev/core-editor` is a core package that contains game main loop. It is used to initialize the game and run it. +`@nanoforge-dev/core-editor` is a package made to allow the editor to make change to a game even if it's already compiled and running. + +If you want a core to run your game use the `@nanoforge-dev/core`. ## Installation @@ -29,22 +31,6 @@ pnpm add @nanoforge-dev/core-editor bun add @nanoforge-dev/core-editor ``` -## Example usage - -Initialize the game in your main file. - -```ts -import { IEditorRunOptions, NanoforgeFactory } from "@nanoforge-dev/core-editor"; - -export async function main(options: IEditorRunOptions) { - const app = NanoforgeFactory.createClient(); - - await app.init(options); - - await app.run(); -} -``` - ## Links - [GitHub][source] diff --git a/packages/core-editor/tsconfig.json b/packages/core-editor/tsconfig.json index 2a9635c8..9e6d724b 100644 --- a/packages/core-editor/tsconfig.json +++ b/packages/core-editor/tsconfig.json @@ -2,10 +2,5 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.json", "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"], - "compilerOptions": { - "paths": { - "@core/*": ["../core/src"] - } - } + "exclude": ["node_modules", "dist"] } From 767a2f1de4998366aa62f2fa656d842e0f51596e Mon Sep 17 00:00:00 2001 From: Tchips46 Date: Tue, 31 Mar 2026 16:14:00 +0900 Subject: [PATCH 14/14] fix(core-editor): review --- .../core-editor/src/common/context/options.type.ts | 10 +++++----- packages/core-editor/test/editor-feature.spec.ts | 2 +- .../helpers/event-emitter.ts} | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename packages/core-editor/{src/editor/event-emitter.manager.ts => test/helpers/event-emitter.ts} (96%) diff --git a/packages/core-editor/src/common/context/options.type.ts b/packages/core-editor/src/common/context/options.type.ts index 1fc49839..eabf7e62 100644 --- a/packages/core-editor/src/common/context/options.type.ts +++ b/packages/core-editor/src/common/context/options.type.ts @@ -1,4 +1,4 @@ -import { type EventEmitter } from "../../editor/event-emitter.manager"; +import { type IEventEmitter } from "./event-emitter.type"; import { type Save } from "./save.type"; export type IEditorRunOptions = IEditorRunClientOptions | IEditorRunServerOptions; @@ -9,8 +9,8 @@ export interface IEditorRunClientOptions { env: Record; editor: { save: Save; - coreEvents: EventEmitter; - editorEvents: EventEmitter; + coreEvents: IEventEmitter; + editorEvents: IEventEmitter; }; } export interface IEditorRunServerOptions { @@ -18,7 +18,7 @@ export interface IEditorRunServerOptions { env: Record; editor: { save: Save; - coreEvents: EventEmitter; - editorEvents: EventEmitter; + coreEvents: IEventEmitter; + editorEvents: IEventEmitter; }; } diff --git a/packages/core-editor/test/editor-feature.spec.ts b/packages/core-editor/test/editor-feature.spec.ts index bd442f78..c02f1a03 100644 --- a/packages/core-editor/test/editor-feature.spec.ts +++ b/packages/core-editor/test/editor-feature.spec.ts @@ -5,7 +5,7 @@ import { EventTypeEnum } from "../src/common/context/event-emitter.type"; import type { IEditorRunOptions } from "../src/common/context/options.type"; import { type Save, type SaveComponent, type SaveEntity } from "../src/common/context/save.type"; import { CoreEditor } from "../src/editor/core-editor"; -import { EventEmitter } from "../src/editor/event-emitter.manager"; +import { EventEmitter } from "./helpers/event-emitter"; describe("EditorFeatures", () => { afterEach(() => { diff --git a/packages/core-editor/src/editor/event-emitter.manager.ts b/packages/core-editor/test/helpers/event-emitter.ts similarity index 96% rename from packages/core-editor/src/editor/event-emitter.manager.ts rename to packages/core-editor/test/helpers/event-emitter.ts index 3498d826..95c6eb62 100644 --- a/packages/core-editor/src/editor/event-emitter.manager.ts +++ b/packages/core-editor/test/helpers/event-emitter.ts @@ -2,7 +2,7 @@ import { type EventTypeEnum, type IEventEmitter, type ListenerType, -} from "../common/context/event-emitter.type"; +} from "../../src/common/context/event-emitter.type"; export class EventEmitter implements IEventEmitter { public listeners: Record = {};