diff --git a/packages/core-editor/.cliff-jumperrc.json b/packages/core-editor/.cliff-jumperrc.json new file mode 100644 index 00000000..c760745b --- /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-editor", + "org": "nanoforge-dev", + "packagePath": "packages/core-editor", + "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..b5da13ff --- /dev/null +++ b/packages/core-editor/.idea/.name @@ -0,0 +1 @@ +[NanoForge] Engine Core Editor \ No newline at end of file diff --git a/packages/core-editor/.idea/[NanoForge] Engine Core Editor.iml b/packages/core-editor/.idea/[NanoForge] Engine Core Editor.iml new file mode 100644 index 00000000..24643cc3 --- /dev/null +++ b/packages/core-editor/.idea/[NanoForge] Engine Core Editor.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..529602fb --- /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..e69de29b 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..7daa7a82 --- /dev/null +++ b/packages/core-editor/README.md @@ -0,0 +1,53 @@ +
+
+

+ NanoForge +

+
+

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

+
+ +## About + +`@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 + +**Node.js 24.11.0 or newer is required.** + +```sh +npm install @nanoforge-dev/core-editor +yarn add @nanoforge-dev/core-editor +pnpm add @nanoforge-dev/core-editor +bun add @nanoforge-dev/core-editor +``` + +## 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-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 new file mode 100644 index 00000000..a9abf0b7 --- /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-editor@[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..74bd0ece --- /dev/null +++ b/packages/core-editor/package.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@nanoforge-dev/core-editor", + "version": "0.0.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/ecs-client": "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/nanoforge-application.ts b/packages/core-editor/src/application/nanoforge-application.ts new file mode 100644 index 00000000..66bb18eb --- /dev/null +++ b/packages/core-editor/src/application/nanoforge-application.ts @@ -0,0 +1,57 @@ +import { + type IAssetManagerLibrary, + type IComponentSystemLibrary, + type ILibrary, + type INetworkLibrary, + NfNotInitializedException, +} from "@nanoforge-dev/common"; + +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"; + +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..98ec685e --- /dev/null +++ b/packages/core-editor/src/application/nanoforge-factory.ts @@ -0,0 +1,15 @@ +import { type IApplicationOptions } from "../../../core/src/application/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/event-emitter.type.ts b/packages/core-editor/src/common/context/event-emitter.type.ts new file mode 100644 index 00000000..c90caba3 --- /dev/null +++ b/packages/core-editor/src/common/context/event-emitter.type.ts @@ -0,0 +1,24 @@ +export enum EventTypeEnum { + HOT_RELOAD = "hot-reload", + HARD_RELOAD = "hard-reload", +} + +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 new file mode 100644 index 00000000..eabf7e62 --- /dev/null +++ b/packages/core-editor/src/common/context/options.type.ts @@ -0,0 +1,24 @@ +import { type IEventEmitter } from "./event-emitter.type"; +import { type Save } from "./save.type"; + +export type IEditorRunOptions = IEditorRunClientOptions | IEditorRunServerOptions; + +export interface IEditorRunClientOptions { + canvas: HTMLCanvasElement; + files: Map; + env: Record; + editor: { + save: Save; + coreEvents: IEventEmitter; + editorEvents: IEventEmitter; + }; +} +export interface IEditorRunServerOptions { + files: Map; + env: Record; + editor: { + save: Save; + coreEvents: IEventEmitter; + editorEvents: IEventEmitter; + }; +} diff --git a/packages/core-editor/src/common/context/save.type.ts b/packages/core-editor/src/common/context/save.type.ts new file mode 100644 index 00000000..36372a33 --- /dev/null +++ b/packages/core-editor/src/common/context/save.type.ts @@ -0,0 +1,38 @@ +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; + paramsNames: string[]; +} + +export interface SaveSystem { + name: string; + path: string; +} + +export interface SaveEntity { + id: string; + components: Record>; +} + +export interface Save { + libraries: SaveLibrary[]; + components: SaveComponent[]; + systems: SaveSystem[]; + entities: SaveEntity[]; +} diff --git a/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts new file mode 100644 index 00000000..b917ce2e --- /dev/null +++ b/packages/core-editor/src/core/core.ts @@ -0,0 +1,112 @@ +import { + ClearContext, + ClientLibraryManager, + Context, + type IRunnerLibrary, + InitContext, + type LibraryHandle, + LibraryStatusEnum, + NfNotInitializedException, +} from "@nanoforge-dev/common"; +import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; + +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 { 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) { + 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)); + this.editor = new CoreEditor( + options.editor, + this.config.getComponentSystemLibrary().library, + ); + } + + 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); + this.editor?.runEvents(); + 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/editor/core-editor.ts b/packages/core-editor/src/editor/core-editor.ts new file mode 100644 index 00000000..553638d5 --- /dev/null +++ b/packages/core-editor/src/editor/core-editor.ts @@ -0,0 +1,51 @@ +import { NfNotFound } from "@nanoforge-dev/common"; +import { type ECSClientLibrary, type Entity } from "@nanoforge-dev/ecs-client"; + +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(editor: IEditorRunOptions["editor"], ecsLibrary: ECSClientLibrary) { + this.editor = editor; + this.ecsLibrary = ecsLibrary; + this.editor.coreEvents?.addListener(EventTypeEnum.HOT_RELOAD, this.askEntitiesHotReload); + } + + public runEvents() { + this.editor.coreEvents?.runEvents(); + } + + public askEntitiesHotReload(): void { + const reg = this.ecsLibrary.registry; + const save = this.editor.save; + save.entities.forEach(({ id, components }) => { + Object.entries(components).forEach(([componentName, params]) => { + const ogComponent = save.components.find(({ name: paramName }) => { + return paramName == componentName; + }); + if (!ogComponent) { + throw new NfNotFound("Component: " + componentName + " not found in saved components"); + } + const ecsEntity: Entity = this.getEntityFromEntityId(id); + const ecsComponent = reg.getEntityComponent(ecsEntity, { + name: componentName, + }); + Object.entries(params).forEach(([paramName, paramValue]) => { + ecsComponent[paramName] = paramValue; + }); + 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/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/editor-feature.spec.ts b/packages/core-editor/test/editor-feature.spec.ts new file mode 100644 index 00000000..c02f1a03 --- /dev/null +++ b/packages/core-editor/test/editor-feature.spec.ts @@ -0,0 +1,149 @@ +import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +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 "./helpers/event-emitter"; + +describe("EditorFeatures", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("eventEmitter", () => { + it("should execute eventQueue once", async () => { + const events = new EventEmitter(); + events.emitEvent(EventTypeEnum.HOT_RELOAD); + events.emitEvent(EventTypeEnum.HOT_RELOAD); + const spyHotReload = vi + .spyOn(CoreEditor.prototype, "askEntitiesHotReload") + .mockImplementation(() => {}); + new CoreEditor( + { coreEvents: events } as IEditorRunOptions["editor"], + {} as ECSClientLibrary, + ).runEvents(); + expect(spyHotReload).toHaveBeenCalledTimes(2); + }); + }); + + 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", + 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( + { + 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", + 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/core-editor/test/helpers/event-emitter.ts b/packages/core-editor/test/helpers/event-emitter.ts new file mode 100644 index 00000000..95c6eb62 --- /dev/null +++ b/packages/core-editor/test/helpers/event-emitter.ts @@ -0,0 +1,50 @@ +import { + type EventTypeEnum, + type IEventEmitter, + type ListenerType, +} from "../../src/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/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 +} 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) { 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/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; 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 4b78210a..35a1e681 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -420,6 +420,55 @@ 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/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.3(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':