diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 1fcdda08..bb68c8b4 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,12 +1,8 @@
* @Exeloo
/packages/core @Tchips46
-/packages/core-editor @Tchips46
-/packages/ecs-client @MartinFillon @Tchips46
-/packages/ecs-lib @MartinFillon @Tchips46
-/packages/ecs-server @MartinFillon @Tchips46
-/packages/network-client @Tchips46
-/packages/network-server @Tchips46
+/modules/ecs @MartinFillon @Tchips46
+/modules/network @Tchips46
/scripts @MartinFillon
/typedoc.* @MartinFillon
diff --git a/.github/ISSUE_TEMPLATE/01-packages_bug_report.yml b/.github/ISSUE_TEMPLATE/01-packages_bug_report.yml
index 469f0ed2..5f9af55b 100644
--- a/.github/ISSUE_TEMPLATE/01-packages_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/01-packages_bug_report.yml
@@ -13,20 +13,16 @@ body:
attributes:
label: Which package is this bug report for?
options:
- - asset-manager
+ - asset
- common
- - config
- core
- - core-editor
- - ecs-client
- - ecs-lib
- - ecs-server
+ - ecs
+ - editor
+ - env
- graphics-2d
- - graphics-2d-editor
- input
- music
- - network-client
- - network-server
+ - network
- sound
- other
validations:
@@ -63,7 +59,7 @@ body:
description: List necessary versions here. This includes your package version, runtime version, operating system etc.
placeholder: |
- @nanoforge-dev/common 1.0.0 (`npm ls @nanoforge-dev/common` or another package)
- - Node.js 25 (`node --version`)
+ - Node.js 26 (`node --version`)
- TypeScript 5.9.3 (`npm ls typescript` if you use it)
- macOS Ventura 13.3.1
validations:
diff --git a/.github/ISSUE_TEMPLATE/02-utils_bug_report.yml b/.github/ISSUE_TEMPLATE/02-utils_bug_report.yml
index 79f4425f..bc1c17f8 100644
--- a/.github/ISSUE_TEMPLATE/02-utils_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/02-utils_bug_report.yml
@@ -38,7 +38,7 @@ body:
description: List necessary versions here. This includes your package version, runtime version, operating system etc.
placeholder: |
- @nanoforge-dev/utils-eslint-config 1.0.0 (`npm ls @nanoforge-dev/utils-eslint-config` or another package)
- - Node.js 25 (`node --version`)
+ - Node.js 26 (`node --version`)
- TypeScript 5.9.3 (`npm ls typescript` if you use it)
- macOS Ventura 13.3.1
validations:
diff --git a/.github/ISSUE_TEMPLATE/03-feature_request.yml b/.github/ISSUE_TEMPLATE/03-feature_request.yml
index 19c3b871..e8ad2b89 100644
--- a/.github/ISSUE_TEMPLATE/03-feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/03-feature_request.yml
@@ -11,20 +11,16 @@ body:
attributes:
label: Which package or utils is this feature request for?
options:
- - asset-manager
+ - asset
- common
- - config
- core
- - core-editor
- - ecs-client
- - ecs-lib
- - ecs-server
+ - ecs
+ - editor
+ - env
- graphics-2d
- - graphics-2d-editor
- input
- music
- - network-client
- - network-server
+ - network
- sound
- eslint-config
- prettier-config
diff --git a/.github/actions/pnpm-install/action.yml b/.github/actions/pnpm-install/action.yml
deleted file mode 100644
index 6a6dff16..00000000
--- a/.github/actions/pnpm-install/action.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-name: "pnpm install"
-description: "Run pnpm install with cache enabled"
-inputs:
- path:
- description: "Path to the directory containing the pnpm workspace"
- default: "."
-runs:
- using: "composite"
- steps:
- - name: Set up swap space
- if: runner.os == 'Linux'
- uses: pierotofy/set-swap-space@v1.0
- with:
- swap-size-gb: 10
-
- - uses: pnpm/action-setup@v4.1.0
- name: Install pnpm
- with:
- run_install: false
- package_json_file: ${{ inputs.path }}/package.json
-
- - name: Expose pnpm config(s) through "$GITHUB_OUTPUT"
- id: pnpm-config
- shell: bash
- run: |
- echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
-
- - name: Cache rotation keys
- id: cache-rotation
- shell: bash
- run: |
- echo "YEAR_MONTH=$(/bin/date -u "+%Y%m")" >> $GITHUB_OUTPUT
-
- - uses: actions/cache@v4
- name: Setup pnpm cache
- with:
- path: ${{ steps.pnpm-config.outputs.STORE_PATH }}
- key: ${{ runner.os }}-pnpm-store-cache-${{ steps.cache-rotation.outputs.YEAR_MONTH }}-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-pnpm-store-cache-${{ steps.cache-rotation.outputs.YEAR_MONTH }}-
-
- - name: Install dependencies
- shell: bash
- run: |
- pnpm install --frozen-lockfile --prefer-offline --loglevel error
- env:
- HUSKY: "0"
- working-directory: ${{ inputs.path }}
diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml
index 5a33c279..1f6fedc3 100644
--- a/.github/actions/prepare/action.yml
+++ b/.github/actions/prepare/action.yml
@@ -1,9 +1,6 @@
name: prepare
description: Prepare action
inputs:
- node-install:
- description: Install node
- default: "true"
pnpm-install:
description: Install pnpm depedencies
default: "true"
@@ -16,17 +13,12 @@ inputs:
runs:
using: composite
steps:
- - if: inputs.node-install == 'true'
- name: Install Node.js
- uses: actions/setup-node@v6
- with:
- node-version: 25
- package-manager-cache: false
- registry-url: https://registry.npmjs.org
-
- if: inputs.pnpm-install == 'true'
- name: Install dependencies
- uses: ./.github/actions/pnpm-install
+ name: Install pnpm and Node.js
+ uses: pnpm/setup@c9883cc79df532ad1a7b81bf9ab944ceb090d65c
+ with:
+ runtime: node@26
+ cache: true
- if: inputs.emsdk-install == 'true'
name: Install emsdk
diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml
index 450cdb9a..f880a66d 100644
--- a/.github/issue-labeler.yml
+++ b/.github/issue-labeler.yml
@@ -1,62 +1,46 @@
-packages:asset-manager:
+modules:ecs:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\nasset-manager\\n"
+ report|feature request) for\\?\\n\\necs\\n"
-packages:common:
- - "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\ncommon\\n"
-
-packages:config:
- - "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\nconfig\\n"
-
-packages:core:
- - "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\ncore\\n"
-
-packages:core-editor:
- - "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\ncore-editor\\n"
-
-packages:ecs-client:
+modules:editor:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\necs-client\\n"
+ report|feature request) for\\?\\n\\neditor\\n"
-packages:ecs-lib:
+modules:graphics-2d:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\necs-lib\\n"
+ report|feature request) for\\?\\n\\ngraphics-2d\\n"
-packages:ecs-server:
+modules:input:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\necs-server\\n"
+ report|feature request) for\\?\\n\\ninput\\n"
-packages:graphics-2d:
+modules:music:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\ngraphics-2d\\n"
+ report|feature request) for\\?\\n\\nmusic\\n"
-packages:graphics-2d-editor:
+modules:network:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\ngraphics-2d-editor\\n"
+ report|feature request) for\\?\\n\\nnetwork\\n"
-packages:input:
+modules:sound:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\ninput\\n"
+ report|feature request) for\\?\\n\\nsound\\n"
-packages:music:
+packages:asset:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\nmusic\\n"
+ report|feature request) for\\?\\n\\nasset\\n"
-packages:network-client:
+packages:common:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\nnetwork-client\\n"
+ report|feature request) for\\?\\n\\ncommon\\n"
-packages:network-server:
+packages:core:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\nnetwork-server\\n"
+ report|feature request) for\\?\\n\\ncore\\n"
-packages:sound:
+packages:env:
- "### Which (package|utils|package or utils) is this (bug
- report|feature request) for\\?\\n\\nsound\\n"
+ report|feature request) for\\?\\n\\nenv\\n"
utils:eslint-config:
- "### Which (package|utils|package or utils) is this (bug
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 311df846..1843dc1e 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,92 +1,76 @@
-packages:asset-manager:
- - changed-files:
- - any-glob-to-any-file:
- - packages/asset-manager/*
- - packages/asset-manager/**/*
-
-packages:common:
- - changed-files:
- - any-glob-to-any-file:
- - packages/common/*
- - packages/common/**/*
-
-packages:config:
- - changed-files:
- - any-glob-to-any-file:
- - packages/config/*
- - packages/config/**/*
-
-packages:core:
+documentation:
- changed-files:
- any-glob-to-any-file:
- - packages/core/*
- - packages/core/**/*
+ - docs/*
+ - docs/**/*
+ - modules/ecs/docs/*
+ - modules/ecs/docs/**/*
-packages:core-editor:
+modules:ecs:
- changed-files:
- any-glob-to-any-file:
- - packages/core-editor/*
- - packages/core-editor/**/*
+ - modules/ecs/*
+ - modules/ecs/**/*
-packages:ecs-client:
+modules:editor:
- changed-files:
- any-glob-to-any-file:
- - packages/ecs-client/*
- - packages/ecs-client/**/*
+ - modules/editor/*
+ - modules/editor/**/*
-packages:ecs-lib:
+modules:graphics-2d:
- changed-files:
- any-glob-to-any-file:
- - packages/ecs-lib/*
- - packages/ecs-lib/**/*
+ - modules/graphics-2d/*
+ - modules/graphics-2d/**/*
-packages:ecs-server:
+modules:input:
- changed-files:
- any-glob-to-any-file:
- - packages/ecs-server/*
- - packages/ecs-server/**/*
+ - modules/input/*
+ - modules/input/**/*
-packages:graphics-2d:
+modules:music:
- changed-files:
- any-glob-to-any-file:
- - packages/graphics-2d/*
- - packages/graphics-2d/**/*
+ - modules/music/*
+ - modules/music/**/*
-packages:graphics-2d-editor:
+modules:network:
- changed-files:
- any-glob-to-any-file:
- - packages/graphics-2d-editor/*
- - packages/graphics-2d-editor/**/*
+ - modules/network/*
+ - modules/network/**/*
-packages:input:
+modules:sound:
- changed-files:
- any-glob-to-any-file:
- - packages/input/*
- - packages/input/**/*
+ - modules/sound/*
+ - modules/sound/**/*
-packages:music:
+packages:asset:
- changed-files:
- any-glob-to-any-file:
- - packages/music/*
- - packages/music/**/*
+ - packages/asset/*
+ - packages/asset/**/*
-packages:network-client:
+packages:common:
- changed-files:
- any-glob-to-any-file:
- - packages/network-client/*
- - packages/network-client/**/*
+ - packages/common/*
+ - packages/common/**/*
-packages:network-server:
+packages:core:
- changed-files:
- any-glob-to-any-file:
- - packages/network-server/*
- - packages/network-server/**/*
+ - packages/core/*
+ - packages/core/**/*
-packages:sound:
+packages:env:
- changed-files:
- any-glob-to-any-file:
- - packages/sound/*
- - packages/sound/**/*
+ - packages/env/*
+ - packages/env/**/*
utils:eslint-config:
- changed-files:
@@ -99,11 +83,3 @@ utils:prettier-config:
- any-glob-to-any-file:
- utils/prettier-config/*
- utils/prettier-config/**/*
-
-documentation:
- - changed-files:
- - any-glob-to-any-file:
- - docs/*
- - docs/**/*
- - packages/ecs-lib/docs/*
- - packages/ecs-lib/docs/**/*
diff --git a/.github/labels.yml b/.github/labels.yml
index ac53356d..cbcf9397 100644
--- a/.github/labels.yml
+++ b/.github/labels.yml
@@ -42,64 +42,48 @@
description: "This doesn't seem right"
color: "e4e669"
-- name: "packages:asset-manager"
- description: "Related to AssetManager library"
- color: "aaa3dc"
+- name: "modules:ecs"
+ description: "Related to ECS library"
+ color: "1c3f78"
-- name: "packages:common"
- description: "Related to Common library"
- color: "aaa3dc"
-
-- name: "packages:config"
- description: "Related to Config library"
- color: "aaa3dc"
-
-- name: "packages:core"
- description: "Related to Core library"
- color: "aaa3dc"
-
-- name: "packages:core-editor"
- description: "Related to Core Editor library"
- color: "aaa3dc"
-
-- name: "packages:ecs-client"
- description: "Related to ECSClient library"
- color: "aaa3dc"
-
-- name: "packages:ecs-lib"
- description: "Related to Abstract ECS library"
- color: "aaa3dc"
+- name: "modules:editor"
+ description: "Related to Editor library"
+ color: "1c3f78"
-- name: "packages:ecs-server"
- description: "Related to ECSServer library"
- color: "aaa3dc"
-
-- name: "packages:graphics-2d"
+- name: "modules:graphics-2d"
description: "Related to Graphics2D library"
- color: "aaa3dc"
-
-- name: "packages:graphics-2d-editor"
- description: "Related to Graphics2D Editor library"
- color: "aaa3dc"
+ color: "1c3f78"
-- name: "packages:input"
+- name: "modules:input"
description: "Related to Input library"
- color: "aaa3dc"
+ color: "1c3f78"
-- name: "packages:music"
+- name: "modules:music"
description: "Related to Music library"
+ color: "1c3f78"
+
+- name: "modules:network"
+ description: "Related to Network library"
+ color: "1c3f78"
+
+- name: "modules:sound"
+ description: "Related to Sound library"
+ color: "1c3f78"
+
+- name: "packages:asset"
+ description: "Related to AssetManager library"
color: "aaa3dc"
-- name: "packages:network-client"
- description: "Related to NetworkClient library"
+- name: "packages:common"
+ description: "Related to Common library"
color: "aaa3dc"
-- name: "packages:network-server"
- description: "Related to NetworkServer library"
+- name: "packages:core"
+ description: "Related to Core library"
color: "aaa3dc"
-- name: "packages:sound"
- description: "Related to Sound library"
+- name: "packages:env"
+ description: "Related to Env library"
color: "aaa3dc"
- name: "question"
diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml
index 76fe91ea..e10eb915 100644
--- a/.github/workflows/alpha-release.yml
+++ b/.github/workflows/alpha-release.yml
@@ -8,20 +8,16 @@ on:
type: choice
required: true
options:
- - "@nanoforge-dev/asset-manager"
+ - "@nanoforge-dev/asset"
- "@nanoforge-dev/common"
- - "@nanoforge-dev/config"
- "@nanoforge-dev/core"
- - "@nanoforge-dev/core-editor"
- - "@nanoforge-dev/ecs-client"
- - "@nanoforge-dev/ecs-lib"
- - "@nanoforge-dev/ecs-server"
+ - "@nanoforge-dev/ecs"
+ - "@nanoforge-dev/editor-lib"
+ - "@nanoforge-dev/env"
- "@nanoforge-dev/graphics-2d"
- - "@nanoforge-dev/graphics-2d-editor"
- "@nanoforge-dev/input"
- "@nanoforge-dev/music"
- - "@nanoforge-dev/network-client"
- - "@nanoforge-dev/network-server"
+ - "@nanoforge-dev/network"
- "@nanoforge-dev/sound"
- "@nanoforge-dev/utils-eslint-config"
- "@nanoforge-dev/utils-prettier-config"
diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml
index bfb55188..1af60574 100644
--- a/.github/workflows/pre-release.yml
+++ b/.github/workflows/pre-release.yml
@@ -34,7 +34,7 @@ jobs:
- name: Create release PR
uses: ./node_modules/@nanoforge-dev/actions/actions/create-packages-release-pr
with:
- packages: "@nanoforge-dev/asset-manager,@nanoforge-dev/common,@nanoforge-dev/config,@nanoforge-dev/core,@nanoforge-dev/core-editor,@nanoforge-dev/ecs-client,@nanoforge-dev/ecs-lib,@nanoforge-dev/ecs-server,@nanoforge-dev/graphics-2d,@nanoforge-dev/graphics-2d-editor,@nanoforge-dev/input,@nanoforge-dev/music,@nanoforge-dev/network-client,@nanoforge-dev/network-server,@nanoforge-dev/sound"
+ packages: "@nanoforge-dev/asset,@nanoforge-dev/common,@nanoforge-dev/core,@nanoforge-dev/ecs,@nanoforge-dev/editor-lib,@nanoforge-dev/env,@nanoforge-dev/graphics-2d,@nanoforge-dev/input,@nanoforge-dev/music,@nanoforge-dev/network,@nanoforge-dev/sound"
version: ${{ inputs.version }}
branch-format: "releases/nanoforge@{version}"
commit-format: "chore: release @nanoforge-dev/engine@{version}"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e7d15658..12d25e80 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -26,7 +26,7 @@ jobs:
- name: Release packages
uses: ./node_modules/@nanoforge-dev/actions/actions/release-packages
with:
- packages: "@nanoforge-dev/asset-manager,@nanoforge-dev/common,@nanoforge-dev/config,@nanoforge-dev/core,@nanoforge-dev/core-editor,@nanoforge-dev/ecs-client,@nanoforge-dev/ecs-lib,@nanoforge-dev/ecs-server,@nanoforge-dev/graphics-2d,@nanoforge-dev/graphics-2d-editor,@nanoforge-dev/input,@nanoforge-dev/music,@nanoforge-dev/network-client,@nanoforge-dev/network-server,@nanoforge-dev/sound"
+ packages: "@nanoforge-dev/asset,@nanoforge-dev/common,@nanoforge-dev/core,@nanoforge-dev/ecs,@nanoforge-dev/editor-lib,@nanoforge-dev/env,@nanoforge-dev/graphics-2d,@nanoforge-dev/input,@nanoforge-dev/music,@nanoforge-dev/network,@nanoforge-dev/sound"
tag-format: "{version}"
latest: true
env:
diff --git a/.gitignore b/.gitignore
index 1279e044..a9ed69d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -241,3 +241,5 @@ emsdk-cache/
references/
.direnv
+
+packages/old-core/
\ No newline at end of file
diff --git a/.idea/[NanoForge] Engine.iml b/.idea/[NanoForge] Engine.iml
index fc5ad77e..94d8561a 100644
--- a/.idea/[NanoForge] Engine.iml
+++ b/.idea/[NanoForge] Engine.iml
@@ -11,48 +11,39 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/.nvmrc b/.nvmrc
index a682cfb9..4aa0e0a7 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v25
+v26
diff --git a/.prettierignore b/.prettierignore
index 881beaab..98eca11c 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -7,6 +7,7 @@ bun.lock
pnpm-workspace.yaml
packages/
+modules/
example/
utils/
releases/
diff --git a/e2e/game/apps/client/nanoforge.config.ts b/e2e/game/apps/client/nanoforge.config.ts
new file mode 100644
index 00000000..d220af78
--- /dev/null
+++ b/e2e/game/apps/client/nanoforge.config.ts
@@ -0,0 +1,5 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "client",
+});
diff --git a/e2e/game/apps/client/package.json b/e2e/game/apps/client/package.json
new file mode 100644
index 00000000..64c59bd6
--- /dev/null
+++ b/e2e/game/apps/client/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "e2e-client",
+ "version": "0.0.0",
+ "description": "",
+ "author": "",
+ "scripts": {
+ "build": "nf build",
+ "start": "nf start",
+ "lint": "prettier --check . && eslint --format=pretty \"{client,server}/**/*.ts\"",
+ "format": "prettier --write . && eslint --fix --format=pretty \"{client,server}/**/*.ts\""
+ },
+ "devDependencies": {
+ "@nanoforge-dev/asset": "workspace:*",
+ "@nanoforge-dev/common": "workspace:*",
+ "@nanoforge-dev/core": "workspace:*",
+ "@nanoforge-dev/env": "workspace:*",
+ "@nanoforge-dev/ecs": "workspace:*",
+ "@nanoforge-dev/network": "workspace:*",
+ "@nanoforge-dev/graphics-2d": "workspace:*",
+ "@nanoforge-dev/input": "workspace:*",
+ "typescript": "catalog:core"
+ },
+ "private": true,
+ "lint-staged": {
+ "**": [
+ "prettier --ignore-unknown --write"
+ ],
+ "{client,server}/**/*.ts": [
+ "eslint --fix"
+ ]
+ }
+}
diff --git a/e2e/game/server/components/example.component.ts b/e2e/game/apps/client/src/components/example.component.ts
similarity index 99%
rename from e2e/game/server/components/example.component.ts
rename to e2e/game/apps/client/src/components/example.component.ts
index 6957308b..25f486bd 100644
--- a/e2e/game/server/components/example.component.ts
+++ b/e2e/game/apps/client/src/components/example.component.ts
@@ -1,4 +1,4 @@
-import { type EditorComponentManifest } from "@nanoforge-dev/ecs-server";
+import { type EditorComponentManifest } from "@nanoforge-dev/ecs/client";
export class ExampleComponent {
name = this.constructor.name;
diff --git a/e2e/game/client/main.ts b/e2e/game/apps/client/src/main.ts
similarity index 60%
rename from e2e/game/client/main.ts
rename to e2e/game/apps/client/src/main.ts
index 0f2057d1..989aa900 100644
--- a/e2e/game/client/main.ts
+++ b/e2e/game/apps/client/src/main.ts
@@ -1,36 +1,31 @@
-import { AssetManagerLibrary } from "@nanoforge-dev/asset-manager";
-import { type IRunOptions } from "@nanoforge-dev/common";
+import type { ClientRunOptions } from "@nanoforge-dev/common";
import { NanoforgeFactory } from "@nanoforge-dev/core";
-import { ECSClientLibrary } from "@nanoforge-dev/ecs-client";
+import { EcsLibrary } from "@nanoforge-dev/ecs/client";
import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d";
import { InputLibrary } from "@nanoforge-dev/input";
import { MusicLibrary } from "@nanoforge-dev/music";
-import { NetworkClientLibrary } from "@nanoforge-dev/network-client";
+import { NetworkClientLibrary } from "@nanoforge-dev/network/client";
import { SoundLibrary } from "@nanoforge-dev/sound";
import { ExampleComponent } from "./components/example.component";
import { exampleSystem } from "./systems/example.system";
-export async function main(options: IRunOptions) {
- const app = NanoforgeFactory.createClient({
- tickRate: 60,
- });
+export const main = async (options: ClientRunOptions): Promise => {
+ const app = NanoforgeFactory.createClient({ tickRate: 60 });
- const assetManager = new AssetManagerLibrary();
- const ecs = new ECSClientLibrary();
const graphics = new Graphics2DLibrary();
+ const ecs = new EcsLibrary();
const input = new InputLibrary();
const music = new MusicLibrary();
const network = new NetworkClientLibrary();
const sound = new SoundLibrary();
- app.useAssetManager(assetManager);
- app.useComponentSystem(ecs);
- app.useGraphics(graphics);
- app.useInput(input);
- app.useSound(sound);
- app.useNetwork(network);
- app.use(Symbol("music"), music);
+ app.use(graphics);
+ app.use(ecs);
+ app.use(input);
+ app.use(music);
+ app.use(network);
+ app.use(sound);
await app.init({
...options,
@@ -50,4 +45,4 @@ export async function main(options: IRunOptions) {
registry.addSystem(exampleSystem);
await app.run();
-}
+};
diff --git a/e2e/game/server/systems/example.system.ts b/e2e/game/apps/client/src/systems/example.system.ts
similarity index 97%
rename from e2e/game/server/systems/example.system.ts
rename to e2e/game/apps/client/src/systems/example.system.ts
index 7400df48..84552fb6 100644
--- a/e2e/game/server/systems/example.system.ts
+++ b/e2e/game/apps/client/src/systems/example.system.ts
@@ -1,5 +1,5 @@
import { type Context } from "@nanoforge-dev/common";
-import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs-server";
+import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs/client";
import { ExampleComponent } from "../components/example.component";
diff --git a/e2e/game/apps/server/nanoforge.config.ts b/e2e/game/apps/server/nanoforge.config.ts
new file mode 100644
index 00000000..bc5491cb
--- /dev/null
+++ b/e2e/game/apps/server/nanoforge.config.ts
@@ -0,0 +1,5 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "server",
+});
diff --git a/e2e/game/apps/server/package.json b/e2e/game/apps/server/package.json
new file mode 100644
index 00000000..805853d9
--- /dev/null
+++ b/e2e/game/apps/server/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "e2e-server",
+ "version": "0.0.0",
+ "description": "",
+ "author": "",
+ "scripts": {
+ "build": "nf build",
+ "start": "nf start",
+ "lint": "prettier --check . && eslint --format=pretty \"{client,server}/**/*.ts\"",
+ "format": "prettier --write . && eslint --fix --format=pretty \"{client,server}/**/*.ts\""
+ },
+ "devDependencies": {
+ "@nanoforge-dev/asset": "workspace:*",
+ "@nanoforge-dev/common": "workspace:*",
+ "@nanoforge-dev/core": "workspace:*",
+ "@nanoforge-dev/env": "workspace:*",
+ "@nanoforge-dev/ecs": "workspace:*",
+ "@nanoforge-dev/network": "workspace:*",
+ "typescript": "catalog:core"
+ },
+ "private": true,
+ "lint-staged": {
+ "**": [
+ "prettier --ignore-unknown --write"
+ ],
+ "{client,server}/**/*.ts": [
+ "eslint --fix"
+ ]
+ }
+}
diff --git a/e2e/game/client/components/example.component.ts b/e2e/game/apps/server/src/components/example.component.ts
similarity index 99%
rename from e2e/game/client/components/example.component.ts
rename to e2e/game/apps/server/src/components/example.component.ts
index 4e16c688..39d9fce0 100644
--- a/e2e/game/client/components/example.component.ts
+++ b/e2e/game/apps/server/src/components/example.component.ts
@@ -1,4 +1,4 @@
-import { type EditorComponentManifest } from "@nanoforge-dev/ecs-client";
+import { type EditorComponentManifest } from "@nanoforge-dev/ecs/server";
export class ExampleComponent {
name = this.constructor.name;
diff --git a/e2e/game/server/main.ts b/e2e/game/apps/server/src/main.ts
similarity index 56%
rename from e2e/game/server/main.ts
rename to e2e/game/apps/server/src/main.ts
index b2e236c0..0fef76aa 100644
--- a/e2e/game/server/main.ts
+++ b/e2e/game/apps/server/src/main.ts
@@ -1,25 +1,19 @@
-import { AssetManagerLibrary } from "@nanoforge-dev/asset-manager";
-import { type IRunOptions } from "@nanoforge-dev/common";
+import { type RunOptions } from "@nanoforge-dev/common";
import { NanoforgeFactory } from "@nanoforge-dev/core";
-import { ECSServerLibrary } from "@nanoforge-dev/ecs-server";
+import { EcsLibrary } from "@nanoforge-dev/ecs/server";
import { ExampleComponent } from "./components/example.component";
import { exampleSystem } from "./systems/example.system";
import { TickerLibrary } from "./ticker.library";
-const TICKER = Symbol.for("ticker");
-
-export async function main(options: IRunOptions) {
+export const main = async (options: RunOptions): Promise => {
const app = NanoforgeFactory.createServer({ tickRate: 60 });
- const assetManager = new AssetManagerLibrary();
- const ecs = new ECSServerLibrary();
-
+ const ecs = new EcsLibrary();
const ticker = new TickerLibrary(5);
- app.useAssetManager(assetManager);
- app.useComponentSystem(ecs);
- app.use(TICKER, ticker);
+ app.use(ecs);
+ app.use(ticker);
await app.init(options);
@@ -33,4 +27,4 @@ export async function main(options: IRunOptions) {
await app.run();
await ticker.done;
-}
+};
diff --git a/e2e/game/client/systems/example.system.ts b/e2e/game/apps/server/src/systems/example.system.ts
similarity index 97%
rename from e2e/game/client/systems/example.system.ts
rename to e2e/game/apps/server/src/systems/example.system.ts
index 39625aad..34b06e0a 100644
--- a/e2e/game/client/systems/example.system.ts
+++ b/e2e/game/apps/server/src/systems/example.system.ts
@@ -1,5 +1,5 @@
import { type Context } from "@nanoforge-dev/common";
-import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs-client";
+import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs/server";
import { ExampleComponent } from "../components/example.component";
diff --git a/e2e/game/apps/server/src/ticker.library.ts b/e2e/game/apps/server/src/ticker.library.ts
new file mode 100644
index 00000000..44d10098
--- /dev/null
+++ b/e2e/game/apps/server/src/ticker.library.ts
@@ -0,0 +1,35 @@
+import { type Context, defineLibraryKey, Library } from "@nanoforge-dev/common";
+
+declare module "@nanoforge-dev/common" {
+ interface Context {
+ ticker: unknown;
+ }
+}
+
+/**
+ * Stops the game loop after a fixed number of ticks.
+ * Exposes `done`, which resolves once the loop has stopped.
+ */
+export class TickerLibrary extends Library {
+ readonly key = defineLibraryKey("ticker");
+
+ private _remaining: number;
+ private _resolve!: () => void;
+
+ public readonly done: Promise = new Promise((resolve) => {
+ this._resolve = resolve;
+ });
+
+ constructor(ticks: number) {
+ super();
+ this._remaining = ticks;
+ }
+
+ public override async __run(ctx: Context): Promise {
+ this._remaining--;
+ if (this._remaining <= 0) {
+ ctx.app.requestStop();
+ this._resolve();
+ }
+ }
+}
diff --git a/e2e/game/nanoforge.config.json b/e2e/game/nanoforge.config.json
deleted file mode 100644
index 1e4becd5..00000000
--- a/e2e/game/nanoforge.config.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "client": {
- "build": {
- "entryFile": "client/main.ts",
- "outDir": ".nanoforge/client"
- },
- "runtime": {
- "dir": ".nanoforge/client"
- }
- },
- "server": {
- "enable": true,
- "build": {
- "entryFile": "server/main.ts",
- "outDir": ".nanoforge/server"
- },
- "runtime": {
- "dir": ".nanoforge/server"
- }
- }
-}
diff --git a/e2e/game/nanoforge.config.ts b/e2e/game/nanoforge.config.ts
new file mode 100644
index 00000000..1bbf8d4d
--- /dev/null
+++ b/e2e/game/nanoforge.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "workspace",
+ packages: ["apps/*"],
+});
diff --git a/e2e/game/package.json b/e2e/game/package.json
index ec977fd5..16460ace 100644
--- a/e2e/game/package.json
+++ b/e2e/game/package.json
@@ -9,22 +9,15 @@
"start": "nf start"
},
"devDependencies": {
- "@nanoforge-dev/asset-manager": "workspace:*",
+ "@nanoforge-dev/asset": "workspace:*",
"@nanoforge-dev/cli": "latest",
"@nanoforge-dev/common": "workspace:*",
"@nanoforge-dev/core": "workspace:*",
- "@nanoforge-dev/ecs-client": "workspace:*",
- "@nanoforge-dev/ecs-server": "workspace:*",
- "@nanoforge-dev/graphics-2d": "workspace:*",
- "@nanoforge-dev/input": "workspace:*",
- "@nanoforge-dev/music": "workspace:*",
- "@nanoforge-dev/network-client": "workspace:*",
- "@nanoforge-dev/network-server": "workspace:*",
- "@nanoforge-dev/sound": "workspace:*",
+ "@nanoforge-dev/env": "workspace:*",
"typescript": "catalog:core"
},
"private": true,
"engines": {
- "node": "25"
+ "node": "26"
}
}
diff --git a/e2e/game/server/ticker.library.ts b/e2e/game/server/ticker.library.ts
deleted file mode 100644
index de5729a3..00000000
--- a/e2e/game/server/ticker.library.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { BaseNetworkLibrary, type Context } from "@nanoforge-dev/common";
-
-/**
- * A runner library that stops the game loop after a fixed number of ticks.
- * Reuses BaseNetworkLibrary (no abstract methods) so it can be registered
- * as a standard library. Exposes `done` which resolves when the game stops.
- */
-export class TickerLibrary extends BaseNetworkLibrary {
- private _remaining: number;
- private _resolve!: () => void;
-
- public readonly done: Promise = new Promise((resolve) => {
- this._resolve = resolve;
- });
-
- constructor(ticks: number) {
- super();
- this._remaining = ticks;
- }
-
- get __name(): string {
- return "TickerLibrary";
- }
-
- async __run(ctx: Context): Promise {
- this._remaining--;
- if (this._remaining <= 0) {
- ctx.app.setIsRunning(false);
- this._resolve();
- }
- }
-}
diff --git a/e2e/run-server.mjs b/e2e/run-server.mjs
index 5a183bb4..0324f3c8 100644
--- a/e2e/run-server.mjs
+++ b/e2e/run-server.mjs
@@ -4,7 +4,7 @@ import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROJECT_DIR = resolve(__dirname, "./game");
-const wasmPath = resolve(PROJECT_DIR, "../../packages/ecs-server/dist/assets/libecs.wasm");
+const wasmPath = resolve(PROJECT_DIR, "../../modules/ecs/lib/node/libecs.wasm");
const timeout = setTimeout(() => {
console.error("[E2E] Server timed out after 10s — aborting");
diff --git a/example/pong-network/.gitignore b/example/pong-network/.gitignore
index ba254b9c..e69de29b 100644
--- a/example/pong-network/.gitignore
+++ b/example/pong-network/.gitignore
@@ -1,229 +0,0 @@
-### 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.*
-
-### Private
-
-# Nanoforge
-.nanoforge/
diff --git a/packages/asset-manager/.prettierignore b/example/pong-network/.prettierignore
similarity index 100%
rename from packages/asset-manager/.prettierignore
rename to example/pong-network/.prettierignore
diff --git a/example/pong-network/README.md b/example/pong-network/README.md
deleted file mode 100644
index 776d79c3..00000000
--- a/example/pong-network/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# pong-network
diff --git a/docs.json b/example/pong-network/apps/client/assets/.gitkeep
similarity index 100%
rename from docs.json
rename to example/pong-network/apps/client/assets/.gitkeep
diff --git a/example/pong-network/apps/client/nanoforge.config.ts b/example/pong-network/apps/client/nanoforge.config.ts
new file mode 100644
index 00000000..ab6e23e1
--- /dev/null
+++ b/example/pong-network/apps/client/nanoforge.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "client",
+
+ libs: ["../../libs/test-lib"], // ? or ["test-lib"] (with a name set in config or in pkg.json) or ["libs/test-lib"]
+});
diff --git a/example/pong-network/apps/client/package.json b/example/pong-network/apps/client/package.json
new file mode 100644
index 00000000..6a4344a1
--- /dev/null
+++ b/example/pong-network/apps/client/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "pong-network-client",
+ "version": "0.0.0",
+ "description": "",
+ "author": "",
+ "scripts": {
+ "build": "nf build",
+ "start": "nf start",
+ "lint": "prettier --check . && eslint --format=pretty \"{client,server}/**/*.ts\"",
+ "format": "prettier --write . && eslint --fix --format=pretty \"{client,server}/**/*.ts\""
+ },
+ "devDependencies": {
+ "@nanoforge-dev/asset": "workspace:*",
+ "@nanoforge-dev/common": "workspace:*",
+ "@nanoforge-dev/core": "workspace:*",
+ "@nanoforge-dev/env": "workspace:*",
+ "@nanoforge-dev/ecs": "workspace:*",
+ "@nanoforge-dev/network": "workspace:*",
+ "@nanoforge-dev/graphics-2d": "workspace:*",
+ "@nanoforge-dev/input": "workspace:*",
+ "@nanoforge-dev/utils-eslint-config": "workspace:*",
+ "@nanoforge-dev/utils-prettier-config": "workspace:*",
+ "@trivago/prettier-plugin-sort-imports": "catalog:lint",
+ "eslint": "catalog:lint",
+ "lint-staged": "catalog:ci",
+ "prettier": "catalog:lint",
+ "typescript": "catalog:core"
+ },
+ "private": true,
+ "lint-staged": {
+ "**": [
+ "prettier --ignore-unknown --write"
+ ],
+ "{client,server}/**/*.ts": [
+ "eslint --fix"
+ ]
+ }
+}
diff --git a/example/pong-network/client/components.ts b/example/pong-network/apps/client/src/components/components.ts
similarity index 97%
rename from example/pong-network/client/components.ts
rename to example/pong-network/apps/client/src/components/components.ts
index f71b6536..616891b0 100644
--- a/example/pong-network/client/components.ts
+++ b/example/pong-network/apps/client/src/components/components.ts
@@ -1,7 +1,7 @@
import type { Circle, Rect } from "@nanoforge-dev/graphics-2d";
import type { InputEnum } from "@nanoforge-dev/input";
-import { layer } from "./main";
+import { layer } from "../main";
export class NetworkId {
name = "NetworkId";
diff --git a/example/pong-network/client/main.ts b/example/pong-network/apps/client/src/main.ts
similarity index 69%
rename from example/pong-network/client/main.ts
rename to example/pong-network/apps/client/src/main.ts
index 0eb0a81e..bfebd2d3 100644
--- a/example/pong-network/client/main.ts
+++ b/example/pong-network/apps/client/src/main.ts
@@ -1,37 +1,36 @@
-import { AssetManagerLibrary } from "@nanoforge-dev/asset-manager";
-import { type IRunOptions } from "@nanoforge-dev/common";
+import type { ClientRunOptions } from "@nanoforge-dev/common";
import { NanoforgeFactory } from "@nanoforge-dev/core";
-import { ECSClientLibrary } from "@nanoforge-dev/ecs-client";
+import { EcsLibrary } from "@nanoforge-dev/ecs/client";
import { Circle, Graphics2DLibrary, Layer, Rect } from "@nanoforge-dev/graphics-2d";
-import { InputEnum } from "@nanoforge-dev/input";
-import { InputLibrary } from "@nanoforge-dev/input";
-import { NetworkClientLibrary } from "@nanoforge-dev/network-client";
-
-import { CircleComponent, Controller, Position, RectangleComponent, Velocity } from "./components";
-import { controlPlayer, draw, move, packetHandler } from "./systems";
-
-export const app = NanoforgeFactory.createClient({
- tickRate: 60,
-});
+import { InputEnum, InputLibrary } from "@nanoforge-dev/input";
+import { NetworkClientLibrary } from "@nanoforge-dev/network/client";
+
+import {
+ CircleComponent,
+ Controller,
+ Position,
+ RectangleComponent,
+ Velocity,
+} from "./components/components";
+import { controlPlayer, draw, move, packetHandler } from "./systems/systems";
export const layer = new Layer();
-export const main = async (options: IRunOptions) => {
+export const main = async (options: ClientRunOptions): Promise => {
+ const app = NanoforgeFactory.createClient({ tickRate: 60 });
const graphics = new Graphics2DLibrary();
- const ecsLibrary = new ECSClientLibrary();
+ const ecs = new EcsLibrary();
const network = new NetworkClientLibrary();
- const assetManager = new AssetManagerLibrary();
const input = new InputLibrary();
- app.useGraphics(graphics);
- app.useComponentSystem(ecsLibrary);
- app.useNetwork(network);
- app.useAssetManager(assetManager);
- app.useInput(input);
+ app.use(graphics);
+ app.use(ecs);
+ app.use(network);
+ app.use(input);
await app.init(options);
- const registry = ecsLibrary.registry;
+ const registry = ecs.registry;
graphics.stage.add(layer);
@@ -90,7 +89,7 @@ export const main = async (options: IRunOptions) => {
return new Promise((resolve) => {
const check = () => {
- if (network.tcp.isConnected()) {
+ if (network.tcp?.isConnected()) {
resolve();
} else {
setTimeout(check, 50);
@@ -101,6 +100,7 @@ export const main = async (options: IRunOptions) => {
}
await waitForConnection();
- network.tcp.sendData(new TextEncoder().encode(JSON.stringify({ type: "play" })));
- app.run();
+ network.tcp?.sendData(new TextEncoder().encode(JSON.stringify({ type: "play" })));
+
+ await app.run();
};
diff --git a/example/pong-network/apps/client/src/scenes/.gitkeep b/example/pong-network/apps/client/src/scenes/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/example/pong-network/client/systems.ts b/example/pong-network/apps/client/src/systems/systems.ts
similarity index 88%
rename from example/pong-network/client/systems.ts
rename to example/pong-network/apps/client/src/systems/systems.ts
index 37d5dc18..d3b41fad 100644
--- a/example/pong-network/client/systems.ts
+++ b/example/pong-network/apps/client/src/systems/systems.ts
@@ -1,7 +1,5 @@
import { type Context } from "@nanoforge-dev/common";
-import { type Registry } from "@nanoforge-dev/ecs-client";
-import { type InputLibrary } from "@nanoforge-dev/input";
-import { type NetworkClientLibrary } from "@nanoforge-dev/network-client";
+import { type Registry } from "@nanoforge-dev/ecs/client";
import {
CircleComponent,
@@ -10,7 +8,7 @@ import {
Position,
RectangleComponent,
Velocity,
-} from "./components";
+} from "../components/components";
export function move(registry: Registry, ctx: Context) {
const entities = registry.getZipper([Position, Velocity]);
@@ -23,8 +21,8 @@ export function move(registry: Registry, ctx: Context) {
export const controlPlayer = (registry: Registry, ctx: Context) => {
const entities = registry.getZipper([Controller]);
- const input = ctx.libs.getInput();
- const network = ctx.libs.getNetwork();
+ const input = ctx.input;
+ const network = ctx.network;
entities.forEach(({ Controller }) => {
const upPressed = input.isKeyPressed(Controller.up);
@@ -62,7 +60,7 @@ export function draw(registry: Registry) {
}
export function packetHandler(registry: Registry, ctx: Context) {
- const network = ctx.libs.getNetwork();
+ const network = ctx.network;
const jsonPackets = network.tcp.getReceivedPackets().map((packet) => {
return JSON.parse(new TextDecoder().decode(packet));
});
diff --git a/example/pong-network/apps/client/tsconfig.json b/example/pong-network/apps/client/tsconfig.json
new file mode 100644
index 00000000..f90a806b
--- /dev/null
+++ b/example/pong-network/apps/client/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ "extends": "../../tsconfig.json",
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist", ".nanoforge"]
+}
diff --git a/example/pong-network/apps/server/assets/.gitkeep b/example/pong-network/apps/server/assets/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/example/pong-network/apps/server/nanoforge.config.ts b/example/pong-network/apps/server/nanoforge.config.ts
new file mode 100644
index 00000000..bc5491cb
--- /dev/null
+++ b/example/pong-network/apps/server/nanoforge.config.ts
@@ -0,0 +1,5 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "server",
+});
diff --git a/example/pong-network/apps/server/package.json b/example/pong-network/apps/server/package.json
new file mode 100644
index 00000000..e163e437
--- /dev/null
+++ b/example/pong-network/apps/server/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "pong-network-server",
+ "version": "0.0.0",
+ "description": "",
+ "author": "",
+ "scripts": {
+ "build": "nf build",
+ "start": "nf start",
+ "lint": "prettier --check . && eslint --format=pretty \"{client,server}/**/*.ts\"",
+ "format": "prettier --write . && eslint --fix --format=pretty \"{client,server}/**/*.ts\""
+ },
+ "devDependencies": {
+ "@nanoforge-dev/asset": "workspace:*",
+ "@nanoforge-dev/common": "workspace:*",
+ "@nanoforge-dev/core": "workspace:*",
+ "@nanoforge-dev/env": "workspace:*",
+ "@nanoforge-dev/ecs": "workspace:*",
+ "@nanoforge-dev/network": "workspace:*",
+ "@nanoforge-dev/utils-eslint-config": "workspace:*",
+ "@nanoforge-dev/utils-prettier-config": "workspace:*",
+ "@trivago/prettier-plugin-sort-imports": "catalog:lint",
+ "eslint": "catalog:lint",
+ "lint-staged": "catalog:ci",
+ "prettier": "catalog:lint",
+ "typescript": "catalog:core"
+ },
+ "private": true,
+ "lint-staged": {
+ "**": [
+ "prettier --ignore-unknown --write"
+ ],
+ "{client,server}/**/*.ts": [
+ "eslint --fix"
+ ]
+ }
+}
diff --git a/example/pong-network/server/components.ts b/example/pong-network/apps/server/src/components/components.ts
similarity index 100%
rename from example/pong-network/server/components.ts
rename to example/pong-network/apps/server/src/components/components.ts
diff --git a/example/pong-network/apps/server/src/main.ts b/example/pong-network/apps/server/src/main.ts
new file mode 100644
index 00000000..f90b95a0
--- /dev/null
+++ b/example/pong-network/apps/server/src/main.ts
@@ -0,0 +1,38 @@
+import type { RunOptions } from "@nanoforge-dev/common";
+import { NanoforgeFactory } from "@nanoforge-dev/core";
+import { EcsLibrary } from "@nanoforge-dev/ecs/server";
+import { NetworkServerLibrary } from "@nanoforge-dev/network/server";
+
+import { Position, Velocity } from "./components/components";
+import { bounce, move, packetHandler } from "./systems/systems";
+
+export const main = async (options: RunOptions): Promise => {
+ const app = NanoforgeFactory.createServer({ tickRate: 60 });
+ const ecs = new EcsLibrary();
+ const network = new NetworkServerLibrary();
+
+ app.use(ecs);
+ app.use(network);
+
+ await app.init(options);
+
+ const registry = ecs.registry;
+
+ const ball = registry.spawnEntity();
+ registry.addComponent(ball, new Position(960, 540));
+ registry.addComponent(ball, new Velocity(0, 0));
+
+ const paddle1 = registry.spawnEntity();
+ registry.addComponent(paddle1, new Position(20, 390));
+ registry.addComponent(paddle1, new Velocity(0, 0));
+
+ const paddle2 = registry.spawnEntity();
+ registry.addComponent(paddle2, new Position(1850, 390));
+ registry.addComponent(paddle2, new Velocity(0, 0));
+
+ registry.addSystem(packetHandler);
+ registry.addSystem(bounce);
+ registry.addSystem(move);
+
+ await app.run();
+};
diff --git a/example/pong-network/apps/server/src/scenes/.gitkeep b/example/pong-network/apps/server/src/scenes/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/example/pong-network/server/systems.ts b/example/pong-network/apps/server/src/systems/systems.ts
similarity index 91%
rename from example/pong-network/server/systems.ts
rename to example/pong-network/apps/server/src/systems/systems.ts
index 68ac92a0..c119dca8 100644
--- a/example/pong-network/server/systems.ts
+++ b/example/pong-network/apps/server/src/systems/systems.ts
@@ -1,8 +1,8 @@
import { type Context } from "@nanoforge-dev/common";
-import { type Registry } from "@nanoforge-dev/ecs-server";
-import { type NetworkServerLibrary } from "@nanoforge-dev/network-server";
+import { type Registry } from "@nanoforge-dev/ecs/server";
+import { type NetworkServerContextApi } from "@nanoforge-dev/network/server";
-import { Position, Velocity } from "./components";
+import { Position, Velocity } from "../components/components";
let cli1 = -1;
let cli2 = -1;
@@ -19,7 +19,7 @@ export function move(registry: Registry, ctx: Context) {
});
}
-function sendMoveAll(id: number, vel: Velocity, pos: Position, network: NetworkServerLibrary) {
+function sendMoveAll(id: number, vel: Velocity, pos: Position, network: NetworkServerContextApi) {
network.tcp.sendToEverybody(
new TextEncoder().encode(
JSON.stringify({
@@ -32,7 +32,7 @@ function sendMoveAll(id: number, vel: Velocity, pos: Position, network: NetworkS
);
}
-function connectNewClient(newCli: number, network: NetworkServerLibrary, zip: any) {
+function connectNewClient(newCli: number, network: NetworkServerContextApi, zip: any) {
network.tcp.sendToClient(
newCli,
new TextEncoder().encode(JSON.stringify({ type: "assignId", assigned: "ball", id: 0 })),
@@ -50,7 +50,12 @@ function connectNewClient(newCli: number, network: NetworkServerLibrary, zip: an
sendMoveAll(2, zip[2].Velocity, zip[2].Position, network);
}
-function handleClientInput(clientId: number, key: string, network: NetworkServerLibrary, zip: any) {
+function handleClientInput(
+ clientId: number,
+ key: string,
+ network: NetworkServerContextApi,
+ zip: any,
+) {
let id;
if (clientId === cli1) {
@@ -84,7 +89,7 @@ function handleClientInput(clientId: number, key: string, network: NetworkServer
export function packetHandler(registry: Registry, ctx: Context) {
const zip = registry.getZipper([Position, Velocity]);
- const network = ctx.libs.getNetwork();
+ const network = ctx.network;
if (network.tcp.getConnectedClients().indexOf(cli1) == -1) cli1 = -1;
if (network.tcp.getConnectedClients().indexOf(cli2) == -1) cli2 = -1;
@@ -110,7 +115,7 @@ export function packetHandler(registry: Registry, ctx: Context) {
});
}
-function checkOutOfTerrain(id: number, paddle: any, network: NetworkServerLibrary) {
+function checkOutOfTerrain(id: number, paddle: any, network: NetworkServerContextApi) {
if (paddle.Position.y < 0) {
paddle.Position.y = 0;
paddle.Velocity.y = 0;
@@ -124,7 +129,7 @@ function checkOutOfTerrain(id: number, paddle: any, network: NetworkServerLibrar
}
export const bounce = (registry: Registry, ctx: Context) => {
- const network = ctx.libs.getNetwork();
+ const network = ctx.network;
const zip = registry.getZipper([Position, Velocity]);
checkOutOfTerrain(1, zip[1], network);
checkOutOfTerrain(2, zip[2], network);
diff --git a/example/pong-network/apps/server/tsconfig.json b/example/pong-network/apps/server/tsconfig.json
new file mode 100644
index 00000000..f90a806b
--- /dev/null
+++ b/example/pong-network/apps/server/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ "extends": "../../tsconfig.json",
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist", ".nanoforge"]
+}
diff --git a/example/pong-network/index.d.ts b/example/pong-network/index.d.ts
new file mode 100644
index 00000000..3e7f8b23
--- /dev/null
+++ b/example/pong-network/index.d.ts
@@ -0,0 +1,65 @@
+// Ambient stub for the `nf` CLI's own `nanoforge/config` resolution. Keep this
+// shape in sync with @nanoforge-dev/env's real types; revisit/remove once
+// the CLI's actual resolution contract for `nanoforge.config.ts` is confirmed.
+declare module "nanoforge/config" {
+ type ContainLibConfig = {
+ libs?: string[];
+ };
+
+ type BuildableConfig = {
+ entryFile?: string;
+ out?: {
+ dir?: string;
+ mainFile?: string;
+ };
+ };
+
+ type SourceableConfig = {
+ dir?: {
+ assets?: string;
+
+ packages?: string;
+
+ // Used by the editor only
+ components?: string;
+
+ // Used by the editor only
+ systems?: string;
+
+ // Used by the editor only
+ scenes?: string;
+ };
+ };
+
+ type BaseConfig = {
+ type: Type;
+ };
+
+ type WorkspaceConfig = BaseConfig<"workspace"> & {
+ packages?: string[];
+ };
+ type LibConfig = BaseConfig<"lib"> & {
+ dir?: {
+ assets?: string;
+ shared?: string;
+
+ // Used by the editor only
+ // Should be inside shared dir
+ components?: string;
+
+ // Used by the editor only
+ // Should be inside shared dir
+ systems?: string;
+
+ // Used by the editor only
+ // Should be inside shared dir
+ scenes?: string;
+ };
+ };
+ type ClientConfig = BaseConfig<"client"> & SourceableConfig & BuildableConfig & ContainLibConfig;
+ type ServerConfig = BaseConfig<"server"> & SourceableConfig & BuildableConfig & ContainLibConfig;
+
+ export type Config = WorkspaceConfig | LibConfig | ClientConfig | ServerConfig;
+
+ export const defineConfig: (config: Config) => any;
+}
diff --git a/example/pong-network/libs/test-lib/nanoforge.config.ts b/example/pong-network/libs/test-lib/nanoforge.config.ts
new file mode 100644
index 00000000..cc139b25
--- /dev/null
+++ b/example/pong-network/libs/test-lib/nanoforge.config.ts
@@ -0,0 +1,5 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "lib",
+});
diff --git a/example/pong-network/nanoforge.config.json b/example/pong-network/nanoforge.config.json
deleted file mode 100644
index 1e4becd5..00000000
--- a/example/pong-network/nanoforge.config.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "client": {
- "build": {
- "entryFile": "client/main.ts",
- "outDir": ".nanoforge/client"
- },
- "runtime": {
- "dir": ".nanoforge/client"
- }
- },
- "server": {
- "enable": true,
- "build": {
- "entryFile": "server/main.ts",
- "outDir": ".nanoforge/server"
- },
- "runtime": {
- "dir": ".nanoforge/server"
- }
- }
-}
diff --git a/example/pong-network/nanoforge.config.ts b/example/pong-network/nanoforge.config.ts
new file mode 100644
index 00000000..cc75b72c
--- /dev/null
+++ b/example/pong-network/nanoforge.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from "nanoforge/config";
+
+export default defineConfig({
+ type: "workspace",
+ packages: ["apps/*", "libs/*"],
+});
diff --git a/example/pong-network/package.json b/example/pong-network/package.json
index 10e9981a..79cc5fc3 100644
--- a/example/pong-network/package.json
+++ b/example/pong-network/package.json
@@ -4,32 +4,23 @@
"description": "",
"author": "",
"scripts": {
- "build": "mkdir -p .nanoforge && nf build",
"start": "nf start",
"lint": "prettier --check . && eslint --format=pretty \"{client,server}/**/*.ts\"",
- "format": "prettier --write . && eslint --fix --format=pretty \"{client,server}/**/*.ts\""
+ "format": "prettier --write . && eslint --fix --format=pretty \"{client,server}/**/*.ts\"",
+ "test:unit": "vitest run --config ../../vitest.config.ts"
},
"devDependencies": {
- "@nanoforge-dev/asset-manager": "workspace:*",
+ "@nanoforge-dev/asset": "workspace:*",
"@nanoforge-dev/cli": "latest",
"@nanoforge-dev/common": "workspace:*",
- "@nanoforge-dev/config": "workspace:*",
"@nanoforge-dev/core": "workspace:*",
- "@nanoforge-dev/ecs-client": "workspace:*",
- "@nanoforge-dev/ecs-server": "workspace:*",
- "@nanoforge-dev/graphics-2d": "workspace:*",
- "@nanoforge-dev/input": "workspace:*",
- "@nanoforge-dev/network-client": "workspace:*",
- "@nanoforge-dev/network-server": "workspace:*",
- "@nanoforge-dev/music": "workspace:*",
- "@nanoforge-dev/sound": "workspace:*",
- "@nanoforge-dev/utils-eslint-config": "workspace:*",
- "@nanoforge-dev/utils-prettier-config": "workspace:*",
+ "@nanoforge-dev/env": "workspace:*",
"@trivago/prettier-plugin-sort-imports": "catalog:lint",
"eslint": "catalog:lint",
"lint-staged": "catalog:ci",
"prettier": "catalog:lint",
- "typescript": "catalog:core"
+ "typescript": "catalog:core",
+ "vitest": "catalog:test"
},
"private": true,
"lint-staged": {
diff --git a/example/pong-network/server/main.ts b/example/pong-network/server/main.ts
deleted file mode 100644
index c30b6475..00000000
--- a/example/pong-network/server/main.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { AssetManagerLibrary } from "@nanoforge-dev/asset-manager";
-import { type IRunOptions } from "@nanoforge-dev/common";
-import { NanoforgeFactory } from "@nanoforge-dev/core";
-import { ECSServerLibrary } from "@nanoforge-dev/ecs-server";
-import { NetworkServerLibrary } from "@nanoforge-dev/network-server";
-
-import { Position, Velocity } from "./components";
-import { bounce, move, packetHandler } from "./systems";
-
-export const app = NanoforgeFactory.createServer({
- tickRate: 60,
-});
-
-export const main = async (options: IRunOptions) => {
- const ecsLibrary = new ECSServerLibrary();
- const network = new NetworkServerLibrary();
- const assetManager = new AssetManagerLibrary();
-
- app.useComponentSystem(ecsLibrary);
- app.useNetwork(network);
- app.useAssetManager(assetManager);
-
- await app.init(options);
-
- const registry = ecsLibrary.registry;
-
- const ball = registry.spawnEntity();
- registry.addComponent(ball, new Position(960, 540));
- registry.addComponent(ball, new Velocity(0, 0));
-
- const paddle1 = registry.spawnEntity();
- registry.addComponent(paddle1, new Position(20, 390));
- registry.addComponent(paddle1, new Velocity(0, 0));
-
- const paddle2 = registry.spawnEntity();
- registry.addComponent(paddle2, new Position(1850, 390));
- registry.addComponent(paddle2, new Velocity(0, 0));
-
- registry.addSystem(packetHandler);
- registry.addSystem(bounce);
- registry.addSystem(move);
-
- app.run();
-};
diff --git a/example/pong-network/test/pong-network.spec.ts b/example/pong-network/test/pong-network.spec.ts
new file mode 100644
index 00000000..6f06d624
--- /dev/null
+++ b/example/pong-network/test/pong-network.spec.ts
@@ -0,0 +1,15 @@
+import { describe, expect, it } from "vitest";
+
+import { main as clientMain } from "../apps/client/src/main";
+import { main as serverMain } from "../apps/server/src/main";
+
+describe("pong-network plumbing skeleton", () => {
+ it("server app runs its tick loop and stops cleanly", async () => {
+ await expect(serverMain({ files: new Map(), env: {} })).resolves.toBeUndefined();
+ });
+
+ it("client app runs its tick loop, writes vars, and stops cleanly", async () => {
+ const container = {} as unknown as HTMLDivElement;
+ await expect(clientMain({ files: new Map(), env: {}, container })).resolves.toBeUndefined();
+ });
+});
diff --git a/example/pong-network/tsconfig.json b/example/pong-network/tsconfig.json
index 2e28238a..076ca025 100644
--- a/example/pong-network/tsconfig.json
+++ b/example/pong-network/tsconfig.json
@@ -1,42 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
- // Mapped from https://www.typescriptlang.org/tsconfig
- "compilerOptions": {
- // Type Checking
- "allowUnreachableCode": false,
- "allowUnusedLabels": false,
- "exactOptionalPropertyTypes": true,
- "noFallthroughCasesInSwitch": true,
- "noImplicitOverride": true,
- "noImplicitReturns": true,
- "noPropertyAccessFromIndexSignature": false,
- "noUncheckedIndexedAccess": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "strict": true,
-
- // Modules
- "allowArbitraryExtensions": false,
- "allowImportingTsExtensions": false,
- "module": "ESNext",
- "moduleResolution": "Bundler",
- "resolveJsonModule": true,
- "resolvePackageJsonExports": true,
- "resolvePackageJsonImports": true,
-
- // Interop Constraints
- "allowSyntheticDefaultImports": true,
- "esModuleInterop": false,
- "forceConsistentCasingInFileNames": true,
- "isolatedModules": true,
-
- // Language and Environment
- "rootDir": ".",
- "lib": ["ESNext", "esnext.disposable", "DOM"],
- "target": "ESNext",
- "experimentalDecorators": true,
- "useDefineForClassFields": true
- },
- "include": ["client/**/*.ts", "server/**/*.ts"],
- "exclude": ["node_modules", ".nanoforge"]
+ "extends": "../../tsconfig.json",
+ "include": ["apps/*/src/**/*.ts", "test/**/*.ts"],
+ "exclude": ["node_modules", "**/dist", "**/.nanoforge"]
}
diff --git a/example/pong-network/tsconfig.spec.json b/example/pong-network/tsconfig.spec.json
new file mode 100644
index 00000000..bbad15dc
--- /dev/null
+++ b/example/pong-network/tsconfig.spec.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noEmit": true,
+ "skipLibCheck": true
+ },
+ "include": ["apps/*/src/**/*.ts", "test/**/*.spec.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/ecs-lib/.clang-format b/modules/ecs/.clang-format
similarity index 100%
rename from packages/ecs-lib/.clang-format
rename to modules/ecs/.clang-format
diff --git a/packages/config/.cliff-jumperrc.json b/modules/ecs/.cliff-jumperrc.json
similarity index 78%
rename from packages/config/.cliff-jumperrc.json
rename to modules/ecs/.cliff-jumperrc.json
index ce77165a..c3a0e115 100644
--- a/packages/config/.cliff-jumperrc.json
+++ b/modules/ecs/.cliff-jumperrc.json
@@ -1,8 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
- "name": "config",
+ "name": "ecs",
"org": "nanoforge-dev",
- "packagePath": "packages/config",
+ "packagePath": "modules/ecs",
"tagTemplate": "{{new-version}}",
"identifierBase": false
}
diff --git a/packages/ecs-server/.gitignore b/modules/ecs/.gitignore
similarity index 98%
rename from packages/ecs-server/.gitignore
rename to modules/ecs/.gitignore
index ec337acd..c1eebfc6 100644
--- a/packages/ecs-server/.gitignore
+++ b/modules/ecs/.gitignore
@@ -267,6 +267,10 @@ dist
src/**/*.js
src/**/*.d.ts
+build/**/*.js
+build/**/*.wasm
+build/libecs-tmp.d.ts
+
# pubilc directory
lib/
compile_commands.json
diff --git a/packages/ecs-server/.prettierignore b/modules/ecs/.prettierignore
similarity index 95%
rename from packages/ecs-server/.prettierignore
rename to modules/ecs/.prettierignore
index 711c4e5a..1efdad3f 100644
--- a/packages/ecs-server/.prettierignore
+++ b/modules/ecs/.prettierignore
@@ -10,4 +10,4 @@ dist/
coverage/
CHANGELOG.md
-lib/
+build/
diff --git a/modules/ecs/CHANGELOG.md b/modules/ecs/CHANGELOG.md
new file mode 100644
index 00000000..6361e43e
--- /dev/null
+++ b/modules/ecs/CHANGELOG.md
@@ -0,0 +1,3 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
diff --git a/packages/asset-manager/LICENSE b/modules/ecs/LICENSE
similarity index 97%
rename from packages/asset-manager/LICENSE
rename to modules/ecs/LICENSE
index 62c6400b..46724861 100644
--- a/packages/asset-manager/LICENSE
+++ b/modules/ecs/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright © 2025 NanoForge
+Copyright © 2026 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
diff --git a/modules/ecs/Makefile b/modules/ecs/Makefile
new file mode 100644
index 00000000..b4300867
--- /dev/null
+++ b/modules/ecs/Makefile
@@ -0,0 +1,61 @@
+SRC = wasm/SparseArray.cpp\
+ wasm/Entity.cpp\
+ wasm/Utils.cpp\
+ wasm/Registry.cpp
+
+NAME := libecs
+
+CFLAGS = -std=c++20
+LDFLAGS = -O3 --no-entry --bind -sNO_DISABLE_EXCEPTION_CATCHING -sEXPORT_EXCEPTION_HANDLING_HELPERS
+
+CC = em++
+
+OBJ = $(SRC:.cpp=.o)
+
+BASE_DIR = build
+WEB_DIR = lib/web
+NODE_DIR = lib/node
+
+%.o: %.cpp
+ $(CC) -c $< -o $@ $(CFLAGS)
+
+all: base web node
+
+base: LDFLAGS += -s MODULARIZE=1
+base: $(OBJ)
+ $(CC) $(OBJ) $(LDFLAGS) --emit-tsd $(NAME)-tmp.d.ts -o $(BASE_DIR)/$(NAME).js
+ cat $(BASE_DIR)/pre.ts > $(BASE_DIR)/$(NAME).d.ts
+ cat $(BASE_DIR)/libecs-tmp.d.ts >> $(BASE_DIR)/$(NAME).d.ts
+ $(RM) $(BASE_DIR)/$(NAME)-tmp.d.ts $(BASE_DIR)/$(NAME).js $(BASE_DIR)/$(NAME).wasm
+
+web: LDFLAGS += -s MODULARIZE=1 -s EXPORT_ES6=1 -s ENVIRONMENT=web
+web: $(OBJ)
+ @mkdir -p $(WEB_DIR)
+ $(CC) $(OBJ) $(LDFLAGS) -o $(WEB_DIR)/$(NAME).js
+ cp $(BASE_DIR)/$(NAME).d.ts $(WEB_DIR)/$(NAME).d.ts
+
+node: LDFLAGS += -s MODULARIZE=1 -s EXPORT_ES6=1 -s ENVIRONMENT=node
+node: $(OBJ)
+ @mkdir -p $(NODE_DIR)
+ $(CC) $(OBJ) $(LDFLAGS) -o $(NODE_DIR)/$(NAME).js
+ cp $(BASE_DIR)/$(NAME).d.ts $(NODE_DIR)/$(NAME).d.ts
+
+tests: LDFLAGS += -s MODULARIZE=1 -s EXPORT_ES6=1
+tests: $(OBJ)
+ @mkdir -p $(WEB_DIR) $(NODE_DIR)
+ $(CC) $(OBJ) $(LDFLAGS) -o $(WEB_DIR)/$(NAME).js
+ cp $(BASE_DIR)/$(NAME).d.ts $(WEB_DIR)/$(NAME).d.ts
+ sed -i 's/@nanoforge-dev\/common-legacy/@nanoforge-dev\/common/' $(WEB_DIR)/$(NAME).d.ts
+ cp $(WEB_DIR)/$(NAME).js $(NODE_DIR)/$(NAME).js
+ cp $(WEB_DIR)/$(NAME).wasm $(NODE_DIR)/$(NAME).wasm
+ cp $(WEB_DIR)/$(NAME).d.ts $(NODE_DIR)/$(NAME).d.ts
+
+clean:
+ $(RM) $(OBJ)
+ $(RM) -r lib
+
+fclean: clean
+
+re: fclean all
+
+.PHONY: re fclean all clean tests web node
diff --git a/modules/ecs/README.md b/modules/ecs/README.md
new file mode 100644
index 00000000..d92b47e1
--- /dev/null
+++ b/modules/ecs/README.md
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## About
+
+`@nanoforge-dev/ecs` is NanoForge's built-in entity-component-system library, backed by a WebAssembly registry (compiled from C++). It ships two subpath entry points — `@nanoforge-dev/ecs/client` and `@nanoforge-dev/ecs/server` — each loading its own WASM build (browser-targeted vs Node-targeted) behind the same `EcsLibrary` API, so a client bundle never pulls in the server build or vice versa.
+
+## Installation
+
+**Node.js 26 or newer is required.**
+
+```sh
+npm install @nanoforge-dev/ecs
+yarn add @nanoforge-dev/ecs
+pnpm add @nanoforge-dev/ecs
+bun add @nanoforge-dev/ecs
+```
+
+## Example usage
+
+Register `EcsLibrary` from the entry point matching your app:
+
+```ts
+// Client app
+import { EcsLibrary } from "@nanoforge-dev/ecs/client";
+
+app.use(new EcsLibrary());
+```
+
+```ts
+// Server app
+import { EcsLibrary } from "@nanoforge-dev/ecs/server";
+
+app.use(new EcsLibrary());
+```
+
+Once registered, `Context.ecs.registry` gives every other library access to the entity/component registry:
+
+```ts
+public override async __run(ctx: Context): Promise {
+ const entity = ctx.ecs.registry.spawnEntity();
+ ctx.ecs.registry.addComponent(entity, new Position(0, 0));
+}
+```
+
+Register systems to run on every tick:
+
+```ts
+ctx.ecs.registry.addSystem((registry) => {
+ for (const { __RESERVED_entityId, Position } of registry.getZipper([
+ { name: "__RESERVED_entityId" },
+ { name: "Position" },
+ ])) {
+ // Move things around.
+ }
+});
+```
+
+When registered alongside `@nanoforge-dev/editor`'s `EditorLibrary`, `EcsLibrary` also listens for a `"hot-reload"` command from the editor and applies incoming components via `registry.addComponent` — useful for live-editing entity state from an external editor host.
+
+## 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/modules/ecs
+[npm]: https://www.npmjs.com/package/@nanoforge-dev/ecs
+[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md
diff --git a/packages/ecs-lib/lib/libecs.d.ts b/modules/ecs/build/libecs.d.ts
similarity index 97%
rename from packages/ecs-lib/lib/libecs.d.ts
rename to modules/ecs/build/libecs.d.ts
index 24825128..9fb10755 100644
--- a/packages/ecs-lib/lib/libecs.d.ts
+++ b/modules/ecs/build/libecs.d.ts
@@ -1,4 +1,4 @@
-import { Context } from "@nanoforge-dev/common";
+import type { Context } from "@nanoforge-dev/common";
export type Component = { name: string; [key: string]: any };
diff --git a/packages/ecs-lib/build/pre.ts b/modules/ecs/build/pre.ts
similarity index 70%
rename from packages/ecs-lib/build/pre.ts
rename to modules/ecs/build/pre.ts
index 55a73580..76ce9649 100644
--- a/packages/ecs-lib/build/pre.ts
+++ b/modules/ecs/build/pre.ts
@@ -1,4 +1,4 @@
-import { Context } from "@nanoforge-dev/common";
+import type { Context } from "@nanoforge-dev/common";
export type Component = { name: string; [key: string]: any };
diff --git a/packages/asset-manager/cliff.toml b/modules/ecs/cliff.toml
similarity index 100%
rename from packages/asset-manager/cliff.toml
rename to modules/ecs/cliff.toml
diff --git a/packages/asset-manager/eslint.config.js b/modules/ecs/eslint.config.js
similarity index 100%
rename from packages/asset-manager/eslint.config.js
rename to modules/ecs/eslint.config.js
diff --git a/packages/ecs-client/mint-tsdocs.config.json b/modules/ecs/mint-tsdocs.config.json
similarity index 69%
rename from packages/ecs-client/mint-tsdocs.config.json
rename to modules/ecs/mint-tsdocs.config.json
index 142757d9..3e4e5c30 100644
--- a/packages/ecs-client/mint-tsdocs.config.json
+++ b/modules/ecs/mint-tsdocs.config.json
@@ -1,7 +1,7 @@
{
"$schema": "./node_modules/mint-tsdocs/lib/schemas/config.schema.json",
- "entryPoint": "dist/index.d.ts",
- "outputFolder": "../../docs/references/ecs-client",
+ "entryPoint": "dist/client/index.d.ts",
+ "outputFolder": "../../docs/references/ecs",
"lint": {
"eslint": {
"enabled": true
diff --git a/packages/ecs-client/package.json b/modules/ecs/package.json
similarity index 67%
rename from packages/ecs-client/package.json
rename to modules/ecs/package.json
index a7618a11..dfdd22cc 100644
--- a/packages/ecs-client/package.json
+++ b/modules/ecs/package.json
@@ -1,8 +1,8 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "name": "@nanoforge-dev/ecs-client",
+ "name": "@nanoforge-dev/ecs",
"version": "1.4.2",
- "description": "NanoForge Engine - ECS Client",
+ "description": "NanoForge Engine - ECS",
"keywords": [
"nanoforge",
"game",
@@ -13,27 +13,36 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips "
],
"files": [
- "dist",
- "lib",
- "wasm"
+ "dist"
],
- "main": "./dist/index.cjs",
- "module": "./dist/index.js",
- "types": "./dist/index.d.cts",
+ "main": "./dist/client/index.cjs",
+ "module": "./dist/client/index.js",
+ "types": "./dist/client/index.d.cts",
"exports": {
- ".": {
+ "./client": {
"require": {
- "types": "./dist/index.d.cts",
- "default": "./dist/index.cjs"
+ "types": "./dist/client/index.d.cts",
+ "default": "./dist/client/index.cjs"
},
"import": {
- "types": "./dist/index.d.ts",
- "default": "./dist/index.js"
+ "types": "./dist/client/index.d.ts",
+ "default": "./dist/client/index.js"
+ }
+ },
+ "./server": {
+ "require": {
+ "types": "./dist/server/index.d.cts",
+ "default": "./dist/server/index.cjs"
+ },
+ "import": {
+ "types": "./dist/server/index.d.ts",
+ "default": "./dist/server/index.js"
}
},
"./package.json": "./package.json"
@@ -45,7 +54,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/NanoForge-dev/Engine.git",
- "directory": "packages/ecs-client"
+ "directory": "modules/ecs"
},
"funding": "https://github.com/NanoForge-dev/Engine?sponsor",
"scripts": {
@@ -57,22 +66,18 @@
"format": "prettier --write . && eslint --fix --format=pretty src",
"test:unit": "make fclean && make tests -j 16 && vitest run --config ../../vitest.config.ts && make fclean",
"prepack": "pnpm run build && pnpm run lint",
- "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/ecs-client/*'",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'modules/ecs/*'",
"release": "cliff-jumper",
- "docs": "mint-tsdocs generate"
+ "docs": "pnpm run build:wasm && mint-tsdocs generate"
},
"dependencies": {
- "@nanoforge-dev/common": "workspace:*",
- "@nanoforge-dev/ecs-lib": "workspace:*"
+ "@nanoforge-dev/common": "workspace:*"
},
"devDependencies": {
"@favware/cliff-jumper": "catalog:ci",
- "@nanoforge-dev/asset-manager": "workspace:*",
- "@nanoforge-dev/core": "workspace:*",
"@nanoforge-dev/utils-eslint-config": "workspace:*",
"@nanoforge-dev/utils-prettier-config": "workspace:*",
"@trivago/prettier-plugin-sort-imports": "catalog:lint",
- "@types/node": "catalog:core",
"eslint": "catalog:lint",
"prettier": "catalog:lint",
"tsdown": "catalog:build",
@@ -80,9 +85,9 @@
"unrun": "catalog:build",
"vitest": "catalog:test"
},
- "packageManager": "pnpm@11.5.2",
+ "packageManager": "pnpm@12.0.0",
"engines": {
- "node": "25"
+ "node": "26"
},
"publishConfig": {
"access": "public"
diff --git a/packages/asset-manager/prettier.config.js b/modules/ecs/prettier.config.js
similarity index 100%
rename from packages/asset-manager/prettier.config.js
rename to modules/ecs/prettier.config.js
diff --git a/modules/ecs/src/client/index.ts b/modules/ecs/src/client/index.ts
new file mode 100644
index 00000000..5923f29c
--- /dev/null
+++ b/modules/ecs/src/client/index.ts
@@ -0,0 +1,13 @@
+import { default as Module } from "../../lib/web/libecs";
+import "../shared/context-augmentation";
+import { EcsLibrary as BaseEcsLibrary } from "../shared/ecs-library";
+
+/** Client-side ECS library — loads the browser-targeted compiled WASM module. */
+export class EcsLibrary extends BaseEcsLibrary {
+ constructor() {
+ super(Module);
+ }
+}
+
+export type { EcsContextApi } from "../shared/ecs-context.type";
+export type { Component, Entity, Registry, SparseArray, System } from "../../lib/web/libecs";
diff --git a/modules/ecs/src/server/index.ts b/modules/ecs/src/server/index.ts
new file mode 100644
index 00000000..74a9f026
--- /dev/null
+++ b/modules/ecs/src/server/index.ts
@@ -0,0 +1,13 @@
+import { default as Module } from "../../lib/node/libecs";
+import "../shared/context-augmentation";
+import { EcsLibrary as BaseEcsLibrary } from "../shared/ecs-library";
+
+/** Server-side ECS library — loads the Node-targeted compiled WASM module. */
+export class EcsLibrary extends BaseEcsLibrary {
+ constructor() {
+ super(Module);
+ }
+}
+
+export type { EcsContextApi } from "../shared/ecs-context.type";
+export type { Component, Entity, Registry, SparseArray, System } from "../../lib/node/libecs";
diff --git a/modules/ecs/src/shared/context-augmentation.ts b/modules/ecs/src/shared/context-augmentation.ts
new file mode 100644
index 00000000..34874d3c
--- /dev/null
+++ b/modules/ecs/src/shared/context-augmentation.ts
@@ -0,0 +1,7 @@
+import type { EcsContextApi } from "./ecs-context.type";
+
+declare module "@nanoforge-dev/common" {
+ interface Context {
+ ecs: EcsContextApi;
+ }
+}
diff --git a/modules/ecs/src/shared/ecs-context.type.ts b/modules/ecs/src/shared/ecs-context.type.ts
new file mode 100644
index 00000000..e297e14b
--- /dev/null
+++ b/modules/ecs/src/shared/ecs-context.type.ts
@@ -0,0 +1,7 @@
+import type { Registry } from "../../lib/web/libecs";
+
+/** Public surface of `EcsLibrary`, exposed on `Context.ecs`. */
+export interface EcsContextApi {
+ /** The WASM-backed entity/component registry. */
+ readonly registry: Registry;
+}
diff --git a/modules/ecs/src/shared/ecs-library.ts b/modules/ecs/src/shared/ecs-library.ts
new file mode 100644
index 00000000..5746a3a0
--- /dev/null
+++ b/modules/ecs/src/shared/ecs-library.ts
@@ -0,0 +1,74 @@
+import {
+ type Context,
+ type InitContext,
+ Library,
+ NfNotFound,
+ defineLibraryKey,
+} from "@nanoforge-dev/common";
+
+import type { Entity, MainModule, Registry } from "../../lib/web/libecs";
+import type { EcsContextApi } from "./ecs-context.type";
+
+/** Loads the compiled WASM module — supplied by the client/server entry point. */
+export type LoadEcsModule = (options: { locateFile: () => string }) => Promise;
+
+/**
+ * ECS library backed by the compiled WASM module.
+ *
+ * @remarks
+ * Loads `libecs.wasm` from `InitContext.files` directly (not `ctx.assets` —
+ * `Context` doesn't exist yet during `__init`, same phase-boundary
+ * constraint every library has) and initialises the entity registry.
+ *
+ * If registered alongside `@nanoforge-dev/editor-lib`'s `EditorLibrary`, listens
+ * for a `"hot-reload"` command and applies it via `registry.addComponent`.
+ *
+ * Not exported directly — `@nanoforge-dev/ecs/client` and `.../server` each
+ * subclass this with their own compiled `Module` factory, since the actual
+ * WASM binary differs by target environment (browser vs Node) even though
+ * this class's logic is identical either way.
+ */
+export abstract class EcsLibrary extends Library {
+ readonly key = defineLibraryKey("ecs");
+
+ private _module?: MainModule;
+ private _registry?: Registry;
+
+ protected constructor(private readonly loadModule: LoadEcsModule) {
+ super({ runAfter: ["graphics"] });
+ }
+
+ public override async __init(ctx: InitContext): Promise {
+ const wasmUrl = ctx.files.get("libecs.wasm");
+ if (!wasmUrl) throw new NfNotFound("libecs.wasm", "Asset");
+
+ this._module = await this.loadModule({ locateFile: () => wasmUrl });
+ this._registry = new this._module.Registry();
+
+ if (ctx.editor) {
+ ctx.editor.fromEditor.on("hot-reload", (...args: unknown[]) => {
+ const [entity, component] = args as [Entity, Parameters[1]];
+ this.registry.addComponent(entity, component);
+ });
+ }
+ }
+
+ public override async __run(ctx: Context): Promise {
+ this.registry.runSystems(ctx);
+ }
+
+ public get registry(): Registry {
+ if (!this._registry) this.throwNotInitializedError();
+ return this._registry;
+ }
+
+ public override expose(): EcsContextApi {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const library = this;
+ return {
+ get registry() {
+ return library.registry;
+ },
+ };
+ }
+}
diff --git a/modules/ecs/test/client/ecs-client.spec.ts b/modules/ecs/test/client/ecs-client.spec.ts
new file mode 100644
index 00000000..5b52465b
--- /dev/null
+++ b/modules/ecs/test/client/ecs-client.spec.ts
@@ -0,0 +1,148 @@
+import type { Context, InitContext } from "@nanoforge-dev/common";
+import { beforeAll, describe, expect, it, vi } from "vitest";
+
+import { EcsLibrary } from "../../src/client";
+import type { Registry } from "../../src/client";
+
+class Position {
+ name = "Position";
+ constructor(
+ public x: number,
+ public y: number,
+ ) {}
+}
+
+class Velocity {
+ name = "Velocity";
+ constructor(
+ public x: number,
+ public y: number,
+ ) {}
+}
+
+const makeInitContext = (): InitContext => ({
+ vars: { get: () => undefined, set: () => {} },
+ env: {},
+ files: new Map([["libecs.wasm", "./lib/web/libecs.wasm"]]),
+});
+
+const makeContext = (): Context =>
+ ({
+ app: {} as any,
+ vars: { get: () => undefined, set: () => {} },
+ assets: {} as any,
+ }) as unknown as Context;
+
+describe("EcsLibrary (client)", () => {
+ let ecs: EcsLibrary;
+ let registry: Registry;
+
+ beforeAll(async () => {
+ ecs = new EcsLibrary();
+ await ecs.__init(makeInitContext());
+ registry = ecs.registry;
+ });
+
+ describe("metadata", () => {
+ it("should expose the reserved 'ecs' key", () => {
+ expect(ecs.key).toBe("ecs");
+ });
+ });
+
+ describe("before initialization", () => {
+ it("should throw when registry is accessed before __init", () => {
+ expect(() => new EcsLibrary().registry).toThrow();
+ });
+ });
+
+ describe("entity management", () => {
+ it("should spawn entities with incrementing ids", () => {
+ const e0 = registry.spawnEntity();
+ const e1 = registry.spawnEntity();
+ expect(e1.getId()).toBe(e0.getId() + 1);
+ });
+
+ it("should kill an entity and remove its components", () => {
+ const entity = registry.spawnEntity();
+ registry.addComponent(entity, new Position(1, 2));
+ registry.killEntity(entity);
+ expect(registry.getComponents(Position).get(entity.getId())).toBeUndefined();
+ });
+ });
+
+ describe("component management", () => {
+ it("should add and override components on an entity", () => {
+ const entity = registry.spawnEntity();
+ registry.addComponent(entity, new Position(1, 2));
+ registry.addComponent(entity, new Position(9, 9));
+ expect(registry.getComponents(Position).get(entity.getId())).toStrictEqual(
+ new Position(9, 9),
+ );
+ });
+
+ it("should add multiple component types to the same entity", () => {
+ const entity = registry.spawnEntity();
+ registry.addComponent(entity, new Position(3, 4));
+ registry.addComponent(entity, new Velocity(1, 0));
+ expect(registry.getComponents(Velocity).get(entity.getId())).toStrictEqual(
+ new Velocity(1, 0),
+ );
+ });
+
+ it("should remove a component from an entity", () => {
+ const entity = registry.spawnEntity();
+ registry.addComponent(entity, new Position(1, 2));
+ registry.removeComponent(entity, Position);
+ expect(registry.getComponents(Position).get(entity.getId())).toBeUndefined();
+ });
+ });
+
+ describe("system management", () => {
+ it("should run all registered systems in order on __run", async () => {
+ const order: number[] = [];
+ registry.clearSystems();
+ registry.addSystem(() => order.push(1));
+ registry.addSystem(() => order.push(2));
+
+ await ecs.__run(makeContext());
+
+ expect(order).toStrictEqual([1, 2]);
+ registry.clearSystems();
+ });
+ });
+
+ describe("editor hot-reload", () => {
+ it("registers a 'hot-reload' listener on ctx.editor once (via __events, not __run), and applies it via addComponent", async () => {
+ const lib = new EcsLibrary();
+ await lib.__init(makeInitContext());
+ const reg = lib.registry;
+ const entity = reg.spawnEntity();
+
+ const on = vi.fn();
+ const ctx = { ...makeContext(), editor: { emit: vi.fn(), on } };
+
+ await lib.__events(ctx as any);
+ expect(on).toHaveBeenCalledTimes(1);
+ expect(on).toHaveBeenCalledWith("hot-reload", expect.any(Function));
+
+ const handler = on.mock.calls[0]![1] as (...args: unknown[]) => void;
+ handler(entity, new Position(5, 5));
+ expect(reg.getComponents(Position).get(entity.getId())).toStrictEqual(new Position(5, 5));
+
+ await lib.__events(ctx as any);
+ expect(on).toHaveBeenCalledTimes(1); // still only wired once
+ });
+ });
+
+ describe("expose", () => {
+ it("should expose the same registry via the context API", () => {
+ expect(ecs.expose().registry).toBe(registry);
+ });
+ });
+
+ describe("lifecycle", () => {
+ it("should clear without error", async () => {
+ await expect(ecs.__clear(makeContext())).resolves.toBeUndefined();
+ });
+ });
+});
diff --git a/modules/ecs/test/server/ecs-server.spec.ts b/modules/ecs/test/server/ecs-server.spec.ts
new file mode 100644
index 00000000..eeefcc07
--- /dev/null
+++ b/modules/ecs/test/server/ecs-server.spec.ts
@@ -0,0 +1,148 @@
+import type { Context, InitContext } from "@nanoforge-dev/common";
+import { beforeAll, describe, expect, it, vi } from "vitest";
+
+import { EcsLibrary } from "../../src/server";
+import type { Registry } from "../../src/server";
+
+class Position {
+ name = "Position";
+ constructor(
+ public x: number,
+ public y: number,
+ ) {}
+}
+
+class Velocity {
+ name = "Velocity";
+ constructor(
+ public x: number,
+ public y: number,
+ ) {}
+}
+
+const makeInitContext = (): InitContext => ({
+ vars: { get: () => undefined, set: () => {} },
+ env: {},
+ files: new Map([["libecs.wasm", "./lib/node/libecs.wasm"]]),
+});
+
+const makeContext = (): Context =>
+ ({
+ app: {} as any,
+ vars: { get: () => undefined, set: () => {} },
+ assets: {} as any,
+ }) as unknown as Context;
+
+describe("EcsLibrary (server)", () => {
+ let ecs: EcsLibrary;
+ let registry: Registry;
+
+ beforeAll(async () => {
+ ecs = new EcsLibrary();
+ await ecs.__init(makeInitContext());
+ registry = ecs.registry;
+ });
+
+ describe("metadata", () => {
+ it("should expose the reserved 'ecs' key", () => {
+ expect(ecs.key).toBe("ecs");
+ });
+ });
+
+ describe("before initialization", () => {
+ it("should throw when registry is accessed before __init", () => {
+ expect(() => new EcsLibrary().registry).toThrow();
+ });
+ });
+
+ describe("entity management", () => {
+ it("should spawn entities with incrementing ids", () => {
+ const e0 = registry.spawnEntity();
+ const e1 = registry.spawnEntity();
+ expect(e1.getId()).toBe(e0.getId() + 1);
+ });
+
+ it("should kill an entity and remove its components", () => {
+ const entity = registry.spawnEntity();
+ registry.addComponent(entity, new Position(1, 2));
+ registry.killEntity(entity);
+ expect(registry.getComponents(Position).get(entity.getId())).toBeUndefined();
+ });
+ });
+
+ describe("component management", () => {
+ it("should add and override components on an entity", () => {
+ const entity = registry.spawnEntity();
+ registry.addComponent(entity, new Position(1, 2));
+ registry.addComponent(entity, new Position(9, 9));
+ expect(registry.getComponents(Position).get(entity.getId())).toStrictEqual(
+ new Position(9, 9),
+ );
+ });
+
+ it("should add multiple component types to the same entity", () => {
+ const entity = registry.spawnEntity();
+ registry.addComponent(entity, new Position(3, 4));
+ registry.addComponent(entity, new Velocity(1, 0));
+ expect(registry.getComponents(Velocity).get(entity.getId())).toStrictEqual(
+ new Velocity(1, 0),
+ );
+ });
+
+ it("should remove a component from an entity", () => {
+ const entity = registry.spawnEntity();
+ registry.addComponent(entity, new Position(1, 2));
+ registry.removeComponent(entity, Position);
+ expect(registry.getComponents(Position).get(entity.getId())).toBeUndefined();
+ });
+ });
+
+ describe("system management", () => {
+ it("should run all registered systems in order on __run", async () => {
+ const order: number[] = [];
+ registry.clearSystems();
+ registry.addSystem(() => order.push(1));
+ registry.addSystem(() => order.push(2));
+
+ await ecs.__run(makeContext());
+
+ expect(order).toStrictEqual([1, 2]);
+ registry.clearSystems();
+ });
+ });
+
+ describe("editor hot-reload", () => {
+ it("registers a 'hot-reload' listener on ctx.editor once (via __events, not __run), and applies it via addComponent", async () => {
+ const lib = new EcsLibrary();
+ await lib.__init(makeInitContext());
+ const reg = lib.registry;
+ const entity = reg.spawnEntity();
+
+ const on = vi.fn();
+ const ctx = { ...makeContext(), editor: { emit: vi.fn(), on } };
+
+ await lib.__events(ctx as any);
+ expect(on).toHaveBeenCalledTimes(1);
+ expect(on).toHaveBeenCalledWith("hot-reload", expect.any(Function));
+
+ const handler = on.mock.calls[0]![1] as (...args: unknown[]) => void;
+ handler(entity, new Position(5, 5));
+ expect(reg.getComponents(Position).get(entity.getId())).toStrictEqual(new Position(5, 5));
+
+ await lib.__events(ctx as any);
+ expect(on).toHaveBeenCalledTimes(1); // still only wired once
+ });
+ });
+
+ describe("expose", () => {
+ it("should expose the same registry via the context API", () => {
+ expect(ecs.expose().registry).toBe(registry);
+ });
+ });
+
+ describe("lifecycle", () => {
+ it("should clear without error", async () => {
+ await expect(ecs.__clear(makeContext())).resolves.toBeUndefined();
+ });
+ });
+});
diff --git a/packages/asset-manager/tsconfig.json b/modules/ecs/tsconfig.json
similarity index 100%
rename from packages/asset-manager/tsconfig.json
rename to modules/ecs/tsconfig.json
diff --git a/packages/asset-manager/tsconfig.spec.json b/modules/ecs/tsconfig.spec.json
similarity index 100%
rename from packages/asset-manager/tsconfig.spec.json
rename to modules/ecs/tsconfig.spec.json
diff --git a/packages/asset-manager/tsdoc.json b/modules/ecs/tsdoc.json
similarity index 100%
rename from packages/asset-manager/tsdoc.json
rename to modules/ecs/tsdoc.json
diff --git a/modules/ecs/tsdown.config.ts b/modules/ecs/tsdown.config.ts
new file mode 100644
index 00000000..cc6d4b65
--- /dev/null
+++ b/modules/ecs/tsdown.config.ts
@@ -0,0 +1,6 @@
+import { createTsdownConfig } from "../../tsdown.config";
+
+export default [
+ createTsdownConfig({ entry: "src/client/index.ts", outDir: "dist/client" }),
+ createTsdownConfig({ entry: "src/server/index.ts", outDir: "dist/server" }),
+];
diff --git a/packages/ecs-lib/wasm/Entity.cpp b/modules/ecs/wasm/Entity.cpp
similarity index 95%
rename from packages/ecs-lib/wasm/Entity.cpp
rename to modules/ecs/wasm/Entity.cpp
index 30064bb8..fc42f5a5 100644
--- a/packages/ecs-lib/wasm/Entity.cpp
+++ b/modules/ecs/wasm/Entity.cpp
@@ -7,7 +7,7 @@
** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇
** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀
** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀
-** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
+** 2026 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀
*/
diff --git a/packages/ecs-lib/wasm/Entity.hpp b/modules/ecs/wasm/Entity.hpp
similarity index 97%
rename from packages/ecs-lib/wasm/Entity.hpp
rename to modules/ecs/wasm/Entity.hpp
index 27e2f46f..40327742 100644
--- a/packages/ecs-lib/wasm/Entity.hpp
+++ b/modules/ecs/wasm/Entity.hpp
@@ -7,7 +7,7 @@
** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇
** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀
** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀
-** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
+** 2026 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀
*/
diff --git a/packages/ecs-lib/wasm/Registry.cpp b/modules/ecs/wasm/Registry.cpp
similarity index 98%
rename from packages/ecs-lib/wasm/Registry.cpp
rename to modules/ecs/wasm/Registry.cpp
index a20dd52b..68c97d6b 100644
--- a/packages/ecs-lib/wasm/Registry.cpp
+++ b/modules/ecs/wasm/Registry.cpp
@@ -7,7 +7,7 @@
** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇
** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀
** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀
-** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
+** 2026 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀
*/
diff --git a/packages/ecs-lib/wasm/Registry.hpp b/modules/ecs/wasm/Registry.hpp
similarity index 99%
rename from packages/ecs-lib/wasm/Registry.hpp
rename to modules/ecs/wasm/Registry.hpp
index 5912df5a..28fb44cf 100644
--- a/packages/ecs-lib/wasm/Registry.hpp
+++ b/modules/ecs/wasm/Registry.hpp
@@ -7,7 +7,7 @@
** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇
** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀
** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀
-** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
+** 2026 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀
*/
diff --git a/packages/ecs-lib/wasm/SparseArray.cpp b/modules/ecs/wasm/SparseArray.cpp
similarity index 98%
rename from packages/ecs-lib/wasm/SparseArray.cpp
rename to modules/ecs/wasm/SparseArray.cpp
index da9e1f02..ee95ef24 100644
--- a/packages/ecs-lib/wasm/SparseArray.cpp
+++ b/modules/ecs/wasm/SparseArray.cpp
@@ -7,7 +7,7 @@
** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇
** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀
** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀
-** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
+** 2026 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀
*/
diff --git a/packages/ecs-lib/wasm/SparseArray.hpp b/modules/ecs/wasm/SparseArray.hpp
similarity index 99%
rename from packages/ecs-lib/wasm/SparseArray.hpp
rename to modules/ecs/wasm/SparseArray.hpp
index 983507cc..a46f9dba 100644
--- a/packages/ecs-lib/wasm/SparseArray.hpp
+++ b/modules/ecs/wasm/SparseArray.hpp
@@ -7,7 +7,7 @@
** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇
** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀
** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀
-** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
+** 2026 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀
*/
diff --git a/packages/ecs-lib/wasm/Utils.cpp b/modules/ecs/wasm/Utils.cpp
similarity index 97%
rename from packages/ecs-lib/wasm/Utils.cpp
rename to modules/ecs/wasm/Utils.cpp
index ad9ee3ce..494ebd3d 100644
--- a/packages/ecs-lib/wasm/Utils.cpp
+++ b/modules/ecs/wasm/Utils.cpp
@@ -7,7 +7,7 @@
** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇
** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀
** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀
-** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
+** 2026 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀
*/
diff --git a/packages/ecs-lib/wasm/Utils.hpp b/modules/ecs/wasm/Utils.hpp
similarity index 96%
rename from packages/ecs-lib/wasm/Utils.hpp
rename to modules/ecs/wasm/Utils.hpp
index e8ae8f74..4b13917d 100644
--- a/packages/ecs-lib/wasm/Utils.hpp
+++ b/modules/ecs/wasm/Utils.hpp
@@ -7,7 +7,7 @@
** /_/ |_/\__,_/_/ /_/\____/_/ \____/_/ \__, /\___/ ⠸⣿⡄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣤⠀⠀⠀⠀⢠⣿⠇
** /____/ ⠀ ⢿⣷⡀⠀⠀⠀⠉⠁⠀⠀⠈⠉⠀⠀⠀⢀⣾⡿⠀
** ⠀⠀⠹⣿⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣿⠏⠀⠀
-** 2025 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
+** 2026 ⠀⠀⠀⠈⠙⢿⣷⣶⣤⣤⣤⣤⣶⣾⡿⠋⠁⠀⠀⠀
**⠀ ⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀
*/
diff --git a/packages/ecs-lib/.cliff-jumperrc.json b/modules/editor/.cliff-jumperrc.json
similarity index 77%
rename from packages/ecs-lib/.cliff-jumperrc.json
rename to modules/editor/.cliff-jumperrc.json
index 7a76cd75..913d7626 100644
--- a/packages/ecs-lib/.cliff-jumperrc.json
+++ b/modules/editor/.cliff-jumperrc.json
@@ -1,8 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
- "name": "ecs-lib",
+ "name": "editor-lib",
"org": "nanoforge-dev",
- "packagePath": "packages/ecs-lib",
+ "packagePath": "modules/editor",
"tagTemplate": "{{new-version}}",
"identifierBase": false
}
diff --git a/packages/config/.gitignore b/modules/editor/.gitignore
similarity index 100%
rename from packages/config/.gitignore
rename to modules/editor/.gitignore
diff --git a/packages/config/.prettierignore b/modules/editor/.prettierignore
similarity index 100%
rename from packages/config/.prettierignore
rename to modules/editor/.prettierignore
diff --git a/modules/editor/CHANGELOG.md b/modules/editor/CHANGELOG.md
new file mode 100644
index 00000000..6361e43e
--- /dev/null
+++ b/modules/editor/CHANGELOG.md
@@ -0,0 +1,3 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
diff --git a/packages/config/LICENSE b/modules/editor/LICENSE
similarity index 97%
rename from packages/config/LICENSE
rename to modules/editor/LICENSE
index 62c6400b..46724861 100644
--- a/packages/config/LICENSE
+++ b/modules/editor/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright © 2025 NanoForge
+Copyright © 2026 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
diff --git a/modules/editor/README.md b/modules/editor/README.md
new file mode 100644
index 00000000..d03f9dc5
--- /dev/null
+++ b/modules/editor/README.md
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## About
+
+`@nanoforge-dev/editor-lib` is the built-in bridge library between a running NanoForge application and an external editor host. It turns the raw `toEditor`/`fromEditor` event-emitter pair supplied via `RunOptions.editor` into the single, asymmetric `Context.editor` facade every other library sees.
+
+## Installation
+
+**Node.js 26 or newer is required.**
+
+```sh
+npm install @nanoforge-dev/editor-lib
+yarn add @nanoforge-dev/editor-lib
+pnpm add @nanoforge-dev/editor-lib
+bun add @nanoforge-dev/editor-lib
+```
+
+## Warning
+
+This library is opt-in — only register it when the app is started under an editor host (`app.use(new EditorLibrary())`). Without it, `Context.editor` is simply absent, and other libraries that check for it (like `@nanoforge-dev/ecs`'s hot-reload wiring) no-op cleanly.
+
+## Example usage
+
+Construct one `QueuedEventEmitter` per direction and hand both to `RunOptions.editor` when starting the app:
+
+```ts
+import { QueuedEventEmitter } from "@nanoforge-dev/editor-lib";
+
+const toEditor = new QueuedEventEmitter(); // engine -> editor
+const fromEditor = new QueuedEventEmitter(); // editor -> engine
+
+await app.init({
+ ...options,
+ editor: { toEditor, fromEditor },
+});
+```
+
+Register `EditorLibrary` alongside your other libraries:
+
+```ts
+import { EditorLibrary } from "@nanoforge-dev/editor-lib";
+
+app.use(new EditorLibrary());
+```
+
+Any registered library can then talk to the editor through `Context.editor`:
+
+```ts
+public override async __run(ctx: Context): Promise {
+ ctx.editor?.emit("component-moved", entityId, position);
+}
+```
+
+The editor host itself drives the other two operations directly on the raw emitters it created — sending commands via `fromEditor.emit(...)` and listening for outgoing notifications via `toEditor.on(...)` — which is why `Context.editor` only exposes `emit`/`on` for the reverse direction, not all four.
+
+## Base commands
+
+`EditorLibrary` handles three reserved editor → engine commands itself, with no app-side wiring required — they're forwarded straight to `Context.app`'s pause/resume/stop actions:
+
+```ts
+import { EditorCommand } from "@nanoforge-dev/editor-lib";
+
+fromEditor.emit(EditorCommand.Pause); // ctx.app.requestPause()
+fromEditor.emit(EditorCommand.Resume); // ctx.app.requestResume()
+fromEditor.emit(EditorCommand.Stop); // ctx.app.requestStop()
+```
+
+Because `EditorLibrary` drains `fromEditor` from `__events` (which keeps running even while paused), a queued `EditorCommand.Resume` always reaches the engine and lifts the pause — it isn't blocked behind the very pause it's meant to undo.
+
+## 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/editor
+[npm]: https://www.npmjs.com/package/@nanoforge-dev/editor-lib
+[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md
diff --git a/packages/config/cliff.toml b/modules/editor/cliff.toml
similarity index 100%
rename from packages/config/cliff.toml
rename to modules/editor/cliff.toml
diff --git a/packages/config/eslint.config.js b/modules/editor/eslint.config.js
similarity index 100%
rename from packages/config/eslint.config.js
rename to modules/editor/eslint.config.js
diff --git a/packages/config/mint-tsdocs.config.json b/modules/editor/mint-tsdocs.config.json
similarity index 82%
rename from packages/config/mint-tsdocs.config.json
rename to modules/editor/mint-tsdocs.config.json
index d68b969c..86f12472 100644
--- a/packages/config/mint-tsdocs.config.json
+++ b/modules/editor/mint-tsdocs.config.json
@@ -1,7 +1,7 @@
{
"$schema": "./node_modules/mint-tsdocs/lib/schemas/config.schema.json",
"entryPoint": "dist/index.d.ts",
- "outputFolder": "../../docs/references/config",
+ "outputFolder": "../../docs/references/editor",
"lint": {
"eslint": {
"enabled": true
diff --git a/modules/editor/package.json b/modules/editor/package.json
new file mode 100644
index 00000000..a3cf9c99
--- /dev/null
+++ b/modules/editor/package.json
@@ -0,0 +1,91 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "@nanoforge-dev/editor-lib",
+ "version": "1.4.2",
+ "description": "NanoForge Engine - 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 ",
+ "Josephine ",
+ "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": "modules/editor"
+ },
+ "funding": "https://github.com/NanoForge-dev/Engine?sponsor",
+ "scripts": {
+ "build": "tsc --noEmit && tsdown --config-loader unrun",
+ "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 'modules/editor/*'",
+ "release": "cliff-jumper",
+ "docs": "mint-tsdocs generate"
+ },
+ "dependencies": {
+ "@nanoforge-dev/common": "workspace:*"
+ },
+ "devDependencies": {
+ "@favware/cliff-jumper": "catalog:ci",
+ "@nanoforge-dev/core": "workspace:*",
+ "@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",
+ "tsdown": "catalog:build",
+ "typescript": "catalog:core",
+ "unrun": "catalog:build",
+ "vitest": "catalog:test"
+ },
+ "packageManager": "pnpm@12.0.0",
+ "engines": {
+ "node": "26"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "lint-staged": {
+ "**": [
+ "prettier --ignore-unknown --write"
+ ],
+ "src/**/*.ts": [
+ "eslint --fix"
+ ]
+ }
+}
diff --git a/packages/config/prettier.config.js b/modules/editor/prettier.config.js
similarity index 100%
rename from packages/config/prettier.config.js
rename to modules/editor/prettier.config.js
diff --git a/modules/editor/src/context-augmentation.ts b/modules/editor/src/context-augmentation.ts
new file mode 100644
index 00000000..eac63b2f
--- /dev/null
+++ b/modules/editor/src/context-augmentation.ts
@@ -0,0 +1,7 @@
+import type { EditorContextApi } from "./editor-context.type";
+
+declare module "@nanoforge-dev/common" {
+ interface Context {
+ editor: EditorContextApi;
+ }
+}
diff --git a/modules/editor/src/editor-command.enum.ts b/modules/editor/src/editor-command.enum.ts
new file mode 100644
index 00000000..1c7bd06e
--- /dev/null
+++ b/modules/editor/src/editor-command.enum.ts
@@ -0,0 +1,19 @@
+/**
+ * Reserved editor → engine command names that `EditorLibrary` handles
+ * itself, wired directly onto `Context.app`'s pause/resume/stop actions.
+ *
+ * @remarks
+ * An editor host can send these through the raw `fromEditor` emitter it
+ * created (see `RunOptions.editor`) without any app-side wiring — no other
+ * library needs to know about them.
+ *
+ * @example
+ * ```ts
+ * fromEditor.emit(EditorCommand.Pause);
+ * ```
+ */
+export enum EditorCommand {
+ Pause = "pause",
+ Resume = "resume",
+ Stop = "stop",
+}
diff --git a/modules/editor/src/editor-context.type.ts b/modules/editor/src/editor-context.type.ts
new file mode 100644
index 00000000..7aef3ccc
--- /dev/null
+++ b/modules/editor/src/editor-context.type.ts
@@ -0,0 +1,18 @@
+/**
+ * Public surface of `EditorLibrary`, exposed on `Context.editor`.
+ *
+ * @remarks
+ * Deliberately asymmetric: `emit` only sends to the editor, `on` only
+ * listens for commands from the editor. `Context.editor` reaches every
+ * registered library, so it must not let arbitrary library code impersonate
+ * an editor command or eavesdrop on the outgoing channel — the reverse two
+ * operations (listening on the outgoing channel, emitting on the incoming
+ * one) are reserved for the external editor host, which holds its own
+ * direct reference to both raw emitters.
+ */
+export interface EditorContextApi {
+ /** Sends a notification to the editor (e.g. "a component moved"). */
+ emit(event: string, ...args: unknown[]): void;
+ /** Listens for a command from the editor (e.g. "hot-reload"). */
+ on(event: string, listener: (...args: unknown[]) => void): void;
+}
diff --git a/modules/editor/src/editor-library.ts b/modules/editor/src/editor-library.ts
new file mode 100644
index 00000000..d9495740
--- /dev/null
+++ b/modules/editor/src/editor-library.ts
@@ -0,0 +1,70 @@
+import {
+ type Context,
+ type EventEmitter,
+ type InitContext,
+ Library,
+ defineLibraryKey,
+} from "@nanoforge-dev/common";
+
+import { EditorCommand } from "./editor-command.enum";
+import type { EditorContextApi } from "./editor-context.type";
+
+/**
+ * Base editor bridge library.
+ *
+ * @remarks
+ * Opt-in — register it only when the app is started under an editor host
+ * (`app.use(new EditorLibrary())`), not a mandatory built-in like the asset
+ * library. Reads the raw `toEditor`/`fromEditor` emitter pair from
+ * `RunOptions.editor` (see `@nanoforge-dev/common`'s `RunOptions`
+ * augmentation) and transforms them into the single `Context.editor` facade
+ * every other library sees.
+ */
+export class EditorLibrary extends Library {
+ readonly key = defineLibraryKey("editor");
+
+ private _toEditor: EventEmitter | undefined;
+ private _fromEditor: EventEmitter | undefined;
+ private _baseCommandsWired = false;
+
+ public override async __init(ctx: InitContext): Promise {
+ this._toEditor = ctx.editor?.toEditor;
+ this._fromEditor = ctx.editor?.fromEditor;
+ }
+
+ /**
+ * Drains queued editor → engine commands every tick — via `__events`, not
+ * `__run`, so this keeps happening even while the app is paused. That's
+ * what lets a queued "resume" command actually reach and lift the pause;
+ * if this drained inside `__run` instead, a paused app could never
+ * process the very command that would unpause it.
+ *
+ * Also wires the base `EditorCommand.Pause`/`.Resume`/`.Stop` commands
+ * (once) directly onto `Context.app` — an editor host gets these three
+ * actions for free, with no app-side listener code required.
+ */
+ public override async __events(ctx: Context): Promise {
+ if (!this._baseCommandsWired && this._fromEditor) {
+ this._fromEditor.on(EditorCommand.Pause, () => ctx.app.requestPause());
+ this._fromEditor.on(EditorCommand.Resume, () => ctx.app.requestResume());
+ this._fromEditor.on(EditorCommand.Stop, () => ctx.app.requestStop());
+ this._baseCommandsWired = true;
+ }
+ this._fromEditor?.runEvents();
+ }
+
+ public override expose(): EditorContextApi {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const library = this;
+ return {
+ emit: (event, ...args) => {
+ if (!library._toEditor) library.throwNotInitializedError();
+ library._toEditor?.emit(event, ...args);
+ },
+ on: (event, listener) => {
+ if (!library._fromEditor) library.throwNotInitializedError();
+ library._fromEditor?.on(event, listener);
+ },
+ };
+ }
+}
diff --git a/modules/editor/src/event-emitter.ts b/modules/editor/src/event-emitter.ts
new file mode 100644
index 00000000..5d122d45
--- /dev/null
+++ b/modules/editor/src/event-emitter.ts
@@ -0,0 +1,47 @@
+import type { EventEmitter } from "@nanoforge-dev/common";
+
+/**
+ * Default `EventEmitter` implementation. Construct one for each direction of
+ * an editor bridge (`toEditor`/`fromEditor`) and hand both to `RunOptions.editor`.
+ */
+export class QueuedEventEmitter<
+ Events extends string = string,
+ EventsMap extends Record = Record,
+> implements EventEmitter {
+ private readonly listeners = new Map void)[]>();
+ private readonly queue: { event: Events; args: EventsMap[Events] }[] = [];
+
+ on(event: K, listener: (...args: EventsMap[K]) => void): void {
+ const list = this.listeners.get(event) ?? [];
+ list.push(listener as (...args: EventsMap[Events]) => void);
+ this.listeners.set(event, list);
+ }
+
+ off(event: K, listener: (...args: EventsMap[K]) => void): void {
+ const list = this.listeners.get(event);
+ if (!list) return;
+ this.listeners.set(
+ event,
+ list.filter(
+ (registered) => registered !== (listener as (...args: EventsMap[Events]) => void),
+ ),
+ );
+ }
+
+ emit(event: K, ...args: EventsMap[K]): void {
+ this.queue.push({ event, args });
+ }
+
+ runEvents(): void {
+ const pending = this.queue.splice(0, this.queue.length);
+ for (const { event, args } of pending) {
+ for (const listener of this.listeners.get(event) ?? []) {
+ try {
+ listener(...args);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ }
+ }
+}
diff --git a/modules/editor/src/index.ts b/modules/editor/src/index.ts
new file mode 100644
index 00000000..3672df5c
--- /dev/null
+++ b/modules/editor/src/index.ts
@@ -0,0 +1,6 @@
+import "./context-augmentation";
+
+export { EditorCommand } from "./editor-command.enum";
+export type { EditorContextApi } from "./editor-context.type";
+export { EditorLibrary } from "./editor-library";
+export { QueuedEventEmitter } from "./event-emitter";
diff --git a/modules/editor/test/editor-library.spec.ts b/modules/editor/test/editor-library.spec.ts
new file mode 100644
index 00000000..e3f7fd3f
--- /dev/null
+++ b/modules/editor/test/editor-library.spec.ts
@@ -0,0 +1,166 @@
+import type { AppContext, EventEmitter, InitContext } from "@nanoforge-dev/common";
+import { describe, expect, it, vi } from "vitest";
+
+import { EditorCommand, EditorLibrary } from "../src";
+
+const makeEmitter = (): EventEmitter => {
+ const listeners = new Map void)[]>();
+ return {
+ on: vi.fn((event: string, listener: (...args: unknown[]) => void) => {
+ const list = listeners.get(event) ?? [];
+ list.push(listener);
+ listeners.set(event, list);
+ }),
+ off: vi.fn(),
+ emit: vi.fn((event: string, ...args: unknown[]) => {
+ for (const listener of listeners.get(event) ?? []) listener(...args);
+ }),
+ runEvents: vi.fn(),
+ };
+};
+
+const makeInitContext = (editor?: InitContext["editor"]): InitContext => ({
+ vars: { get: () => undefined, set: () => {} },
+ env: {},
+ files: new Map(),
+ ...(editor ? { editor } : {}),
+});
+
+const makeAppContext = (): AppContext => ({
+ isRunning: true,
+ isPaused: false,
+ delta: 0,
+ tickRate: 60,
+ requestStop: vi.fn(),
+ requestPause: vi.fn(),
+ requestResume: vi.fn(),
+});
+
+describe("EditorLibrary", () => {
+ it("exposes the reserved 'editor' key", () => {
+ expect(new EditorLibrary().key).toBe("editor");
+ });
+
+ it("expose().emit forwards to toEditor.emit, not fromEditor", async () => {
+ const toEditor = makeEmitter();
+ const fromEditor = makeEmitter();
+ const lib = new EditorLibrary();
+ await lib.__init(makeInitContext({ toEditor, fromEditor }));
+
+ lib.expose().emit("move-component", "e1", "Position", { x: 1, y: 2 });
+
+ expect(toEditor.emit).toHaveBeenCalledWith("move-component", "e1", "Position", { x: 1, y: 2 });
+ expect(fromEditor.emit).not.toHaveBeenCalled();
+ });
+
+ it("expose().on forwards to fromEditor.on, not toEditor", async () => {
+ const toEditor = makeEmitter();
+ const fromEditor = makeEmitter();
+ const lib = new EditorLibrary();
+ await lib.__init(makeInitContext({ toEditor, fromEditor }));
+
+ const handler = vi.fn();
+ lib.expose().on("hot-reload", handler);
+
+ expect(fromEditor.on).toHaveBeenCalledWith("hot-reload", handler);
+ expect(toEditor.on).not.toHaveBeenCalled();
+ });
+
+ it("only exposes emit and on — the reverse two operations are not reachable", async () => {
+ const lib = new EditorLibrary();
+ await lib.__init(makeInitContext({ toEditor: makeEmitter(), fromEditor: makeEmitter() }));
+
+ expect(Object.keys(lib.expose())).toEqual(["emit", "on"]);
+ });
+
+ it("__events drains fromEditor's queue each tick — not __run, so it still runs while paused", async () => {
+ const toEditor = makeEmitter();
+ const fromEditor = makeEmitter();
+ const lib = new EditorLibrary();
+ await lib.__init(makeInitContext({ toEditor, fromEditor }));
+
+ await lib.__events({} as any);
+
+ expect(fromEditor.runEvents).toHaveBeenCalledOnce();
+ });
+
+ it("__events does not throw when no editor bridge was provided (non-editor runs)", async () => {
+ const lib = new EditorLibrary();
+ await expect(lib.__init(makeInitContext())).resolves.toBeUndefined();
+ await expect(lib.__events({} as any)).resolves.toBeUndefined();
+ });
+
+ describe("base commands", () => {
+ it("wires EditorCommand.Pause/Resume/Stop to ctx.app once, on the first __events call", async () => {
+ const toEditor = makeEmitter();
+ const fromEditor = makeEmitter();
+ const app = makeAppContext();
+ const lib = new EditorLibrary();
+ await lib.__init(makeInitContext({ toEditor, fromEditor }));
+
+ await lib.__events({ app } as any);
+ await lib.__events({ app } as any);
+
+ expect(fromEditor.on).toHaveBeenCalledTimes(3);
+ expect(fromEditor.on).toHaveBeenCalledWith(EditorCommand.Pause, expect.any(Function));
+ expect(fromEditor.on).toHaveBeenCalledWith(EditorCommand.Resume, expect.any(Function));
+ expect(fromEditor.on).toHaveBeenCalledWith(EditorCommand.Stop, expect.any(Function));
+ });
+
+ it("EditorCommand.Pause calls ctx.app.requestPause", async () => {
+ const toEditor = makeEmitter();
+ const fromEditor = makeEmitter();
+ const app = makeAppContext();
+ const lib = new EditorLibrary();
+ await lib.__init(makeInitContext({ toEditor, fromEditor }));
+
+ await lib.__events({ app } as any);
+ fromEditor.emit(EditorCommand.Pause);
+
+ expect(app.requestPause).toHaveBeenCalledOnce();
+ expect(app.requestResume).not.toHaveBeenCalled();
+ expect(app.requestStop).not.toHaveBeenCalled();
+ });
+
+ it("EditorCommand.Resume calls ctx.app.requestResume", async () => {
+ const toEditor = makeEmitter();
+ const fromEditor = makeEmitter();
+ const app = makeAppContext();
+ const lib = new EditorLibrary();
+ await lib.__init(makeInitContext({ toEditor, fromEditor }));
+
+ await lib.__events({ app } as any);
+ fromEditor.emit(EditorCommand.Resume);
+
+ expect(app.requestResume).toHaveBeenCalledOnce();
+ });
+
+ it("EditorCommand.Stop calls ctx.app.requestStop", async () => {
+ const toEditor = makeEmitter();
+ const fromEditor = makeEmitter();
+ const app = makeAppContext();
+ const lib = new EditorLibrary();
+ await lib.__init(makeInitContext({ toEditor, fromEditor }));
+
+ await lib.__events({ app } as any);
+ fromEditor.emit(EditorCommand.Stop);
+
+ expect(app.requestStop).toHaveBeenCalledOnce();
+ });
+
+ it("does not wire base commands when no editor bridge was provided", async () => {
+ const lib = new EditorLibrary();
+ await lib.__init(makeInitContext());
+
+ await expect(lib.__events({ app: makeAppContext() } as any)).resolves.toBeUndefined();
+ });
+ });
+
+ it("expose().emit/on throw when no editor bridge was provided, like every other library's not-initialized guard", async () => {
+ const lib = new EditorLibrary();
+ await lib.__init(makeInitContext());
+
+ expect(() => lib.expose().emit("x")).toThrow();
+ expect(() => lib.expose().on("x", vi.fn())).toThrow();
+ });
+});
diff --git a/modules/editor/test/event-emitter.spec.ts b/modules/editor/test/event-emitter.spec.ts
new file mode 100644
index 00000000..f51a81b1
--- /dev/null
+++ b/modules/editor/test/event-emitter.spec.ts
@@ -0,0 +1,83 @@
+import { describe, expect, it, vi } from "vitest";
+
+import { QueuedEventEmitter } from "../src";
+
+describe("QueuedEventEmitter", () => {
+ it("does not invoke listeners synchronously on emit", () => {
+ const emitter = new QueuedEventEmitter<"ping", { ping: [] }>();
+ const listener = vi.fn();
+ emitter.on("ping", listener);
+
+ emitter.emit("ping");
+
+ expect(listener).not.toHaveBeenCalled();
+ });
+
+ it("invokes listeners with the emitted args once runEvents() is called", () => {
+ const emitter = new QueuedEventEmitter<"move", { move: [number, number] }>();
+ const listener = vi.fn();
+ emitter.on("move", listener);
+
+ emitter.emit("move", 1, 2);
+ emitter.runEvents();
+
+ expect(listener).toHaveBeenCalledOnce();
+ expect(listener).toHaveBeenCalledWith(1, 2);
+ });
+
+ it("supports multiple listeners for the same event", () => {
+ const emitter = new QueuedEventEmitter<"ping", { ping: [] }>();
+ const a = vi.fn();
+ const b = vi.fn();
+ emitter.on("ping", a);
+ emitter.on("ping", b);
+
+ emitter.emit("ping");
+ emitter.runEvents();
+
+ expect(a).toHaveBeenCalledOnce();
+ expect(b).toHaveBeenCalledOnce();
+ });
+
+ it("off() removes a listener", () => {
+ const emitter = new QueuedEventEmitter<"ping", { ping: [] }>();
+ const listener = vi.fn();
+ emitter.on("ping", listener);
+ emitter.off("ping", listener);
+
+ emitter.emit("ping");
+ emitter.runEvents();
+
+ expect(listener).not.toHaveBeenCalled();
+ });
+
+ it("drains the queue so events are not re-delivered on a later runEvents()", () => {
+ const emitter = new QueuedEventEmitter<"ping", { ping: [] }>();
+ const listener = vi.fn();
+ emitter.on("ping", listener);
+
+ emitter.emit("ping");
+ emitter.runEvents();
+ emitter.runEvents();
+
+ expect(listener).toHaveBeenCalledOnce();
+ });
+
+ it("catches a listener error so other listeners still run", () => {
+ const emitter = new QueuedEventEmitter<"ping", { ping: [] }>();
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+ const throwing = vi.fn(() => {
+ throw new Error("boom");
+ });
+ const ok = vi.fn();
+ emitter.on("ping", throwing);
+ emitter.on("ping", ok);
+
+ emitter.emit("ping");
+ emitter.runEvents();
+
+ expect(ok).toHaveBeenCalledOnce();
+ expect(errorSpy).toHaveBeenCalledOnce();
+ errorSpy.mockRestore();
+ });
+});
diff --git a/modules/editor/test/integration.spec.ts b/modules/editor/test/integration.spec.ts
new file mode 100644
index 00000000..00f63d4e
--- /dev/null
+++ b/modules/editor/test/integration.spec.ts
@@ -0,0 +1,102 @@
+import { type Context, Library } from "@nanoforge-dev/common";
+import { NanoforgeFactory } from "@nanoforge-dev/core";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { EditorCommand, EditorLibrary, QueuedEventEmitter } from "../src";
+
+class ProbeLibrary extends Library {
+ readonly key = "probe";
+ public received: unknown[][] = [];
+ public capturedContext: Context | undefined;
+ private _wired = false;
+
+ public override async __events(ctx: Context): Promise {
+ this.capturedContext = ctx;
+ }
+
+ public override async __run(ctx: Context): Promise {
+ if (!this._wired && ctx.editor) {
+ ctx.editor.on("ping", (...args) => this.received.push(args));
+ this._wired = true;
+ }
+ ctx.editor?.emit("pong");
+ }
+}
+
+describe("editor bridge integration with @nanoforge-dev/core", () => {
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("wires RunOptions.editor through core's generic InitContext spread into ctx.editor for every library", async () => {
+ vi.useFakeTimers();
+
+ const toEditor = new QueuedEventEmitter();
+ const fromEditor = new QueuedEventEmitter();
+ const toEditorListener = vi.fn();
+ toEditor.on("pong", toEditorListener);
+
+ const server = NanoforgeFactory.createServer({ tickRate: 60 });
+ const probe = new ProbeLibrary();
+ server.use(new EditorLibrary());
+ server.use(probe);
+
+ await server.init({ files: new Map(), env: {}, editor: { toEditor, fromEditor } });
+ await server.run();
+
+ await vi.advanceTimersByTimeAsync(1);
+
+ toEditor.runEvents();
+ expect(toEditorListener).toHaveBeenCalled();
+
+ fromEditor.emit("ping", "hello");
+ await vi.advanceTimersByTimeAsync(50);
+
+ expect(probe.received).toEqual([["hello"]]);
+ });
+
+ it("leaves ctx.editor undefined when the app isn't started under an editor host", async () => {
+ vi.useFakeTimers();
+
+ const server = NanoforgeFactory.createServer({ tickRate: 60 });
+ const probe = new ProbeLibrary();
+ server.use(probe);
+
+ await server.init({ files: new Map(), env: {} });
+ await server.run();
+ await vi.advanceTimersByTimeAsync(1);
+
+ expect(probe.received).toEqual([]);
+ });
+
+ it("EditorCommand.Pause/Resume/Stop reach Context.app with no app-side wiring required", async () => {
+ vi.useFakeTimers();
+
+ const toEditor = new QueuedEventEmitter();
+ const fromEditor = new QueuedEventEmitter();
+
+ const server = NanoforgeFactory.createServer({ tickRate: 1000 });
+ const probe = new ProbeLibrary();
+ server.use(new EditorLibrary());
+ server.use(probe);
+
+ await server.init({ files: new Map(), env: {}, editor: { toEditor, fromEditor } });
+ await server.run();
+ await vi.advanceTimersByTimeAsync(10);
+
+ fromEditor.emit(EditorCommand.Pause);
+ await vi.advanceTimersByTimeAsync(10);
+
+ expect(probe.capturedContext!.app.isPaused).toBe(true);
+
+ fromEditor.emit(EditorCommand.Resume);
+ await vi.advanceTimersByTimeAsync(10);
+
+ expect(probe.capturedContext!.app.isPaused).toBe(false);
+
+ fromEditor.emit(EditorCommand.Stop);
+ await vi.advanceTimersByTimeAsync(10);
+
+ expect(probe.capturedContext!.app.isRunning).toBe(false);
+ });
+});
diff --git a/packages/config/tsconfig.json b/modules/editor/tsconfig.json
similarity index 100%
rename from packages/config/tsconfig.json
rename to modules/editor/tsconfig.json
diff --git a/packages/core-editor/tsconfig.spec.json b/modules/editor/tsconfig.spec.json
similarity index 100%
rename from packages/core-editor/tsconfig.spec.json
rename to modules/editor/tsconfig.spec.json
diff --git a/packages/config/tsdoc.json b/modules/editor/tsdoc.json
similarity index 100%
rename from packages/config/tsdoc.json
rename to modules/editor/tsdoc.json
diff --git a/packages/asset-manager/tsdown.config.ts b/modules/editor/tsdown.config.ts
similarity index 100%
rename from packages/asset-manager/tsdown.config.ts
rename to modules/editor/tsdown.config.ts
diff --git a/packages/graphics-2d/.cliff-jumperrc.json b/modules/graphics-2d/.cliff-jumperrc.json
similarity index 84%
rename from packages/graphics-2d/.cliff-jumperrc.json
rename to modules/graphics-2d/.cliff-jumperrc.json
index 4460df82..0390faa7 100644
--- a/packages/graphics-2d/.cliff-jumperrc.json
+++ b/modules/graphics-2d/.cliff-jumperrc.json
@@ -2,7 +2,7 @@
"$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
"name": "graphics-2d",
"org": "nanoforge-dev",
- "packagePath": "packages/graphics-2d",
+ "packagePath": "modules/graphics-2d",
"tagTemplate": "{{new-version}}",
"identifierBase": false
}
diff --git a/packages/core-editor/.gitignore b/modules/graphics-2d/.gitignore
similarity index 100%
rename from packages/core-editor/.gitignore
rename to modules/graphics-2d/.gitignore
diff --git a/packages/core-editor/.prettierignore b/modules/graphics-2d/.prettierignore
similarity index 100%
rename from packages/core-editor/.prettierignore
rename to modules/graphics-2d/.prettierignore
diff --git a/modules/graphics-2d/CHANGELOG.md b/modules/graphics-2d/CHANGELOG.md
new file mode 100644
index 00000000..6361e43e
--- /dev/null
+++ b/modules/graphics-2d/CHANGELOG.md
@@ -0,0 +1,3 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
diff --git a/packages/core-editor/LICENSE b/modules/graphics-2d/LICENSE
similarity index 97%
rename from packages/core-editor/LICENSE
rename to modules/graphics-2d/LICENSE
index 62c6400b..46724861 100644
--- a/packages/core-editor/LICENSE
+++ b/modules/graphics-2d/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright © 2025 NanoForge
+Copyright © 2026 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
diff --git a/packages/graphics-2d/README.md b/modules/graphics-2d/README.md
similarity index 56%
rename from packages/graphics-2d/README.md
rename to modules/graphics-2d/README.md
index 2781e486..b9761b88 100644
--- a/packages/graphics-2d/README.md
+++ b/modules/graphics-2d/README.md
@@ -8,18 +8,18 @@
-
+
## About
-`@nanoforge-dev/graphics-2d` is a base 2D graphics library.
+`@nanoforge-dev/graphics-2d` is NanoForge's built-in 2D rendering library, powered by [Konva][konva]. It creates a full-container `Stage` and a default `Layer` and exposes them on `Context.graphics`. Client-only.
## Installation
-**Node.js 25 or newer is required.**
+**Node.js 26 or newer is required.**
```sh
npm install @nanoforge-dev/graphics-2d
@@ -30,64 +30,27 @@ bun add @nanoforge-dev/graphics-2d
## Example usage
-Initialize the library in your main file with a red circle.
+Register `Graphics2DLibrary` on your client app:
```ts
-import { type IRunClientOptions } from "@nanoforge-dev/common";
-import { NanoforgeFactory } from "@nanoforge-dev/core";
-import { ECSClientLibrary } from "@nanoforge-dev/ecs-client";
-import { Circle, Graphics2DLibrary, Layer } from "@nanoforge-dev/graphics-2d";
+import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d";
-import { CircleComponent } from "./components/CircleComponent";
-
-export async function main(options: IRunClientOptions) {
- const app = NanoforgeFactory.createClient();
-
- const ecs = new ECSClientLibrary();
- const graphics = new Graphics2DLibrary();
-
- app.useComponentSystem(ecs);
- app.useGraphics(graphics);
-
- await app.init(options);
-
- const registry = ecs.registry;
-
- const layer = new Layer();
- graphics.stage.add(layer);
-
- const entity = registry.spawnEntity();
- registry.addComponent(
- entity,
- new CircleComponent(
- new Circle({
- radius: 70,
- fill: "red",
- }),
- layer,
- ),
- );
-
- await app.run();
-}
+app.use(new Graphics2DLibrary());
```
-With circle component defined.
+Draw shapes directly on `Context.graphics.stage`/`.baseLayer`:
```ts
-import { type Circle, type Layer } from "@nanoforge-dev/graphics-2d";
+import { Circle } from "@nanoforge-dev/graphics-2d";
-export class CircleComponent {
- name = "CircleComponent";
- component: Circle;
-
- constructor(component: Circle, layer: Layer) {
- this.component = component;
- layer.add(this.component);
- }
+public override async __run(ctx: Context): Promise {
+ const circle = new Circle({ x: 100, y: 100, radius: 40, fill: "red" });
+ ctx.graphics.baseLayer.add(circle);
}
```
+When registered alongside `@nanoforge-dev/ecs`'s `EcsLibrary` and `@nanoforge-dev/editor`'s `EditorLibrary`, `Graphics2DLibrary` automatically makes any entity carrying a `DrawableCircle2D`/`DrawableRect2D`/`DrawableText2D` component draggable in the viewport and notifies the editor on drag end — no extra wiring required.
+
## Links
- [GitHub][source]
@@ -105,6 +68,7 @@ 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/graphics-2d
+[source]: https://github.com/NanoForge-dev/Engine/tree/main/modules/graphics-2d
[npm]: https://www.npmjs.com/package/@nanoforge-dev/graphics-2d
[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md
+[konva]: https://konvajs.org/
diff --git a/packages/core-editor/cliff.toml b/modules/graphics-2d/cliff.toml
similarity index 100%
rename from packages/core-editor/cliff.toml
rename to modules/graphics-2d/cliff.toml
diff --git a/packages/core-editor/eslint.config.js b/modules/graphics-2d/eslint.config.js
similarity index 100%
rename from packages/core-editor/eslint.config.js
rename to modules/graphics-2d/eslint.config.js
diff --git a/packages/graphics-2d/mint-tsdocs.config.json b/modules/graphics-2d/mint-tsdocs.config.json
similarity index 100%
rename from packages/graphics-2d/mint-tsdocs.config.json
rename to modules/graphics-2d/mint-tsdocs.config.json
diff --git a/packages/graphics-2d/package.json b/modules/graphics-2d/package.json
similarity index 86%
rename from packages/graphics-2d/package.json
rename to modules/graphics-2d/package.json
index 54ba5e5f..e95d45fe 100644
--- a/packages/graphics-2d/package.json
+++ b/modules/graphics-2d/package.json
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "@nanoforge-dev/graphics-2d",
"version": "1.4.2",
- "description": "NanoForge Engine - Graphics-2d",
+ "description": "NanoForge Engine - Graphics 2D",
"keywords": [
"nanoforge",
"game",
@@ -13,8 +13,9 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips "
],
"files": [
@@ -43,7 +44,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/NanoForge-dev/Engine.git",
- "directory": "packages/graphics-2d"
+ "directory": "modules/graphics-2d"
},
"funding": "https://github.com/NanoForge-dev/Engine?sponsor",
"scripts": {
@@ -52,7 +53,7 @@
"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/graphics-2d/*'",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'modules/graphics-2d/*'",
"release": "cliff-jumper",
"docs": "mint-tsdocs generate"
},
@@ -62,6 +63,8 @@
},
"devDependencies": {
"@favware/cliff-jumper": "catalog:ci",
+ "@nanoforge-dev/ecs": "workspace:*",
+ "@nanoforge-dev/editor-lib": "workspace:*",
"@nanoforge-dev/utils-eslint-config": "workspace:*",
"@nanoforge-dev/utils-prettier-config": "workspace:*",
"@trivago/prettier-plugin-sort-imports": "catalog:lint",
@@ -72,9 +75,9 @@
"unrun": "catalog:build",
"vitest": "catalog:test"
},
- "packageManager": "pnpm@11.5.2",
+ "packageManager": "pnpm@12.0.0",
"engines": {
- "node": "25"
+ "node": "26"
},
"publishConfig": {
"access": "public"
diff --git a/packages/core-editor/prettier.config.js b/modules/graphics-2d/prettier.config.js
similarity index 100%
rename from packages/core-editor/prettier.config.js
rename to modules/graphics-2d/prettier.config.js
diff --git a/modules/graphics-2d/src/context-augmentation.ts b/modules/graphics-2d/src/context-augmentation.ts
new file mode 100644
index 00000000..d8928437
--- /dev/null
+++ b/modules/graphics-2d/src/context-augmentation.ts
@@ -0,0 +1,7 @@
+import type { GraphicsContextApi } from "./graphics-context.type";
+
+declare module "@nanoforge-dev/common" {
+ interface Context {
+ graphics: GraphicsContextApi;
+ }
+}
diff --git a/packages/graphics-2d/src/exports/konva.ts b/modules/graphics-2d/src/exports/konva.ts
similarity index 100%
rename from packages/graphics-2d/src/exports/konva.ts
rename to modules/graphics-2d/src/exports/konva.ts
diff --git a/modules/graphics-2d/src/graphics-2d.library.ts b/modules/graphics-2d/src/graphics-2d.library.ts
new file mode 100644
index 00000000..68fee906
--- /dev/null
+++ b/modules/graphics-2d/src/graphics-2d.library.ts
@@ -0,0 +1,118 @@
+import { type Context, type InitContext, Library, defineLibraryKey } from "@nanoforge-dev/common";
+// Side-effect-only: loads Context.editor/Context.ecs augmentations for the
+// optional editor-drag integration below. graphics-2d is client-only, so
+// it only ever coexists with @nanoforge-dev/ecs/client (never /server) in practice.
+import type { Registry } from "@nanoforge-dev/ecs/client";
+import type {} from "@nanoforge-dev/editor-lib";
+
+import * as Graphics from "./exports/konva";
+import type { GraphicsContextApi } from "./graphics-context.type";
+
+type DragSystemEditor = NonNullable;
+
+/**
+ * Built-in 2D graphics library powered by [Konva](https://konvajs.org/).
+ *
+ * @remarks
+ * Creates a full-container Konva `Stage` and a default `Layer` during
+ * `__init`. Game code interacts with `Context.graphics.stage`/`.baseLayer`
+ * directly to add shapes, images, and animations. Client-only — `__init`
+ * throws if `InitContext.container` is missing.
+ */
+export class Graphics2DLibrary extends Library {
+ readonly key = defineLibraryKey("graphics");
+
+ private _stage?: Graphics.Stage;
+ private _baseLayer?: Graphics.Layer;
+ private _editorDragWired = false;
+
+ public override async __init(ctx: InitContext): Promise {
+ if (!ctx.container) {
+ throw new Error(
+ "Graphics2DLibrary must be registered on the client (InitContext must contain a container element).",
+ );
+ }
+ this._stage = new Graphics.Stage({
+ container: ctx.container,
+ width: ctx.container.offsetWidth,
+ height: ctx.container.offsetHeight,
+ });
+ this._baseLayer = new Graphics.Layer();
+ this._stage.add(this._baseLayer);
+ }
+
+ public override async __clear(): Promise {
+ this._stage?.destroy();
+ delete (window as unknown as { Konva?: unknown }).Konva;
+ }
+
+ /**
+ * When both an editor bridge and ecs are registered, makes drawable
+ * shapes (`DrawableCircle2D`/`DrawableRect2D`/`DrawableText2D`) draggable
+ * in the viewport and notifies the editor on drag end. No-op otherwise —
+ * plain graphics-2d usage is entirely unaffected.
+ *
+ * @remarks
+ * One-time wiring lives in `__events` (always runs, even while paused),
+ * not `__run` — so it isn't blocked behind an app that starts paused.
+ */
+ public override async __events(ctx: Context): Promise {
+ if (!this._editorDragWired && ctx.editor && ctx.ecs) {
+ ctx.ecs.registry.addSystem(this._dragSystem(ctx.editor));
+ this._editorDragWired = true;
+ }
+ }
+
+ public get stage(): Graphics.Stage {
+ if (!this._stage) this.throwNotInitializedError();
+ return this._stage;
+ }
+
+ public get baseLayer(): Graphics.Layer {
+ if (!this._baseLayer) this.throwNotInitializedError();
+ return this._baseLayer;
+ }
+
+ public override expose(): GraphicsContextApi {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const library = this;
+ return {
+ get stage() {
+ return library.stage;
+ },
+ get baseLayer() {
+ return library.baseLayer;
+ },
+ };
+ }
+
+ private _dragSystem(editor: DragSystemEditor): (registry: Registry) => void {
+ const wiredComponents = new Map>();
+
+ return (registry: Registry) => {
+ const entities = [
+ ...registry.getZipper([{ name: "__RESERVED_entityId" }, { name: "DrawableCircle2D" }]),
+ ...registry.getZipper([{ name: "__RESERVED_entityId" }, { name: "DrawableRect2D" }]),
+ ...registry.getZipper([{ name: "__RESERVED_entityId" }, { name: "DrawableText2D" }]),
+ ];
+
+ for (const entry of entities as any[]) {
+ const { __RESERVED_entityId, DrawableCircle2D, DrawableRect2D, DrawableText2D } = entry;
+ const entityId = __RESERVED_entityId.entityId;
+
+ if (!wiredComponents.has(entityId)) wiredComponents.set(entityId, new Set());
+ const wired = wiredComponents.get(entityId) as Set;
+
+ for (const comp of [DrawableCircle2D, DrawableRect2D, DrawableText2D]) {
+ if (!comp || wired.has(comp.name)) continue;
+
+ comp.shape.draggable(true);
+ comp.shape.on("dragend", ({ target }: any) => {
+ editor.emit("move-component", entityId, comp.name, target._lastPos);
+ });
+ wired.add(comp.name);
+ }
+ }
+ };
+ }
+}
diff --git a/modules/graphics-2d/src/graphics-context.type.ts b/modules/graphics-2d/src/graphics-context.type.ts
new file mode 100644
index 00000000..ba578ba2
--- /dev/null
+++ b/modules/graphics-2d/src/graphics-context.type.ts
@@ -0,0 +1,9 @@
+import type * as Graphics from "./exports/konva";
+
+/** Public surface of `Graphics2DLibrary`, exposed on `Context.graphics`. */
+export interface GraphicsContextApi {
+ /** The Konva `Stage` covering the client container. */
+ readonly stage: Graphics.Stage;
+ /** The default Konva `Layer` added to the stage — add shapes here to render them. */
+ readonly baseLayer: Graphics.Layer;
+}
diff --git a/modules/graphics-2d/src/index.ts b/modules/graphics-2d/src/index.ts
new file mode 100644
index 00000000..16fc5bd1
--- /dev/null
+++ b/modules/graphics-2d/src/index.ts
@@ -0,0 +1,5 @@
+import "./context-augmentation";
+
+export * from "./exports/konva";
+export type { GraphicsContextApi } from "./graphics-context.type";
+export { Graphics2DLibrary } from "./graphics-2d.library";
diff --git a/modules/graphics-2d/test/graphics-2d.library.spec.ts b/modules/graphics-2d/test/graphics-2d.library.spec.ts
new file mode 100644
index 00000000..711c527e
--- /dev/null
+++ b/modules/graphics-2d/test/graphics-2d.library.spec.ts
@@ -0,0 +1,132 @@
+import type { InitContext } from "@nanoforge-dev/common";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { Graphics2DLibrary } from "../src";
+
+const makeInitContext = (container: HTMLDivElement | null | undefined): InitContext =>
+ ({
+ vars: { get: () => undefined, set: () => {} },
+ env: {},
+ files: new Map(),
+ container,
+ }) as InitContext;
+
+describe("Graphics2DLibrary", () => {
+ describe("metadata", () => {
+ it("should expose the reserved 'graphics' key", () => {
+ expect(new Graphics2DLibrary().key).toBe("graphics");
+ });
+ });
+
+ describe("before initialization", () => {
+ let library: Graphics2DLibrary;
+
+ beforeEach(() => {
+ library = new Graphics2DLibrary();
+ });
+
+ it("should throw when stage is accessed before __init", () => {
+ expect(() => library.stage).toThrow();
+ });
+
+ it("should throw when baseLayer is accessed before __init", () => {
+ expect(() => library.baseLayer).toThrow();
+ });
+
+ it("should throw when __init is called without a container", async () => {
+ await expect(library.__init(makeInitContext(null))).rejects.toThrow();
+ await expect(library.__init(makeInitContext(undefined))).rejects.toThrow();
+ });
+ });
+
+ describe("__clear", () => {
+ beforeEach(() => {
+ vi.stubGlobal("window", {});
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it("does not throw when called before __init", async () => {
+ await expect(new Graphics2DLibrary().__clear({} as any)).resolves.toBeUndefined();
+ });
+ });
+
+ describe("expose", () => {
+ it("throws through to the same not-initialized guard as the instance getters", () => {
+ const library = new Graphics2DLibrary();
+ expect(() => library.expose().stage).toThrow();
+ expect(() => library.expose().baseLayer).toThrow();
+ });
+ });
+
+ describe("editor drag integration", () => {
+ const makeShape = () => ({
+ draggable: vi.fn(),
+ on: vi.fn(),
+ });
+
+ const makeEntry = (entityId: string, shapes: Record>) => ({
+ __RESERVED_entityId: { entityId },
+ ...Object.fromEntries(Object.entries(shapes).map(([name, shape]) => [name, { name, shape }])),
+ });
+
+ it("does nothing when ctx.editor or ctx.ecs is absent", async () => {
+ const library = new Graphics2DLibrary();
+ await expect(
+ library.__events({ editor: undefined, ecs: undefined } as any),
+ ).resolves.toBeUndefined();
+ });
+
+ it("makes drawable shapes draggable and emits move-component on drag end, once per component", async () => {
+ const library = new Graphics2DLibrary();
+ const shape = makeShape();
+ const entries = [makeEntry("entity-1", { DrawableCircle2D: shape })];
+ const getZipper = vi.fn((components: { name: string }[]) =>
+ components.some((c) => c.name === "DrawableCircle2D") ? entries : [],
+ );
+ const addSystem = vi.fn();
+ const emit = vi.fn();
+
+ const ctx = {
+ editor: { emit, on: vi.fn() },
+ ecs: { registry: { getZipper, addSystem } },
+ } as any;
+
+ await library.__events(ctx);
+ expect(addSystem).toHaveBeenCalledOnce();
+
+ const system = addSystem.mock.calls[0]![0] as (registry: unknown) => void;
+ system({ getZipper });
+ expect(shape.draggable).toHaveBeenCalledWith(true);
+ expect(shape.on).toHaveBeenCalledWith("dragend", expect.any(Function));
+
+ const dragEndHandler = shape.on.mock.calls[0]![1] as (e: any) => void;
+ dragEndHandler({ target: { _lastPos: { x: 1, y: 2 } } });
+ expect(emit).toHaveBeenCalledWith("move-component", "entity-1", "DrawableCircle2D", {
+ x: 1,
+ y: 2,
+ });
+
+ // Running the system again for the same entity/component must not re-wire it.
+ system({ getZipper });
+ expect(shape.draggable).toHaveBeenCalledOnce();
+ expect(shape.on).toHaveBeenCalledOnce();
+ });
+
+ it("only wires the drag system once across multiple __events calls", async () => {
+ const library = new Graphics2DLibrary();
+ const addSystem = vi.fn();
+ const ctx = {
+ editor: { emit: vi.fn(), on: vi.fn() },
+ ecs: { registry: { getZipper: vi.fn(() => []), addSystem } },
+ } as any;
+
+ await library.__events(ctx);
+ await library.__events(ctx);
+
+ expect(addSystem).toHaveBeenCalledOnce();
+ });
+ });
+});
diff --git a/packages/core-editor/tsconfig.json b/modules/graphics-2d/tsconfig.json
similarity index 100%
rename from packages/core-editor/tsconfig.json
rename to modules/graphics-2d/tsconfig.json
diff --git a/packages/ecs-client/tsconfig.spec.json b/modules/graphics-2d/tsconfig.spec.json
similarity index 100%
rename from packages/ecs-client/tsconfig.spec.json
rename to modules/graphics-2d/tsconfig.spec.json
diff --git a/packages/core-editor/tsdoc.json b/modules/graphics-2d/tsdoc.json
similarity index 100%
rename from packages/core-editor/tsdoc.json
rename to modules/graphics-2d/tsdoc.json
diff --git a/packages/config/tsdown.config.ts b/modules/graphics-2d/tsdown.config.ts
similarity index 100%
rename from packages/config/tsdown.config.ts
rename to modules/graphics-2d/tsdown.config.ts
diff --git a/packages/input/.cliff-jumperrc.json b/modules/input/.cliff-jumperrc.json
similarity index 86%
rename from packages/input/.cliff-jumperrc.json
rename to modules/input/.cliff-jumperrc.json
index e77a22e6..c92fa0a1 100644
--- a/packages/input/.cliff-jumperrc.json
+++ b/modules/input/.cliff-jumperrc.json
@@ -2,7 +2,7 @@
"$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
"name": "input",
"org": "nanoforge-dev",
- "packagePath": "packages/input",
+ "packagePath": "modules/input",
"tagTemplate": "{{new-version}}",
"identifierBase": false
}
diff --git a/packages/network-client/.gitignore b/modules/input/.gitignore
similarity index 100%
rename from packages/network-client/.gitignore
rename to modules/input/.gitignore
diff --git a/packages/graphics-2d-editor/.prettierignore b/modules/input/.prettierignore
similarity index 100%
rename from packages/graphics-2d-editor/.prettierignore
rename to modules/input/.prettierignore
diff --git a/modules/input/CHANGELOG.md b/modules/input/CHANGELOG.md
new file mode 100644
index 00000000..6361e43e
--- /dev/null
+++ b/modules/input/CHANGELOG.md
@@ -0,0 +1,3 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
diff --git a/packages/ecs-client/LICENSE b/modules/input/LICENSE
similarity index 97%
rename from packages/ecs-client/LICENSE
rename to modules/input/LICENSE
index 62c6400b..46724861 100644
--- a/packages/ecs-client/LICENSE
+++ b/modules/input/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright © 2025 NanoForge
+Copyright © 2026 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
diff --git a/packages/input/README.md b/modules/input/README.md
similarity index 66%
rename from packages/input/README.md
rename to modules/input/README.md
index 8b0d0b41..e6ac6b5b 100644
--- a/packages/input/README.md
+++ b/modules/input/README.md
@@ -8,18 +8,18 @@
-
+
## About
-`@nanoforge-dev/input` is an input manager for handling keyboard and mouse events.
+`@nanoforge-dev/input` is NanoForge's built-in keyboard/mouse input library. It listens to browser keyboard and mouse events and exposes a per-frame snapshot of the current input state on `Context.input`. Client-only.
## Installation
-**Node.js 25 or newer is required.**
+**Node.js 26 or newer is required.**
```sh
npm install @nanoforge-dev/input
@@ -30,38 +30,34 @@ bun add @nanoforge-dev/input
## Example usage
-Initialize the library in your main file
+Register `InputLibrary` on your client app:
```ts
-import { type IRunClientOptions } from "@nanoforge-dev/common";
-import { NanoforgeFactory } from "@nanoforge-dev/core";
import { InputLibrary } from "@nanoforge-dev/input";
-export async function main(options: IRunClientOptions) {
- const app = NanoforgeFactory.createClient();
-
- const input = new InputLibrary();
-
- app.useInput(input);
-
- await app.init(options);
-
- await app.run();
-}
+app.use(new InputLibrary());
```
-Use to check for key presses
+Query the current input state from any library's `__run`:
```ts
-export function mySystem(registry: Registry, ctx: Context) {
- const input = ctx.libs.getInput();
+import { InputEnum } from "@nanoforge-dev/input";
+
+public override async __run(ctx: Context): Promise {
+ if (ctx.input.isKeyPressed(InputEnum.Space)) {
+ player.jump();
+ }
- const isArrowUpPressed = input.isKeyPressed(InputEnum.ArrowUp);
+ if (ctx.input.isDragging(InputEnum.MouseLeft)) {
+ camera.pan(ctx.input.getDragState());
+ }
- // Do something with the input
+ const { x, y } = ctx.input.getMousePosition();
}
```
+`Context.input` also exposes `getPressedKeys()`, `getMouseState()`, and `getWheelState()` for scroll input.
+
## Links
- [GitHub][source]
@@ -79,6 +75,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/input
+[source]: https://github.com/NanoForge-dev/Engine/tree/main/modules/input
[npm]: https://www.npmjs.com/package/@nanoforge-dev/input
[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md
diff --git a/packages/ecs-client/cliff.toml b/modules/input/cliff.toml
similarity index 100%
rename from packages/ecs-client/cliff.toml
rename to modules/input/cliff.toml
diff --git a/packages/ecs-client/eslint.config.js b/modules/input/eslint.config.js
similarity index 100%
rename from packages/ecs-client/eslint.config.js
rename to modules/input/eslint.config.js
diff --git a/packages/input/mint-tsdocs.config.json b/modules/input/mint-tsdocs.config.json
similarity index 100%
rename from packages/input/mint-tsdocs.config.json
rename to modules/input/mint-tsdocs.config.json
diff --git a/packages/input/package.json b/modules/input/package.json
similarity index 91%
rename from packages/input/package.json
rename to modules/input/package.json
index 591f296c..cb3657a0 100644
--- a/packages/input/package.json
+++ b/modules/input/package.json
@@ -13,8 +13,9 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips "
],
"files": [
@@ -43,7 +44,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/NanoForge-dev/Engine.git",
- "directory": "packages/input"
+ "directory": "modules/input"
},
"funding": "https://github.com/NanoForge-dev/Engine?sponsor",
"scripts": {
@@ -52,7 +53,7 @@
"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/input/*'",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'modules/input/*'",
"release": "cliff-jumper",
"docs": "mint-tsdocs generate"
},
@@ -71,9 +72,9 @@
"unrun": "catalog:build",
"vitest": "catalog:test"
},
- "packageManager": "pnpm@11.5.2",
+ "packageManager": "pnpm@12.0.0",
"engines": {
- "node": "25"
+ "node": "26"
},
"publishConfig": {
"access": "public"
diff --git a/packages/ecs-client/prettier.config.js b/modules/input/prettier.config.js
similarity index 100%
rename from packages/ecs-client/prettier.config.js
rename to modules/input/prettier.config.js
diff --git a/modules/input/src/context-augmentation.ts b/modules/input/src/context-augmentation.ts
new file mode 100644
index 00000000..b0044ce1
--- /dev/null
+++ b/modules/input/src/context-augmentation.ts
@@ -0,0 +1,7 @@
+import type { InputContextApi } from "./input-context.type";
+
+declare module "@nanoforge-dev/common" {
+ interface Context {
+ input: InputContextApi;
+ }
+}
diff --git a/modules/input/src/index.ts b/modules/input/src/index.ts
new file mode 100644
index 00000000..c3ea2db1
--- /dev/null
+++ b/modules/input/src/index.ts
@@ -0,0 +1,6 @@
+import "./context-augmentation";
+
+export { InputEnum } from "./input.enum";
+export type { InputContextApi } from "./input-context.type";
+export { InputLibrary } from "./input.library";
+export type { DragState, MouseState, WheelState } from "./mouse.types";
diff --git a/modules/input/src/input-context.type.ts b/modules/input/src/input-context.type.ts
new file mode 100644
index 00000000..c1b6f499
--- /dev/null
+++ b/modules/input/src/input-context.type.ts
@@ -0,0 +1,13 @@
+import type { InputEnum } from "./input.enum";
+import type { DragState, MouseState, WheelState } from "./mouse.types";
+
+/** Public surface of `InputLibrary`, exposed on `Context.input`. */
+export interface InputContextApi {
+ isKeyPressed(key: InputEnum): boolean | undefined;
+ getPressedKeys(): InputEnum[];
+ getMousePosition(): { x: number; y: number };
+ getMouseState(): MouseState;
+ isDragging(button?: InputEnum): boolean;
+ getDragState(): DragState;
+ getWheelState(): WheelState;
+}
diff --git a/modules/input/src/input-handler.ts b/modules/input/src/input-handler.ts
new file mode 100644
index 00000000..edc4ed40
--- /dev/null
+++ b/modules/input/src/input-handler.ts
@@ -0,0 +1,207 @@
+import { InputEnum } from "./input.enum";
+import {
+ BUTTONS_MASKS,
+ type DragState,
+ MOUSE_BUTTON_MAP,
+ type MouseState,
+ type WheelState,
+} from "./mouse.types";
+
+export class InputHandler {
+ private readonly offset: { x: number; y: number };
+ private readonly container: HTMLDivElement;
+ public inputs: Record = {};
+ public mouse: MouseState = {
+ x: 0,
+ y: 0,
+ prevX: 0,
+ prevY: 0,
+ deltaX: 0,
+ deltaY: 0,
+ focus: false,
+ };
+ public wheel: WheelState = {
+ deltaX: 0,
+ deltaY: 0,
+ deltaZ: 0,
+ };
+ public drag: DragState = {
+ active: false,
+ startX: 0,
+ startY: 0,
+ x: 0,
+ y: 0,
+ deltaX: 0,
+ deltaY: 0,
+ };
+
+ constructor(container: HTMLDivElement) {
+ this.container = container;
+ this.resetInputs();
+
+ this.offset = container.getBoundingClientRect();
+
+ window.addEventListener("keydown", this.onKeyDown);
+ window.addEventListener("keyup", this.onKeyUp);
+ container.addEventListener("mousedown", this.onMouseDown);
+ container.addEventListener("mouseup", this.onMouseUp);
+ container.addEventListener("mousemove", this.onMouseMove);
+ container.addEventListener("wheel", this.onWheel);
+ container.addEventListener("mouseenter", this.onMouseEnter);
+ container.addEventListener("mouseleave", this.onMouseLeave);
+ container.addEventListener("blur", this.onBlur);
+ container.addEventListener("visibilitychange", this.onVisibilityChange);
+
+ for (const key in InputEnum) {
+ this.inputs[key] = false;
+ }
+ }
+
+ /** Removes every listener registered by the constructor. */
+ public destroy(): void {
+ window.removeEventListener("keydown", this.onKeyDown);
+ window.removeEventListener("keyup", this.onKeyUp);
+ this.container.removeEventListener("mousedown", this.onMouseDown);
+ this.container.removeEventListener("mouseup", this.onMouseUp);
+ this.container.removeEventListener("mousemove", this.onMouseMove);
+ this.container.removeEventListener("wheel", this.onWheel);
+ this.container.removeEventListener("mouseenter", this.onMouseEnter);
+ this.container.removeEventListener("mouseleave", this.onMouseLeave);
+ this.container.removeEventListener("blur", this.onBlur);
+ this.container.removeEventListener("visibilitychange", this.onVisibilityChange);
+ }
+
+ public getKeyStatus(key: InputEnum): boolean {
+ return this.inputs[key] || false;
+ }
+
+ public getMousePosition(): { x: number; y: number } {
+ return { x: this.mouse.x, y: this.mouse.y };
+ }
+
+ public isDragging(button?: InputEnum): boolean {
+ if (!button) return this.drag.active;
+ return this.drag.active && this.drag.button === button;
+ }
+
+ public resetPerFrame(): void {
+ this.mouse.deltaX = 0;
+ this.mouse.deltaY = 0;
+ this.wheel.deltaX = 0;
+ this.wheel.deltaY = 0;
+ this.wheel.deltaZ = 0;
+ }
+
+ private readonly onKeyDown = (e: KeyboardEvent): void => {
+ this.inputs[e.code] = true;
+ };
+
+ private readonly onKeyUp = (e: KeyboardEvent): void => {
+ this.inputs[e.code] = false;
+ };
+
+ private readonly onMouseDown = (e: MouseEvent): void => {
+ const button = MOUSE_BUTTON_MAP[e.button];
+ if (button === undefined) return;
+
+ this.inputs[button] = true;
+ this.updatePointer(e);
+
+ this.drag.active = true;
+ this.drag.button = button;
+ this.drag.startX = e.clientX - this.offset.x;
+ this.drag.startY = e.clientY - this.offset.y;
+ this.drag.x = e.clientX - this.offset.x;
+ this.drag.y = e.clientY - this.offset.y;
+ this.drag.deltaX = 0;
+ this.drag.deltaY = 0;
+ };
+
+ private readonly onMouseUp = (e: MouseEvent): void => {
+ const button = MOUSE_BUTTON_MAP[e.button];
+ if (button !== undefined) this.inputs[button] = false;
+
+ this.updatePointer(e);
+
+ if (this.drag.button === button) {
+ this.drag.active = false;
+ this.drag.button = undefined;
+ this.drag.x = 0;
+ this.drag.y = 0;
+ this.drag.deltaX = 0;
+ this.drag.deltaY = 0;
+ }
+ };
+
+ private readonly onMouseMove = (e: MouseEvent): void => {
+ this.updatePointer(e);
+ this.updateInputsMouseButtons(e.buttons);
+
+ if (this.drag.active) {
+ this.drag.x = e.clientX - this.offset.x;
+ this.drag.y = e.clientY - this.offset.y;
+ this.drag.deltaX = e.clientX - this.drag.startX - this.offset.x;
+ this.drag.deltaY = e.clientY - this.drag.startY - this.offset.y;
+ }
+ };
+
+ private readonly onWheel = (e: WheelEvent): void => {
+ this.wheel.deltaX += e.deltaX;
+ this.wheel.deltaY += e.deltaY;
+ this.wheel.deltaZ += e.deltaZ;
+ };
+
+ private readonly onMouseEnter = (): void => {
+ this.mouse.focus = true;
+ };
+
+ private readonly onMouseLeave = (): void => {
+ this.mouse.focus = false;
+ };
+
+ private readonly onBlur = (): void => {
+ this.resetInputs();
+ };
+
+ private readonly onVisibilityChange = (): void => {
+ if (document.hidden) this.resetInputs();
+ };
+
+ private updatePointer(e: MouseEvent): void {
+ this.mouse.prevX = this.mouse.x;
+ this.mouse.prevY = this.mouse.y;
+ this.mouse.x = e.clientX - this.offset.x;
+ this.mouse.y = e.clientY - this.offset.y;
+ this.mouse.deltaX = this.mouse.x - this.mouse.prevX;
+ this.mouse.deltaY = this.mouse.y - this.mouse.prevY;
+ }
+
+ private updateInputsMouseButtons(buttons: number): void {
+ for (const [mask, input] of BUTTONS_MASKS) {
+ this.inputs[input] = (buttons & mask) !== 0;
+ }
+ }
+
+ private resetInputs(): void {
+ for (const key of Object.values(InputEnum)) {
+ this.inputs[key] = false;
+ }
+
+ this.drag.active = false;
+ this.drag.button = undefined;
+ this.drag.startX = 0;
+ this.drag.startY = 0;
+ this.drag.x = 0;
+ this.drag.y = 0;
+ this.drag.deltaX = 0;
+ this.drag.deltaY = 0;
+
+ this.wheel.deltaX = 0;
+ this.wheel.deltaY = 0;
+ this.wheel.deltaZ = 0;
+
+ this.mouse.deltaX = 0;
+ this.mouse.deltaY = 0;
+ this.mouse.focus = false;
+ }
+}
diff --git a/packages/input/src/input.enum.ts b/modules/input/src/input.enum.ts
similarity index 100%
rename from packages/input/src/input.enum.ts
rename to modules/input/src/input.enum.ts
diff --git a/modules/input/src/input.library.ts b/modules/input/src/input.library.ts
new file mode 100644
index 00000000..423cb62c
--- /dev/null
+++ b/modules/input/src/input.library.ts
@@ -0,0 +1,102 @@
+import { type InitContext, Library, defineLibraryKey } from "@nanoforge-dev/common";
+
+import type { InputContextApi } from "./input-context.type";
+import { InputHandler } from "./input-handler";
+import type { InputEnum } from "./input.enum";
+import type { DragState, MouseState, WheelState } from "./mouse.types";
+
+/**
+ * Built-in input library.
+ *
+ * @remarks
+ * Listens to browser `keydown`/`keyup` (on `window`) and `mousedown`/
+ * `mouseup`/`mousemove`/`wheel`/`mouseenter`/`mouseleave`/`blur`/
+ * `visibilitychange` (on the client container) and exposes a per-frame
+ * snapshot of the current input state on `Context.input`. Client-only —
+ * `__init` throws if `InitContext.container` is missing.
+ */
+export class InputLibrary extends Library {
+ readonly key = defineLibraryKey("input");
+
+ private _inputHandler?: InputHandler;
+
+ constructor() {
+ super({ runAfter: ["graphics"] });
+ }
+
+ public override async __init(ctx: InitContext): Promise {
+ if (!ctx.container) {
+ throw new Error(
+ "InputLibrary must be registered on the client (InitContext must contain a container element).",
+ );
+ }
+ this._inputHandler = new InputHandler(ctx.container);
+ }
+
+ public override async __run(): Promise {
+ if (!this._inputHandler) this.throwNotInitializedError();
+ this._inputHandler.resetPerFrame();
+ }
+
+ public override async __clear(): Promise {
+ this._inputHandler?.destroy();
+ }
+
+ /**
+ * @returns `true`/`false` for a held/released key, `undefined` if the key
+ * has never been seen.
+ */
+ public isKeyPressed(key: InputEnum): boolean | undefined {
+ if (!this._inputHandler) this.throwNotInitializedError();
+ return this._inputHandler.getKeyStatus(key);
+ }
+
+ public getPressedKeys(): InputEnum[] {
+ if (!this._inputHandler) this.throwNotInitializedError();
+ const pressed: InputEnum[] = [];
+ for (const rawKey in this._inputHandler.inputs) {
+ const key = rawKey as InputEnum;
+ if (this._inputHandler.getKeyStatus(key)) pressed.push(key);
+ }
+ return pressed;
+ }
+
+ public getMousePosition(): { x: number; y: number } {
+ if (!this._inputHandler) this.throwNotInitializedError();
+ return this._inputHandler.getMousePosition();
+ }
+
+ public getMouseState(): MouseState {
+ if (!this._inputHandler) this.throwNotInitializedError();
+ return this._inputHandler.mouse;
+ }
+
+ public isDragging(button?: InputEnum): boolean {
+ if (!this._inputHandler) this.throwNotInitializedError();
+ return this._inputHandler.isDragging(button);
+ }
+
+ public getDragState(): DragState {
+ if (!this._inputHandler) this.throwNotInitializedError();
+ return this._inputHandler.drag;
+ }
+
+ public getWheelState(): WheelState {
+ if (!this._inputHandler) this.throwNotInitializedError();
+ return this._inputHandler.wheel;
+ }
+
+ public override expose(): InputContextApi {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const library = this;
+ return {
+ isKeyPressed: (key) => library.isKeyPressed(key),
+ getPressedKeys: () => library.getPressedKeys(),
+ getMousePosition: () => library.getMousePosition(),
+ getMouseState: () => library.getMouseState(),
+ isDragging: (button) => library.isDragging(button),
+ getDragState: () => library.getDragState(),
+ getWheelState: () => library.getWheelState(),
+ };
+ }
+}
diff --git a/packages/input/src/mouse.types.ts b/modules/input/src/mouse.types.ts
similarity index 100%
rename from packages/input/src/mouse.types.ts
rename to modules/input/src/mouse.types.ts
diff --git a/modules/input/test/input.library.spec.ts b/modules/input/test/input.library.spec.ts
new file mode 100644
index 00000000..558b810c
--- /dev/null
+++ b/modules/input/test/input.library.spec.ts
@@ -0,0 +1,222 @@
+import type { InitContext } from "@nanoforge-dev/common";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import { InputEnum, InputLibrary } from "../src";
+
+type MockInputEvent = {
+ code?: string;
+ button?: number;
+ buttons?: number;
+ clientX?: number;
+ clientY?: number;
+ deltaX?: number;
+ deltaY?: number;
+ deltaZ?: number;
+};
+
+const makeEventTargetMock = () => {
+ const listeners: Record void)[]> = {};
+ return {
+ addEventListener: vi.fn((event: string, handler: (e: MockInputEvent) => void) => {
+ if (!listeners[event]) listeners[event] = [];
+ listeners[event].push(handler);
+ }),
+ removeEventListener: vi.fn((event: string, handler: (e: MockInputEvent) => void) => {
+ listeners[event] = (listeners[event] ?? []).filter((h) => h !== handler);
+ }),
+ dispatch: (event: string, e: Partial) => {
+ listeners[event]?.forEach((h) => h(e as MockInputEvent));
+ },
+ };
+};
+
+const makeInitContext = (container: unknown): InitContext =>
+ ({
+ vars: { get: () => undefined, set: () => {} },
+ env: {},
+ files: new Map(),
+ container,
+ }) as InitContext;
+
+describe("InputLibrary", () => {
+ let windowMock: ReturnType & {
+ getBoundingClientRect: () => { x: number; y: number };
+ };
+ let documentMock: ReturnType & { hidden: boolean };
+
+ beforeEach(() => {
+ windowMock = { ...makeEventTargetMock(), getBoundingClientRect: () => ({ x: 0, y: 0 }) };
+ documentMock = { ...makeEventTargetMock(), hidden: false };
+
+ vi.stubGlobal("window", windowMock);
+ vi.stubGlobal("document", documentMock);
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ describe("metadata", () => {
+ it("should expose the reserved 'input' key", () => {
+ expect(new InputLibrary().key).toBe("input");
+ });
+ });
+
+ describe("before initialization", () => {
+ it("should throw when methods are called before __init", () => {
+ const library = new InputLibrary();
+ expect(() => library.isKeyPressed(InputEnum.KeyA)).toThrow();
+ expect(() => library.getPressedKeys()).toThrow();
+ expect(() => library.getMousePosition()).toThrow();
+ expect(() => library.getMouseState()).toThrow();
+ expect(() => library.isDragging()).toThrow();
+ expect(() => library.getDragState()).toThrow();
+ expect(() => library.getWheelState()).toThrow();
+ });
+
+ it("should throw when __init is called without a container", async () => {
+ const library = new InputLibrary();
+ await expect(library.__init(makeInitContext(undefined))).rejects.toThrow();
+ });
+
+ it("should throw when __run is called before __init", async () => {
+ const library = new InputLibrary();
+ await expect(library.__run({} as any)).rejects.toThrow();
+ });
+ });
+
+ describe("after initialization", () => {
+ let library: InputLibrary;
+
+ beforeEach(async () => {
+ library = new InputLibrary();
+ await library.__init(makeInitContext(windowMock));
+ });
+
+ it("should register all expected event listeners", () => {
+ for (const event of [
+ "keydown",
+ "keyup",
+ "mousedown",
+ "mouseup",
+ "mousemove",
+ "wheel",
+ "mouseenter",
+ "mouseleave",
+ "blur",
+ "visibilitychange",
+ ]) {
+ expect(windowMock.addEventListener).toHaveBeenCalledWith(event, expect.any(Function));
+ }
+ });
+
+ it("should return false for any key before any key event", () => {
+ expect(library.isKeyPressed(InputEnum.KeyA)).toBe(false);
+ });
+
+ it("should return true for a key after keydown, then false after keyup", () => {
+ windowMock.dispatch("keydown", { code: InputEnum.KeyA });
+ expect(library.isKeyPressed(InputEnum.KeyA)).toBe(true);
+
+ windowMock.dispatch("keyup", { code: InputEnum.KeyA });
+ expect(library.isKeyPressed(InputEnum.KeyA)).toBe(false);
+ });
+
+ it("should list currently pressed keys", () => {
+ windowMock.dispatch("keydown", { code: InputEnum.KeyA });
+ windowMock.dispatch("keydown", { code: InputEnum.Space });
+ const pressed = library.getPressedKeys();
+ expect(pressed).toContain(InputEnum.KeyA);
+ expect(pressed).toContain(InputEnum.Space);
+ expect(pressed).toHaveLength(2);
+ });
+
+ it("should handle mouse button press and release", () => {
+ windowMock.dispatch("mousedown", { button: 0, clientX: 10, clientY: 20 });
+ expect(library.isKeyPressed(InputEnum.MouseLeft)).toBe(true);
+
+ windowMock.dispatch("mouseup", { button: 0, clientX: 10, clientY: 20 });
+ expect(library.isKeyPressed(InputEnum.MouseLeft)).toBe(false);
+ });
+
+ it("should update mouse position and delta on move", () => {
+ windowMock.dispatch("mousemove", { clientX: 100, clientY: 200, buttons: 0 });
+
+ expect(library.getMousePosition()).toStrictEqual({ x: 100, y: 200 });
+ expect(library.getMouseState()).toMatchObject({ x: 100, y: 200, deltaX: 100, deltaY: 200 });
+ });
+
+ it("should start, update and stop dragging", () => {
+ windowMock.dispatch("mousedown", { button: 0, clientX: 10, clientY: 20 });
+ expect(library.isDragging(InputEnum.MouseLeft)).toBe(true);
+
+ windowMock.dispatch("mousemove", { clientX: 25, clientY: 45, buttons: 1 });
+ expect(library.getDragState()).toMatchObject({ x: 25, y: 45, deltaX: 15, deltaY: 25 });
+
+ windowMock.dispatch("mouseup", { button: 0, clientX: 25, clientY: 45 });
+ expect(library.isDragging()).toBe(false);
+ });
+
+ it("should accumulate wheel delta and reset it on __run", async () => {
+ windowMock.dispatch("wheel", { deltaX: 1, deltaY: 2, deltaZ: 3 });
+ windowMock.dispatch("wheel", { deltaX: 4, deltaY: 5, deltaZ: 6 });
+ expect(library.getWheelState()).toStrictEqual({ deltaX: 5, deltaY: 7, deltaZ: 9 });
+
+ await library.__run({} as any);
+
+ expect(library.getWheelState()).toStrictEqual({ deltaX: 0, deltaY: 0, deltaZ: 0 });
+ });
+
+ it("should reset inputs on blur", () => {
+ windowMock.dispatch("keydown", { code: InputEnum.KeyA });
+ windowMock.dispatch("blur", {});
+ expect(library.isKeyPressed(InputEnum.KeyA)).toBe(false);
+ });
+
+ it("should reset inputs when document becomes hidden", () => {
+ windowMock.dispatch("keydown", { code: InputEnum.KeyA });
+ documentMock.hidden = true;
+ windowMock.dispatch("visibilitychange", {});
+ expect(library.isKeyPressed(InputEnum.KeyA)).toBe(false);
+ });
+
+ it("should update pointer focus on mouse enter and leave", () => {
+ windowMock.dispatch("mouseenter", {});
+ expect(library.getMouseState().focus).toBe(true);
+
+ windowMock.dispatch("mouseleave", {});
+ expect(library.getMouseState().focus).toBe(false);
+ });
+ });
+
+ describe("__clear", () => {
+ it("removes every registered listener", async () => {
+ const library = new InputLibrary();
+ await library.__init(makeInitContext(windowMock));
+
+ await library.__clear({} as any);
+
+ expect(windowMock.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
+ expect(windowMock.removeEventListener).toHaveBeenCalledWith("keyup", expect.any(Function));
+ expect(windowMock.removeEventListener).toHaveBeenCalledWith(
+ "mousedown",
+ expect.any(Function),
+ );
+ expect(windowMock.removeEventListener.mock.calls.length).toBeGreaterThanOrEqual(10);
+ });
+
+ it("does not throw when called before __init", async () => {
+ await expect(new InputLibrary().__clear({} as any)).resolves.toBeUndefined();
+ });
+ });
+
+ describe("expose", () => {
+ it("returns bound methods that behave like the instance methods", async () => {
+ const library = new InputLibrary();
+ await library.__init(makeInitContext(windowMock));
+ windowMock.dispatch("keydown", { code: InputEnum.KeyA });
+
+ expect(library.expose().isKeyPressed(InputEnum.KeyA)).toBe(true);
+ });
+ });
+});
diff --git a/packages/ecs-client/tsconfig.json b/modules/input/tsconfig.json
similarity index 100%
rename from packages/ecs-client/tsconfig.json
rename to modules/input/tsconfig.json
diff --git a/packages/ecs-lib/tsconfig.spec.json b/modules/input/tsconfig.spec.json
similarity index 100%
rename from packages/ecs-lib/tsconfig.spec.json
rename to modules/input/tsconfig.spec.json
diff --git a/packages/ecs-client/tsdoc.json b/modules/input/tsdoc.json
similarity index 100%
rename from packages/ecs-client/tsdoc.json
rename to modules/input/tsdoc.json
diff --git a/packages/core-editor/tsdown.config.ts b/modules/input/tsdown.config.ts
similarity index 100%
rename from packages/core-editor/tsdown.config.ts
rename to modules/input/tsdown.config.ts
diff --git a/packages/music/.cliff-jumperrc.json b/modules/music/.cliff-jumperrc.json
similarity index 86%
rename from packages/music/.cliff-jumperrc.json
rename to modules/music/.cliff-jumperrc.json
index c81ff1b4..40d327ec 100644
--- a/packages/music/.cliff-jumperrc.json
+++ b/modules/music/.cliff-jumperrc.json
@@ -2,7 +2,7 @@
"$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
"name": "music",
"org": "nanoforge-dev",
- "packagePath": "packages/music",
+ "packagePath": "modules/music",
"tagTemplate": "{{new-version}}",
"identifierBase": false
}
diff --git a/packages/network-server/.gitignore b/modules/music/.gitignore
similarity index 100%
rename from packages/network-server/.gitignore
rename to modules/music/.gitignore
diff --git a/packages/graphics-2d/.prettierignore b/modules/music/.prettierignore
similarity index 100%
rename from packages/graphics-2d/.prettierignore
rename to modules/music/.prettierignore
diff --git a/modules/music/CHANGELOG.md b/modules/music/CHANGELOG.md
new file mode 100644
index 00000000..6361e43e
--- /dev/null
+++ b/modules/music/CHANGELOG.md
@@ -0,0 +1,3 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
diff --git a/modules/music/LICENSE b/modules/music/LICENSE
new file mode 100644
index 00000000..46724861
--- /dev/null
+++ b/modules/music/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright © 2026 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/music/README.md b/modules/music/README.md
similarity index 63%
rename from packages/music/README.md
rename to modules/music/README.md
index 6cf40351..40e0b087 100644
--- a/packages/music/README.md
+++ b/modules/music/README.md
@@ -8,18 +8,18 @@
-
+
## About
-`@nanoforge-dev/music` is a music player for your game.
+`@nanoforge-dev/music` is NanoForge's built-in music library. It manages a collection of named `HTMLAudioElement` tracks exposed on `Context.music` and ensures only one track plays at a time — starting a new track pauses whichever one was playing.
## Installation
-**Node.js 25 or newer is required.**
+**Node.js 26 or newer is required.**
```sh
npm install @nanoforge-dev/music
@@ -30,44 +30,29 @@ bun add @nanoforge-dev/music
## Example usage
-Initialize the library in your main file
+Register `MusicLibrary` on your client app:
```ts
-import { AssetManagerLibrary } from "@nanoforge-dev/asset-manager";
-import { type IRunClientOptions } from "@nanoforge-dev/common";
-import { NanoforgeFactory } from "@nanoforge-dev/core";
import { MusicLibrary } from "@nanoforge-dev/music";
-export const MUSIC_SYM = Symbol("music");
-
-export async function main(options: IRunClientOptions) {
- const app = NanoforgeFactory.createClient();
-
- const assetManager = new AssetManagerLibrary();
- const music = new MusicLibrary();
-
- app.useAssetManager(assetManager);
- app.use(MUSIC_SYM, music);
-
- await app.init(options);
-
- music.load("music", assetManager.getAsset("/path/to/music.mp3").path);
-
- await app.run();
-}
+app.use(new MusicLibrary());
```
-Use to play music
+Load and play tracks through `Context.music`:
```ts
-export function mySystem(registry: Registry, ctx: Context) {
- const assetManager = ctx.libs.getAssetManager();
- const music = ctx.libs.get(MUSIC_SYM);
+public override async __init(): Promise {
+ ctx.music.load("menu", "/assets/menu.mp3");
+ ctx.music.load("level-1", "/assets/level-1.mp3");
+}
- music.play("music");
+public override async __run(ctx: Context): Promise {
+ if (levelStarted) ctx.music.play("level-1"); // stops "menu" automatically
}
```
+Call `ctx.music.mute()` to toggle the muted state of every loaded track. `play` throws `NfNotFound` when the given key was never loaded.
+
## Links
- [GitHub][source]
@@ -85,6 +70,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/music
+[source]: https://github.com/NanoForge-dev/Engine/tree/main/modules/music
[npm]: https://www.npmjs.com/package/@nanoforge-dev/music
[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md
diff --git a/packages/ecs-lib/cliff.toml b/modules/music/cliff.toml
similarity index 100%
rename from packages/ecs-lib/cliff.toml
rename to modules/music/cliff.toml
diff --git a/packages/ecs-server/eslint.config.js b/modules/music/eslint.config.js
similarity index 100%
rename from packages/ecs-server/eslint.config.js
rename to modules/music/eslint.config.js
diff --git a/packages/music/mint-tsdocs.config.json b/modules/music/mint-tsdocs.config.json
similarity index 100%
rename from packages/music/mint-tsdocs.config.json
rename to modules/music/mint-tsdocs.config.json
diff --git a/packages/music/package.json b/modules/music/package.json
similarity index 91%
rename from packages/music/package.json
rename to modules/music/package.json
index f667f9a6..cf504e05 100644
--- a/packages/music/package.json
+++ b/modules/music/package.json
@@ -13,8 +13,9 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips "
],
"files": [
@@ -43,7 +44,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/NanoForge-dev/Engine.git",
- "directory": "packages/music"
+ "directory": "modules/music"
},
"funding": "https://github.com/NanoForge-dev/Engine?sponsor",
"scripts": {
@@ -52,7 +53,7 @@
"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/music/*'",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'modules/music/*'",
"release": "cliff-jumper",
"docs": "mint-tsdocs generate"
},
@@ -71,9 +72,9 @@
"unrun": "catalog:build",
"vitest": "catalog:test"
},
- "packageManager": "pnpm@11.5.2",
+ "packageManager": "pnpm@12.0.0",
"engines": {
- "node": "25"
+ "node": "26"
},
"publishConfig": {
"access": "public"
diff --git a/packages/ecs-lib/prettier.config.js b/modules/music/prettier.config.js
similarity index 100%
rename from packages/ecs-lib/prettier.config.js
rename to modules/music/prettier.config.js
diff --git a/modules/music/src/context-augmentation.ts b/modules/music/src/context-augmentation.ts
new file mode 100644
index 00000000..7111a651
--- /dev/null
+++ b/modules/music/src/context-augmentation.ts
@@ -0,0 +1,7 @@
+import type { MusicContextApi } from "./music-context.type";
+
+declare module "@nanoforge-dev/common" {
+ interface Context {
+ music: MusicContextApi;
+ }
+}
diff --git a/modules/music/src/index.ts b/modules/music/src/index.ts
new file mode 100644
index 00000000..b6ebf51e
--- /dev/null
+++ b/modules/music/src/index.ts
@@ -0,0 +1,4 @@
+import "./context-augmentation";
+
+export type { MusicContextApi } from "./music-context.type";
+export { MusicLibrary } from "./music.library";
diff --git a/modules/music/src/music-context.type.ts b/modules/music/src/music-context.type.ts
new file mode 100644
index 00000000..d1d79af4
--- /dev/null
+++ b/modules/music/src/music-context.type.ts
@@ -0,0 +1,9 @@
+/** Public surface of `MusicLibrary`, exposed on `Context.music`. */
+export interface MusicContextApi {
+ /** Registers a music track under a unique key, loaded from `file`. */
+ load(key: string, file: string): void;
+ /** Stops the currently playing track (if any) and starts the requested one. */
+ play(key: string): void;
+ /** Toggles the muted state of every loaded track. */
+ mute(): void;
+}
diff --git a/modules/music/src/music.library.ts b/modules/music/src/music.library.ts
new file mode 100644
index 00000000..2ac36a09
--- /dev/null
+++ b/modules/music/src/music.library.ts
@@ -0,0 +1,81 @@
+import { Library, NfNotFound, defineLibraryKey } from "@nanoforge-dev/common";
+
+import type { MusicContextApi } from "./music-context.type";
+
+/**
+ * Built-in music library.
+ *
+ * @remarks
+ * Manages a collection of named `HTMLAudioElement` instances and ensures
+ * only one track plays at a time — unlike `SoundLibrary`, starting a new
+ * track pauses whichever one was playing.
+ */
+export class MusicLibrary extends Library {
+ readonly key = defineLibraryKey("music");
+
+ private _muted = true;
+ private _tracks?: Map;
+ private _current: HTMLAudioElement | null = null;
+
+ public override async __init(): Promise {
+ this._tracks = new Map();
+ this._muted = true;
+ this._current = null;
+ }
+
+ public override async __clear(): Promise {
+ this._current?.pause();
+ this._current = null;
+ for (const element of this._tracks?.values() ?? []) element.pause();
+ this._tracks?.clear();
+ }
+
+ /**
+ * Registers a music track under a unique key.
+ *
+ * @remarks
+ * Creates an `HTMLAudioElement` from the given URL. The track respects
+ * the current muted state at load time.
+ */
+ public load(key: string, file: string): void {
+ if (!this._tracks) this.throwNotInitializedError();
+ const element = new Audio(file);
+ element.muted = this._muted;
+ this._tracks.set(key, element);
+ }
+
+ /**
+ * @throws `NfNotFound` When no track is registered under the given key.
+ */
+ public play(key: string): void {
+ if (!this._tracks) this.throwNotInitializedError();
+ const element = this._tracks.get(key);
+ if (!element) {
+ this._current = null;
+ throw new NfNotFound(key);
+ }
+
+ this._current?.pause();
+ this._current = element;
+ this._current.play().catch((error: unknown) => {
+ console.error(`Got error: ${error}`);
+ });
+ }
+
+ /** Toggles the muted state of every loaded track. */
+ public mute(): void {
+ if (!this._tracks) this.throwNotInitializedError();
+ this._muted = !this._muted;
+ for (const element of this._tracks.values()) element.muted = this._muted;
+ }
+
+ public override expose(): MusicContextApi {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const library = this;
+ return {
+ load: (key, file) => library.load(key, file),
+ play: (key) => library.play(key),
+ mute: () => library.mute(),
+ };
+ }
+}
diff --git a/packages/music/test/music.library.spec.ts b/modules/music/test/music.library.spec.ts
similarity index 63%
rename from packages/music/test/music.library.spec.ts
rename to modules/music/test/music.library.spec.ts
index 56f2b4cd..1351793e 100644
--- a/packages/music/test/music.library.spec.ts
+++ b/modules/music/test/music.library.spec.ts
@@ -23,24 +23,16 @@ const makeMockAudio = () => {
describe("MusicLibrary", () => {
describe("metadata", () => {
- it("should expose the correct library name", () => {
- expect(new MusicLibrary().__name).toBe("NfMusic");
+ it("should expose the reserved 'music' key", () => {
+ expect(new MusicLibrary().key).toBe("music");
});
});
describe("before initialization", () => {
- it("should throw when play is called before __init", () => {
+ it("should throw when play/mute/load are called before __init", () => {
const library = new MusicLibrary();
expect(() => library.play("theme")).toThrow();
- });
-
- it("should throw when mute is called before __init", () => {
- const library = new MusicLibrary();
expect(() => library.mute()).toThrow();
- });
-
- it("should throw when load is called before __init", () => {
- const library = new MusicLibrary();
expect(() => library.load("theme", "theme.mp3")).toThrow();
});
});
@@ -64,11 +56,6 @@ describe("MusicLibrary", () => {
expect(() => library.play("unknown")).toThrow(NfNotFound);
});
- it("should play a loaded track without error", () => {
- library.load("theme", "theme.mp3");
- expect(() => library.play("theme")).not.toThrow();
- });
-
it("should call play on the audio element", () => {
library.load("theme", "theme.mp3");
library.play("theme");
@@ -83,14 +70,6 @@ describe("MusicLibrary", () => {
expect(mock.pause).toHaveBeenCalled();
});
- it("should toggle mute state for all loaded tracks", async () => {
- library.load("theme", "theme.mp3");
- library.load("battle", "battle.mp3");
- library.mute();
- library.load("credits", "credits.mp3");
- expect(() => library.play("credits")).not.toThrow();
- });
-
it("should load multiple tracks independently", () => {
library.load("theme", "theme.mp3");
library.load("battle", "battle.mp3");
@@ -98,9 +77,46 @@ describe("MusicLibrary", () => {
expect(() => library.play("battle")).not.toThrow();
});
- it("should throw NfNotFound after loading a different track", () => {
+ it("should clear the current track when play() fails", () => {
+ library.load("theme", "theme.mp3");
+ library.play("theme");
+ expect(() => library.play("unknown")).toThrow(NfNotFound);
+ });
+ });
+
+ describe("__clear", () => {
+ it("pauses the current track and clears every loaded track", async () => {
+ const mock = makeMockAudio();
+ vi.stubGlobal("Audio", mock.AudioClass);
+ const library = new MusicLibrary();
+ await library.__init();
library.load("theme", "theme.mp3");
- expect(() => library.play("credits")).toThrow(NfNotFound);
+ library.play("theme");
+
+ await library.__clear({} as any);
+
+ expect(mock.pause).toHaveBeenCalled();
+ expect(() => library.play("theme")).toThrow(NfNotFound);
+ vi.unstubAllGlobals();
+ });
+
+ it("does not throw when called before __init", async () => {
+ await expect(new MusicLibrary().__clear({} as any)).resolves.toBeUndefined();
+ });
+ });
+
+ describe("expose", () => {
+ it("returns methods that behave like the instance methods", async () => {
+ const mock = makeMockAudio();
+ vi.stubGlobal("Audio", mock.AudioClass);
+ const library = new MusicLibrary();
+ await library.__init();
+
+ library.expose().load("theme", "theme.mp3");
+ library.expose().play("theme");
+
+ expect(mock.play).toHaveBeenCalled();
+ vi.unstubAllGlobals();
});
});
});
diff --git a/packages/ecs-lib/tsconfig.json b/modules/music/tsconfig.json
similarity index 100%
rename from packages/ecs-lib/tsconfig.json
rename to modules/music/tsconfig.json
diff --git a/packages/ecs-server/tsconfig.spec.json b/modules/music/tsconfig.spec.json
similarity index 100%
rename from packages/ecs-server/tsconfig.spec.json
rename to modules/music/tsconfig.spec.json
diff --git a/packages/ecs-lib/tsdoc.json b/modules/music/tsdoc.json
similarity index 100%
rename from packages/ecs-lib/tsdoc.json
rename to modules/music/tsdoc.json
diff --git a/packages/ecs-client/tsdown.config.ts b/modules/music/tsdown.config.ts
similarity index 100%
rename from packages/ecs-client/tsdown.config.ts
rename to modules/music/tsdown.config.ts
diff --git a/modules/network/.cliff-jumperrc.json b/modules/network/.cliff-jumperrc.json
new file mode 100644
index 00000000..ebd62e89
--- /dev/null
+++ b/modules/network/.cliff-jumperrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
+ "name": "network",
+ "org": "nanoforge-dev",
+ "packagePath": "modules/network",
+ "tagTemplate": "{{new-version}}",
+ "identifierBase": false
+}
diff --git a/packages/graphics-2d/.gitignore b/modules/network/.gitignore
similarity index 93%
rename from packages/graphics-2d/.gitignore
rename to modules/network/.gitignore
index e56e932b..fee0a7b7 100644
--- a/packages/graphics-2d/.gitignore
+++ b/modules/network/.gitignore
@@ -12,40 +12,6 @@
# Built Visual Studio Code Extensions
*.vsix
-### C++ template
-# Prerequisites
-*.d
-
-# Compiled Object files
-*.slo
-*.lo
-*.o
-*.obj
-
-# Precompiled Headers
-*.gch
-*.pch
-
-# Compiled Dynamic libraries
-*.so
-*.dylib
-*.dll
-
-# Fortran module files
-*.mod
-*.smod
-
-# Compiled Static libraries
-*.lai
-*.la
-*.a
-*.lib
-
-# Executables
-*.exe
-*.out
-*.app
-
### 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
@@ -257,14 +223,13 @@ dist
.yarn/install-state.gz
.pnp.*
-### Private
-
# Turbo
.turbo/
# Compiled files
src/**/*.js
src/**/*.d.ts
+!src/server/wrtc.d.ts
# mint-tsdocs cache
docs/.tsdocs/
diff --git a/packages/input/.prettierignore b/modules/network/.prettierignore
similarity index 100%
rename from packages/input/.prettierignore
rename to modules/network/.prettierignore
diff --git a/modules/network/CHANGELOG.md b/modules/network/CHANGELOG.md
new file mode 100644
index 00000000..6361e43e
--- /dev/null
+++ b/modules/network/CHANGELOG.md
@@ -0,0 +1,3 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
diff --git a/modules/network/LICENSE b/modules/network/LICENSE
new file mode 100644
index 00000000..46724861
--- /dev/null
+++ b/modules/network/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright © 2026 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/modules/network/README.md b/modules/network/README.md
new file mode 100644
index 00000000..a4658513
--- /dev/null
+++ b/modules/network/README.md
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## About
+
+`@nanoforge-dev/network` is NanoForge's built-in networking library. It ships two subpath entry points — `@nanoforge-dev/network/client` and `@nanoforge-dev/network/server` — each providing reliable, ordered TCP (WebSocket) and unreliable, unordered UDP (WebRTC data channel) transports. Splitting client and server into separate entry points keeps Node-only dependencies (`ws`, `wrtc`) out of a client's bundle graph entirely.
+
+## Installation
+
+**Node.js 26 or newer is required.**
+
+```sh
+npm install @nanoforge-dev/network
+yarn add @nanoforge-dev/network
+pnpm add @nanoforge-dev/network
+bun add @nanoforge-dev/network
+```
+
+## Warning
+
+Configuration is read from the environment via `@nanoforge-dev/env`. Put the relevant variables in your `.env` file (or `InitContext.env`):
+
+```dotenv
+# Client
+SERVER_ADDRESS=127.0.0.1
+SERVER_TCP_PORT=4445
+SERVER_UDP_PORT=4444
+
+# Server
+LISTENING_INTERFACE=0.0.0.0
+LISTENING_TCP_PORT=4445
+LISTENING_UDP_PORT=4444
+```
+
+Either the TCP port or the UDP port (or both) must be set on each side.
+
+## Example usage
+
+```ts
+// Client app
+import { NetworkClientLibrary } from "@nanoforge-dev/network/client";
+
+app.use(new NetworkClientLibrary());
+
+public override async __run(ctx: Context): Promise {
+ ctx.network.tcp?.sendData(payload);
+ for (const packet of ctx.network.tcp?.getReceivedPackets() ?? []) {
+ // Handle incoming packets.
+ }
+}
+```
+
+```ts
+// Server app
+import { NetworkServerLibrary } from "@nanoforge-dev/network/server";
+
+app.use(new NetworkServerLibrary());
+
+public override async __run(ctx: Context): Promise {
+ for (const clientId of ctx.network.tcp?.getConnectedClients() ?? []) {
+ ctx.network.tcp?.sendToClient(clientId, payload);
+ }
+ ctx.network.tcp?.sendToEverybody(broadcastPayload);
+}
+```
+
+`ctx.network.tcp`/`.udp` are `undefined` when the corresponding port wasn't configured on that side.
+
+## 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/modules/network
+[npm]: https://www.npmjs.com/package/@nanoforge-dev/network
+[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md
diff --git a/modules/network/api-extractor.json b/modules/network/api-extractor.json
new file mode 100644
index 00000000..c24e7134
--- /dev/null
+++ b/modules/network/api-extractor.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+ "projectFolder": ".",
+ "mainEntryPointFilePath": "/dist/client/index.d.ts",
+ "bundledPackages": [],
+ "compiler": {
+ "tsconfigFilePath": "/tsconfig.client.json"
+ },
+ "apiReport": {
+ "enabled": false
+ },
+ "docModel": {
+ "enabled": true,
+ "apiJsonFilePath": "/docs/.tsdocs/.api.json"
+ },
+ "dtsRollup": {
+ "enabled": false
+ },
+ "tsdocMetadata": {
+ "enabled": true
+ },
+ "messages": {
+ "compilerMessageReporting": {
+ "default": {
+ "logLevel": "none"
+ }
+ },
+ "extractorMessageReporting": {
+ "default": {
+ "logLevel": "none"
+ }
+ },
+ "tsdocMessageReporting": {
+ "default": {
+ "logLevel": "none"
+ }
+ }
+ }
+}
diff --git a/packages/ecs-server/cliff.toml b/modules/network/cliff.toml
similarity index 100%
rename from packages/ecs-server/cliff.toml
rename to modules/network/cliff.toml
diff --git a/packages/graphics-2d-editor/eslint.config.js b/modules/network/eslint.config.js
similarity index 100%
rename from packages/graphics-2d-editor/eslint.config.js
rename to modules/network/eslint.config.js
diff --git a/modules/network/mint-tsdocs.config.json b/modules/network/mint-tsdocs.config.json
new file mode 100644
index 00000000..e80bbb97
--- /dev/null
+++ b/modules/network/mint-tsdocs.config.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "./node_modules/mint-tsdocs/lib/schemas/config.schema.json",
+ "entryPoint": "dist/client/index.d.ts",
+ "outputFolder": "../../docs/references/network",
+ "apiExtractor": {
+ "configPath": "./api-extractor.json",
+ "compiler": {
+ "tsconfigFilePath": "tsconfig.client.json"
+ }
+ },
+ "lint": {
+ "eslint": {
+ "enabled": true
+ }
+ },
+ "templates": {
+ "userTemplateDir": "../../docs/templates"
+ }
+}
diff --git a/packages/network-server/package.json b/modules/network/package.json
similarity index 66%
rename from packages/network-server/package.json
rename to modules/network/package.json
index 512f91b1..4035b491 100644
--- a/packages/network-server/package.json
+++ b/modules/network/package.json
@@ -1,8 +1,8 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "name": "@nanoforge-dev/network-server",
+ "name": "@nanoforge-dev/network",
"version": "1.4.2",
- "description": "NanoForge Engine - Network Server",
+ "description": "NanoForge Engine - Network",
"keywords": [
"nanoforge",
"game",
@@ -13,25 +13,36 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips "
],
"files": [
"dist"
],
- "main": "./dist/index.cjs",
- "module": "./dist/index.js",
- "types": "./dist/index.d.cts",
+ "main": "./dist/client/index.cjs",
+ "module": "./dist/client/index.js",
+ "types": "./dist/client/index.d.cts",
"exports": {
- ".": {
+ "./client": {
"require": {
- "types": "./dist/index.d.cts",
- "default": "./dist/index.cjs"
+ "types": "./dist/client/index.d.cts",
+ "default": "./dist/client/index.cjs"
},
"import": {
- "types": "./dist/index.d.ts",
- "default": "./dist/index.js"
+ "types": "./dist/client/index.d.ts",
+ "default": "./dist/client/index.js"
+ }
+ },
+ "./server": {
+ "require": {
+ "types": "./dist/server/index.d.cts",
+ "default": "./dist/server/index.cjs"
+ },
+ "import": {
+ "types": "./dist/server/index.d.ts",
+ "default": "./dist/server/index.js"
}
},
"./package.json": "./package.json"
@@ -43,23 +54,23 @@
"repository": {
"type": "git",
"url": "git+https://github.com/NanoForge-dev/Engine.git",
- "directory": "packages/network-server"
+ "directory": "modules/network"
},
"funding": "https://github.com/NanoForge-dev/Engine?sponsor",
"scripts": {
- "build": "tsc --noEmit && tsdown --config-loader unrun",
+ "build": "tsc -p tsconfig.client.json --noEmit && tsc -p tsconfig.server.json --noEmit && tsdown --config-loader unrun",
"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/network-server/*'",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'modules/network/*'",
"release": "cliff-jumper",
"docs": "mint-tsdocs generate"
},
"dependencies": {
"@mapbox/node-pre-gyp": "catalog:network",
"@nanoforge-dev/common": "workspace:*",
- "@nanoforge-dev/config": "workspace:*",
+ "@nanoforge-dev/env": "workspace:*",
"wrtc": "catalog:network",
"ws": "catalog:network"
},
@@ -76,9 +87,9 @@
"unrun": "catalog:build",
"vitest": "catalog:test"
},
- "packageManager": "pnpm@11.5.2",
+ "packageManager": "pnpm@12.0.0",
"engines": {
- "node": "25"
+ "node": "26"
},
"publishConfig": {
"access": "public"
diff --git a/packages/ecs-server/prettier.config.js b/modules/network/prettier.config.js
similarity index 100%
rename from packages/ecs-server/prettier.config.js
rename to modules/network/prettier.config.js
diff --git a/modules/network/src/client/client.network.library.ts b/modules/network/src/client/client.network.library.ts
new file mode 100644
index 00000000..19867381
--- /dev/null
+++ b/modules/network/src/client/client.network.library.ts
@@ -0,0 +1,74 @@
+import { type InitContext, Library, defineLibraryKey } from "@nanoforge-dev/common";
+import { registerEnv } from "@nanoforge-dev/env";
+
+import { ClientConfigNetwork } from "./config.client.network";
+import type { NetworkClientContextApi } from "./network-client-context.type";
+import { TCPClient } from "./tcp.client.network";
+import { UDPClient } from "./udp.client.network";
+
+/**
+ * Built-in network library for client-side applications.
+ *
+ * @remarks
+ * Reads network configuration from the environment via `ClientConfigNetwork`
+ * and automatically connects to the server over TCP (WebSocket), UDP
+ * (WebRTC data channel), or both.
+ *
+ * Configuration (via environment variables):
+ * - `SERVER_ADDRESS` — hostname or IP of the server (required).
+ * - `SERVER_TCP_PORT` — WebSocket port for TCP (optional).
+ * - `SERVER_UDP_PORT` — signaling port for UDP/WebRTC (optional).
+ * - `MAGIC_VALUE` — packet framing delimiter (default: `"PACKET_END"`).
+ * - `WSS` — set to `"true"` to use `wss://` / `https://` (default: `false`).
+ */
+export class NetworkClientLibrary extends Library {
+ readonly key = defineLibraryKey("network");
+
+ /** Only set when `SERVER_TCP_PORT` was configured. */
+ public tcp?: TCPClient;
+ /** Only set when `SERVER_UDP_PORT` was configured. */
+ public udp?: UDPClient;
+
+ public override async __init(ctx: InitContext): Promise {
+ const config = await registerEnv(ClientConfigNetwork, ctx.env);
+
+ if (config.SERVER_TCP_PORT === undefined && config.SERVER_UDP_PORT === undefined) {
+ throw new Error("NetworkClientLibrary: no server port specified to connect to.");
+ }
+
+ if (config.SERVER_TCP_PORT !== undefined) {
+ this.tcp = new TCPClient(
+ +config.SERVER_TCP_PORT,
+ config.SERVER_ADDRESS,
+ config.MAGIC_VALUE,
+ config.WSS,
+ );
+ await this.tcp.connect();
+ }
+
+ if (config.SERVER_UDP_PORT !== undefined) {
+ this.udp = new UDPClient(
+ +config.SERVER_UDP_PORT,
+ config.SERVER_ADDRESS,
+ config.MAGIC_VALUE,
+ config.WSS,
+ );
+ await this.udp.connect();
+ }
+ }
+
+ public override expose(): NetworkClientContextApi {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const library = this;
+ return {
+ get tcp() {
+ if (!library.tcp) throw new Error("TCP isn't defined");
+ return library.tcp;
+ },
+ get udp() {
+ if (!library.udp) throw new Error("UDP isn't defined");
+ return library.udp;
+ },
+ };
+ }
+}
diff --git a/packages/network-client/src/config.client.network.ts b/modules/network/src/client/config.client.network.ts
similarity index 83%
rename from packages/network-client/src/config.client.network.ts
rename to modules/network/src/client/config.client.network.ts
index e84edc03..9b953e0b 100644
--- a/packages/network-client/src/config.client.network.ts
+++ b/modules/network/src/client/config.client.network.ts
@@ -7,15 +7,13 @@ import {
IsOptional,
IsPort,
TransformToBoolean,
-} from "@nanoforge-dev/config";
+} from "@nanoforge-dev/env";
/**
* Environment-variable configuration for `NetworkClientLibrary`.
*
* @remarks
- * Resolved automatically by `IConfigRegistry.registerConfig` during `__init`.
- * Set these variables in the environment (or pass them via
- * `IRunClientOptions.env`) before running the application.
+ * Resolved via `registerEnv(ClientConfigNetwork, ctx.env)` during `__init`.
*/
export class ClientConfigNetwork {
/**
diff --git a/modules/network/src/client/context-augmentation.ts b/modules/network/src/client/context-augmentation.ts
new file mode 100644
index 00000000..110a2c7c
--- /dev/null
+++ b/modules/network/src/client/context-augmentation.ts
@@ -0,0 +1,7 @@
+import type { NetworkClientContextApi } from "./network-client-context.type";
+
+declare module "@nanoforge-dev/common" {
+ interface Context {
+ network: NetworkClientContextApi;
+ }
+}
diff --git a/packages/network-client/src/index.ts b/modules/network/src/client/index.ts
similarity index 50%
rename from packages/network-client/src/index.ts
rename to modules/network/src/client/index.ts
index f5016dc5..56c29bd9 100644
--- a/packages/network-client/src/index.ts
+++ b/modules/network/src/client/index.ts
@@ -1,3 +1,7 @@
+import "./context-augmentation";
+
export { NetworkClientLibrary } from "./client.network.library";
+export { ClientConfigNetwork } from "./config.client.network";
+export type { NetworkClientContextApi } from "./network-client-context.type";
export type { TCPClient } from "./tcp.client.network";
export type { UDPClient } from "./udp.client.network";
diff --git a/modules/network/src/client/network-client-context.type.ts b/modules/network/src/client/network-client-context.type.ts
new file mode 100644
index 00000000..fd102c0d
--- /dev/null
+++ b/modules/network/src/client/network-client-context.type.ts
@@ -0,0 +1,10 @@
+import type { TCPClient } from "./tcp.client.network";
+import type { UDPClient } from "./udp.client.network";
+
+/** Public surface of `NetworkClientLibrary`, exposed on `Context.network`. */
+export interface NetworkClientContextApi {
+ /** Reliable, ordered WebSocket connection. Only set when `SERVER_TCP_PORT` was configured. */
+ readonly tcp: TCPClient;
+ /** Unreliable, unordered WebRTC data-channel connection. Only set when `SERVER_UDP_PORT` was configured. */
+ readonly udp: UDPClient;
+}
diff --git a/packages/network-client/src/tcp.client.network.ts b/modules/network/src/client/tcp.client.network.ts
similarity index 100%
rename from packages/network-client/src/tcp.client.network.ts
rename to modules/network/src/client/tcp.client.network.ts
diff --git a/packages/network-client/src/udp.client.network.ts b/modules/network/src/client/udp.client.network.ts
similarity index 100%
rename from packages/network-client/src/udp.client.network.ts
rename to modules/network/src/client/udp.client.network.ts
diff --git a/packages/network-client/src/utils.ts b/modules/network/src/client/utils.ts
similarity index 100%
rename from packages/network-client/src/utils.ts
rename to modules/network/src/client/utils.ts
diff --git a/packages/network-server/src/config.server.network.ts b/modules/network/src/server/config.server.network.ts
similarity index 86%
rename from packages/network-server/src/config.server.network.ts
rename to modules/network/src/server/config.server.network.ts
index ec8c6ede..300bda37 100644
--- a/packages/network-server/src/config.server.network.ts
+++ b/modules/network/src/server/config.server.network.ts
@@ -6,15 +6,13 @@ import {
IsOptional,
IsPort,
IsString,
-} from "@nanoforge-dev/config";
+} from "@nanoforge-dev/env";
/**
* Environment-variable configuration for `NetworkServerLibrary`.
*
* @remarks
- * Resolved automatically by `IConfigRegistry.registerConfig` during `__init`.
- * Set these variables in the environment (or pass them via
- * `IRunServerOptions.env`) before running the application.
+ * Resolved via `registerEnv(ServerConfigNetwork, ctx.env)` during `__init`.
*/
export class ServerConfigNetwork {
/**
diff --git a/modules/network/src/server/context-augmentation.ts b/modules/network/src/server/context-augmentation.ts
new file mode 100644
index 00000000..426f9667
--- /dev/null
+++ b/modules/network/src/server/context-augmentation.ts
@@ -0,0 +1,7 @@
+import type { NetworkServerContextApi } from "./network-server-context.type";
+
+declare module "@nanoforge-dev/common" {
+ interface Context {
+ network: NetworkServerContextApi;
+ }
+}
diff --git a/packages/network-server/src/index.ts b/modules/network/src/server/index.ts
similarity index 50%
rename from packages/network-server/src/index.ts
rename to modules/network/src/server/index.ts
index 96a56a99..a749ff50 100644
--- a/packages/network-server/src/index.ts
+++ b/modules/network/src/server/index.ts
@@ -1,3 +1,7 @@
+import "./context-augmentation";
+
+export { ServerConfigNetwork } from "./config.server.network";
+export type { NetworkServerContextApi } from "./network-server-context.type";
export { NetworkServerLibrary } from "./server.network.library";
export type { TCPServer } from "./tcp.server.network";
export type { UDPServer } from "./udp.server.network";
diff --git a/modules/network/src/server/network-server-context.type.ts b/modules/network/src/server/network-server-context.type.ts
new file mode 100644
index 00000000..cf218c57
--- /dev/null
+++ b/modules/network/src/server/network-server-context.type.ts
@@ -0,0 +1,10 @@
+import type { TCPServer } from "./tcp.server.network";
+import type { UDPServer } from "./udp.server.network";
+
+/** Public surface of `NetworkServerLibrary`, exposed on `Context.network`. */
+export interface NetworkServerContextApi {
+ /** Reliable, ordered WebSocket server. Only set when `LISTENING_TCP_PORT` was configured. */
+ readonly tcp: TCPServer;
+ /** Unreliable, unordered WebRTC data-channel server. Only set when `LISTENING_UDP_PORT` was configured. */
+ readonly udp: UDPServer;
+}
diff --git a/packages/network-server/src/server.network.library.ts b/modules/network/src/server/server.network.library.ts
similarity index 50%
rename from packages/network-server/src/server.network.library.ts
rename to modules/network/src/server/server.network.library.ts
index a422970d..90be83be 100644
--- a/packages/network-server/src/server.network.library.ts
+++ b/modules/network/src/server/server.network.library.ts
@@ -1,6 +1,8 @@
-import { BaseNetworkLibrary, type InitContext, NfConfigException } from "@nanoforge-dev/common";
+import { type InitContext, Library, defineLibraryKey } from "@nanoforge-dev/common";
+import { registerEnv } from "@nanoforge-dev/env";
import { ServerConfigNetwork } from "./config.server.network";
+import type { NetworkServerContextApi } from "./network-server-context.type";
import { TCPServer } from "./tcp.server.network";
import { UDPServer } from "./udp.server.network";
@@ -8,13 +10,8 @@ import { UDPServer } from "./udp.server.network";
* Built-in network library for server-side applications.
*
* @remarks
- * Reads network configuration from the environment via
- * `ServerConfigNetwork` and starts TCP (WebSocket) and/or UDP (WebRTC)
- * servers. Register with:
- *
- * ```ts
- * server.useNetwork(new NetworkServerLibrary());
- * ```
+ * Reads network configuration from the environment via `ServerConfigNetwork`
+ * and starts TCP (WebSocket) and/or UDP (WebRTC) servers.
*
* Configuration (via environment variables):
* - `LISTENING_INTERFACE` — bind address (default: `"0.0.0.0"`).
@@ -22,47 +19,27 @@ import { UDPServer } from "./udp.server.network";
* - `LISTENING_UDP_PORT` — signaling listen port for UDP (optional).
* - `MAGIC_VALUE` — packet framing delimiter (default: `"PACKET_END"`).
* - `WSS_CERT` / `WSS_KEY` — paths to TLS certificate and key files for WSS (optional).
- *
- * After `init`, access the servers via `tcp` and `udp`.
*/
-export class NetworkServerLibrary extends BaseNetworkLibrary {
- /**
- * Unreliable, unordered WebRTC data-channel server for connected clients.
- *
- * @remarks
- * Only available when `LISTENING_UDP_PORT` was specified in the environment.
- */
- public udp!: UDPServer;
+export class NetworkServerLibrary extends Library {
+ readonly key = defineLibraryKey("network");
- /**
- * Reliable, ordered WebSocket-based server for connected clients.
- *
- * @remarks
- * Only available when `LISTENING_TCP_PORT` was specified in the environment.
- */
- public tcp!: TCPServer;
-
- /** @internal */
- get __name(): string {
- return "NetworkServerLibrary";
- }
+ /** Only set when `LISTENING_TCP_PORT` was configured. */
+ public tcp?: TCPServer;
+ /** Only set when `LISTENING_UDP_PORT` was configured. */
+ public udp?: UDPServer;
- /** @internal */
- public override async __init(context: InitContext): Promise {
- const config: ServerConfigNetwork = await context.config.registerConfig(ServerConfigNetwork);
+ public override async __init(ctx: InitContext): Promise {
+ const config = await registerEnv(ServerConfigNetwork, ctx.env);
- if (config.LISTENING_INTERFACE === undefined) {
- throw new NfConfigException("No listenning address provided", this.__name);
- }
if (config.LISTENING_TCP_PORT === undefined && config.LISTENING_UDP_PORT === undefined) {
- throw new NfConfigException("No listenning port specified", this.__name);
+ throw new Error("NetworkServerLibrary: no listening port specified.");
}
if (
(config.WSS_CERT !== undefined && config.WSS_KEY === undefined) ||
(config.WSS_CERT === undefined && config.WSS_KEY !== undefined)
) {
- throw new NfConfigException("Both cert and key must be provided together", this.__name);
+ throw new Error("NetworkServerLibrary: both WSS_CERT and WSS_KEY must be provided together.");
}
if (config.LISTENING_TCP_PORT !== undefined) {
@@ -87,4 +64,19 @@ export class NetworkServerLibrary extends BaseNetworkLibrary {
this.udp.listen();
}
}
+
+ public override expose(): NetworkServerContextApi {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const library = this;
+ return {
+ get tcp() {
+ if (!library.tcp) throw new Error("TCP isn't defined");
+ return library.tcp;
+ },
+ get udp() {
+ if (!library.udp) throw new Error("UDP isn't defined");
+ return library.udp;
+ },
+ };
+ }
}
diff --git a/packages/network-server/src/tcp.server.network.ts b/modules/network/src/server/tcp.server.network.ts
similarity index 100%
rename from packages/network-server/src/tcp.server.network.ts
rename to modules/network/src/server/tcp.server.network.ts
diff --git a/packages/network-server/src/udp.server.network.ts b/modules/network/src/server/udp.server.network.ts
similarity index 100%
rename from packages/network-server/src/udp.server.network.ts
rename to modules/network/src/server/udp.server.network.ts
diff --git a/packages/network-server/src/utils.ts b/modules/network/src/server/utils.ts
similarity index 100%
rename from packages/network-server/src/utils.ts
rename to modules/network/src/server/utils.ts
diff --git a/modules/network/src/server/wrtc.d.ts b/modules/network/src/server/wrtc.d.ts
new file mode 100644
index 00000000..5712fdf2
--- /dev/null
+++ b/modules/network/src/server/wrtc.d.ts
@@ -0,0 +1 @@
+declare module "wrtc";
diff --git a/packages/network-client/test/client.network.library.spec.ts b/modules/network/test/client/client.network.library.spec.ts
similarity index 63%
rename from packages/network-client/test/client.network.library.spec.ts
rename to modules/network/test/client/client.network.library.spec.ts
index cede52a9..c7ec8ef2 100644
--- a/packages/network-client/test/client.network.library.spec.ts
+++ b/modules/network/test/client/client.network.library.spec.ts
@@ -1,14 +1,13 @@
-import { type InitContext, NfConfigException } from "@nanoforge-dev/common";
+import type { InitContext } from "@nanoforge-dev/common";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { NetworkClientLibrary } from "../src";
+import { NetworkClientLibrary } from "../../src/client";
-const makeContext = (config: Record) =>
- ({
- config: {
- registerConfig: vi.fn().mockResolvedValue(config),
- },
- }) as unknown as InitContext;
+const makeInitContext = (env: Record): InitContext => ({
+ vars: { get: () => undefined, set: () => {} },
+ env,
+ files: new Map(),
+});
describe("NetworkClientLibrary", () => {
beforeEach(() => {
@@ -53,21 +52,21 @@ describe("NetworkClientLibrary", () => {
});
describe("metadata", () => {
- it("should expose the correct library name", () => {
- expect(new NetworkClientLibrary().__name).toBe("NetworkClientLibrary");
+ it("should expose the reserved 'network' key", () => {
+ expect(new NetworkClientLibrary().key).toBe("network");
});
});
describe("config validation", () => {
- it("should throw NfConfigException when neither TCP nor UDP port is provided", async () => {
- const ctx = makeContext({ SERVER_ADDRESS: "127.0.0.1", MAGIC_VALUE: "END" });
- await expect(new NetworkClientLibrary().__init(ctx)).rejects.toThrow(NfConfigException);
+ it("should throw when neither TCP nor UDP port is provided", async () => {
+ const ctx = makeInitContext({ SERVER_ADDRESS: "127.0.0.1", MAGIC_VALUE: "END" });
+ await expect(new NetworkClientLibrary().__init(ctx)).rejects.toThrow();
});
});
describe("initialization", () => {
it("should initialize a TCP client when only SERVER_TCP_PORT is provided", async () => {
- const ctx = makeContext({
+ const ctx = makeInitContext({
SERVER_TCP_PORT: "8080",
SERVER_ADDRESS: "127.0.0.1",
MAGIC_VALUE: "END",
@@ -79,7 +78,7 @@ describe("NetworkClientLibrary", () => {
});
it("should initialize a UDP client when only SERVER_UDP_PORT is provided", async () => {
- const ctx = makeContext({
+ const ctx = makeInitContext({
SERVER_UDP_PORT: "8081",
SERVER_ADDRESS: "127.0.0.1",
MAGIC_VALUE: "END",
@@ -91,7 +90,7 @@ describe("NetworkClientLibrary", () => {
});
it("should initialize both TCP and UDP clients when both ports are provided", async () => {
- const ctx = makeContext({
+ const ctx = makeInitContext({
SERVER_TCP_PORT: "8080",
SERVER_UDP_PORT: "8081",
SERVER_ADDRESS: "127.0.0.1",
@@ -102,5 +101,25 @@ describe("NetworkClientLibrary", () => {
expect(lib.tcp).toBeDefined();
expect(lib.udp).toBeDefined();
});
+
+ it("should default MAGIC_VALUE and WSS when not provided", async () => {
+ const ctx = makeInitContext({ SERVER_TCP_PORT: "8080", SERVER_ADDRESS: "127.0.0.1" });
+ const lib = new NetworkClientLibrary();
+ await expect(lib.__init(ctx)).resolves.toBeUndefined();
+ });
+ });
+
+ describe("expose", () => {
+ it("returns the tcp/udp clients", async () => {
+ const ctx = makeInitContext({
+ SERVER_TCP_PORT: "8080",
+ SERVER_ADDRESS: "127.0.0.1",
+ MAGIC_VALUE: "END",
+ });
+ const lib = new NetworkClientLibrary();
+ await lib.__init(ctx);
+ expect(lib.expose().tcp).toBe(lib.tcp);
+ expect(lib.expose().udp).toBeUndefined();
+ });
});
});
diff --git a/packages/network-client/test/tcp.client.spec.ts b/modules/network/test/client/tcp.client.spec.ts
similarity index 97%
rename from packages/network-client/test/tcp.client.spec.ts
rename to modules/network/test/client/tcp.client.spec.ts
index 7790c2cf..67f94733 100644
--- a/packages/network-client/test/tcp.client.spec.ts
+++ b/modules/network/test/client/tcp.client.spec.ts
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { TCPClient } from "../src/tcp.client.network";
+import { TCPClient } from "../../src/client/tcp.client.network";
const makeWsMock = (readyState = 0) => ({
readyState,
diff --git a/packages/network-client/test/udp.client.spec.ts b/modules/network/test/client/udp.client.spec.ts
similarity index 96%
rename from packages/network-client/test/udp.client.spec.ts
rename to modules/network/test/client/udp.client.spec.ts
index ae76a8ca..9bc9196f 100644
--- a/packages/network-client/test/udp.client.spec.ts
+++ b/modules/network/test/client/udp.client.spec.ts
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { UDPClient } from "../src/udp.client.network";
+import { UDPClient } from "../../src/client/udp.client.network";
describe("UDPClient", () => {
beforeEach(() => {
diff --git a/packages/network-client/test/utils.spec.ts b/modules/network/test/client/utils.spec.ts
similarity index 97%
rename from packages/network-client/test/utils.spec.ts
rename to modules/network/test/client/utils.spec.ts
index fb846764..4e0d9792 100644
--- a/packages/network-client/test/utils.spec.ts
+++ b/modules/network/test/client/utils.spec.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
-import { buildMagicPacket, parsePacketsFromChunks } from "../src/utils";
+import { buildMagicPacket, parsePacketsFromChunks } from "../../src/client/utils";
const magic = new TextEncoder().encode("END");
diff --git a/packages/network-server/test/server.network.library.spec.ts b/modules/network/test/server/server.network.library.spec.ts
similarity index 52%
rename from packages/network-server/test/server.network.library.spec.ts
rename to modules/network/test/server/server.network.library.spec.ts
index 4a0f1a84..63581d85 100644
--- a/packages/network-server/test/server.network.library.spec.ts
+++ b/modules/network/test/server/server.network.library.spec.ts
@@ -1,7 +1,7 @@
-import { type InitContext, NfConfigException } from "@nanoforge-dev/common";
+import type { InitContext } from "@nanoforge-dev/common";
import { afterEach, describe, expect, it, vi } from "vitest";
-import { NetworkServerLibrary } from "../src";
+import { NetworkServerLibrary } from "../../src/server";
vi.mock("ws", () => ({
WebSocketServer: vi.fn(function (this: any) {
@@ -18,12 +18,11 @@ vi.mock("wrtc", () => ({
}),
}));
-const makeContext = (config: Record) =>
- ({
- config: {
- registerConfig: vi.fn().mockResolvedValue(config),
- },
- }) as unknown as InitContext;
+const makeInitContext = (env: Record): InitContext => ({
+ vars: { get: () => undefined, set: () => {} },
+ env,
+ files: new Map(),
+});
describe("NetworkServerLibrary", () => {
afterEach(() => {
@@ -31,30 +30,26 @@ describe("NetworkServerLibrary", () => {
});
describe("metadata", () => {
- it("should expose the correct library name", () => {
- expect(new NetworkServerLibrary().__name).toBe("NetworkServerLibrary");
+ it("should expose the reserved 'network' key", () => {
+ expect(new NetworkServerLibrary().key).toBe("network");
});
});
describe("config validation", () => {
- it("should throw NfConfigException when LISTENING_INTERFACE is not provided", async () => {
- const ctx = makeContext({ LISTENING_TCP_PORT: "9000", MAGIC_VALUE: "END" });
- await expect(new NetworkServerLibrary().__init(ctx)).rejects.toThrow(NfConfigException);
+ it("should throw when neither TCP nor UDP port is provided", async () => {
+ const ctx = makeInitContext({ MAGIC_VALUE: "END" });
+ await expect(new NetworkServerLibrary().__init(ctx)).rejects.toThrow();
});
- it("should throw NfConfigException when neither TCP nor UDP port is provided", async () => {
- const ctx = makeContext({ LISTENING_INTERFACE: "0.0.0.0", MAGIC_VALUE: "END" });
- await expect(new NetworkServerLibrary().__init(ctx)).rejects.toThrow(NfConfigException);
+ it("should throw when only WSS_CERT is provided without WSS_KEY", async () => {
+ const ctx = makeInitContext({ LISTENING_TCP_PORT: "9000", WSS_CERT: "cert.pem" });
+ await expect(new NetworkServerLibrary().__init(ctx)).rejects.toThrow();
});
});
describe("initialization", () => {
it("should initialize a TCP server when only LISTENING_TCP_PORT is provided", async () => {
- const ctx = makeContext({
- LISTENING_TCP_PORT: "9000",
- LISTENING_INTERFACE: "0.0.0.0",
- MAGIC_VALUE: "END",
- });
+ const ctx = makeInitContext({ LISTENING_TCP_PORT: "9000", MAGIC_VALUE: "END" });
const lib = new NetworkServerLibrary();
await lib.__init(ctx);
expect(lib.tcp).toBeDefined();
@@ -62,11 +57,7 @@ describe("NetworkServerLibrary", () => {
});
it("should initialize a UDP server when only LISTENING_UDP_PORT is provided", async () => {
- const ctx = makeContext({
- LISTENING_UDP_PORT: "9001",
- LISTENING_INTERFACE: "0.0.0.0",
- MAGIC_VALUE: "END",
- });
+ const ctx = makeInitContext({ LISTENING_UDP_PORT: "9001", MAGIC_VALUE: "END" });
const lib = new NetworkServerLibrary();
await lib.__init(ctx);
expect(lib.udp).toBeDefined();
@@ -74,10 +65,9 @@ describe("NetworkServerLibrary", () => {
});
it("should initialize both TCP and UDP servers when both ports are provided", async () => {
- const ctx = makeContext({
+ const ctx = makeInitContext({
LISTENING_TCP_PORT: "9000",
LISTENING_UDP_PORT: "9001",
- LISTENING_INTERFACE: "0.0.0.0",
MAGIC_VALUE: "END",
});
const lib = new NetworkServerLibrary();
@@ -85,5 +75,21 @@ describe("NetworkServerLibrary", () => {
expect(lib.tcp).toBeDefined();
expect(lib.udp).toBeDefined();
});
+
+ it("should default LISTENING_INTERFACE to 0.0.0.0 and MAGIC_VALUE when not provided", async () => {
+ const ctx = makeInitContext({ LISTENING_TCP_PORT: "9000" });
+ const lib = new NetworkServerLibrary();
+ await expect(lib.__init(ctx)).resolves.toBeUndefined();
+ });
+ });
+
+ describe("expose", () => {
+ it("returns the tcp/udp servers", async () => {
+ const ctx = makeInitContext({ LISTENING_TCP_PORT: "9000" });
+ const lib = new NetworkServerLibrary();
+ await lib.__init(ctx);
+ expect(lib.expose().tcp).toBe(lib.tcp);
+ expect(lib.expose().udp).toBeUndefined();
+ });
});
});
diff --git a/packages/network-server/test/tcp.server.spec.ts b/modules/network/test/server/tcp.server.spec.ts
similarity index 98%
rename from packages/network-server/test/tcp.server.spec.ts
rename to modules/network/test/server/tcp.server.spec.ts
index 64a4a292..f506f484 100644
--- a/packages/network-server/test/tcp.server.spec.ts
+++ b/modules/network/test/server/tcp.server.spec.ts
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
-import { TCPServer } from "../src/tcp.server.network";
+import { TCPServer } from "../../src/server/tcp.server.network";
vi.mock("ws", () => ({
WebSocketServer: vi.fn(function (this: any) {
diff --git a/packages/network-server/test/udp.server.spec.ts b/modules/network/test/server/udp.server.spec.ts
similarity index 97%
rename from packages/network-server/test/udp.server.spec.ts
rename to modules/network/test/server/udp.server.spec.ts
index 7dd4ccf6..68bfd039 100644
--- a/packages/network-server/test/udp.server.spec.ts
+++ b/modules/network/test/server/udp.server.spec.ts
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
-import { UDPServer } from "../src/udp.server.network";
+import { UDPServer } from "../../src/server/udp.server.network";
vi.mock("ws", () => ({
WebSocketServer: vi.fn(function (this: any) {
diff --git a/packages/network-server/test/utils.spec.ts b/modules/network/test/server/utils.spec.ts
similarity index 97%
rename from packages/network-server/test/utils.spec.ts
rename to modules/network/test/server/utils.spec.ts
index eb08205d..e64312b8 100644
--- a/packages/network-server/test/utils.spec.ts
+++ b/modules/network/test/server/utils.spec.ts
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
-import { buildMagicPacket, parsePacketsFromChunks, rawDataToUint8Array } from "../src/utils";
+import {
+ buildMagicPacket,
+ parsePacketsFromChunks,
+ rawDataToUint8Array,
+} from "../../src/server/utils";
const magic = new TextEncoder().encode("END");
diff --git a/modules/network/tsconfig.client.json b/modules/network/tsconfig.client.json
new file mode 100644
index 00000000..22c56907
--- /dev/null
+++ b/modules/network/tsconfig.client.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ "extends": "./tsconfig.json",
+ "include": ["src/client/**/*.ts"],
+ "compilerOptions": {
+ "declaration": true
+ }
+}
diff --git a/packages/graphics-2d-editor/tsconfig.json b/modules/network/tsconfig.json
similarity index 100%
rename from packages/graphics-2d-editor/tsconfig.json
rename to modules/network/tsconfig.json
diff --git a/modules/network/tsconfig.server.json b/modules/network/tsconfig.server.json
new file mode 100644
index 00000000..cc4b2420
--- /dev/null
+++ b/modules/network/tsconfig.server.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig.json",
+ "extends": "./tsconfig.json",
+ "include": ["src/server/**/*.ts"],
+ "compilerOptions": {
+ "declaration": true
+ }
+}
diff --git a/packages/graphics-2d-editor/tsconfig.spec.json b/modules/network/tsconfig.spec.json
similarity index 100%
rename from packages/graphics-2d-editor/tsconfig.spec.json
rename to modules/network/tsconfig.spec.json
diff --git a/packages/ecs-server/tsdoc.json b/modules/network/tsdoc.json
similarity index 100%
rename from packages/ecs-server/tsdoc.json
rename to modules/network/tsdoc.json
diff --git a/modules/network/tsdown.config.ts b/modules/network/tsdown.config.ts
new file mode 100644
index 00000000..8985c61e
--- /dev/null
+++ b/modules/network/tsdown.config.ts
@@ -0,0 +1,14 @@
+import { createTsdownConfig } from "../../tsdown.config";
+
+export default [
+ createTsdownConfig({
+ entry: "src/client/index.ts",
+ outDir: "dist/client",
+ tsconfig: "tsconfig.client.json",
+ }),
+ createTsdownConfig({
+ entry: "src/server/index.ts",
+ outDir: "dist/server",
+ tsconfig: "tsconfig.server.json",
+ }),
+];
diff --git a/packages/sound/.cliff-jumperrc.json b/modules/sound/.cliff-jumperrc.json
similarity index 86%
rename from packages/sound/.cliff-jumperrc.json
rename to modules/sound/.cliff-jumperrc.json
index df5cffc2..7679c0e9 100644
--- a/packages/sound/.cliff-jumperrc.json
+++ b/modules/sound/.cliff-jumperrc.json
@@ -2,7 +2,7 @@
"$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
"name": "sound",
"org": "nanoforge-dev",
- "packagePath": "packages/sound",
+ "packagePath": "modules/sound",
"tagTemplate": "{{new-version}}",
"identifierBase": false
}
diff --git a/packages/graphics-2d-editor/.gitignore b/modules/sound/.gitignore
similarity index 93%
rename from packages/graphics-2d-editor/.gitignore
rename to modules/sound/.gitignore
index e56e932b..88ef62e8 100644
--- a/packages/graphics-2d-editor/.gitignore
+++ b/modules/sound/.gitignore
@@ -12,40 +12,6 @@
# Built Visual Studio Code Extensions
*.vsix
-### C++ template
-# Prerequisites
-*.d
-
-# Compiled Object files
-*.slo
-*.lo
-*.o
-*.obj
-
-# Precompiled Headers
-*.gch
-*.pch
-
-# Compiled Dynamic libraries
-*.so
-*.dylib
-*.dll
-
-# Fortran module files
-*.mod
-*.smod
-
-# Compiled Static libraries
-*.lai
-*.la
-*.a
-*.lib
-
-# Executables
-*.exe
-*.out
-*.app
-
### 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
@@ -257,8 +223,6 @@ dist
.yarn/install-state.gz
.pnp.*
-### Private
-
# Turbo
.turbo/
diff --git a/packages/music/.prettierignore b/modules/sound/.prettierignore
similarity index 100%
rename from packages/music/.prettierignore
rename to modules/sound/.prettierignore
diff --git a/modules/sound/CHANGELOG.md b/modules/sound/CHANGELOG.md
new file mode 100644
index 00000000..6361e43e
--- /dev/null
+++ b/modules/sound/CHANGELOG.md
@@ -0,0 +1,3 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
diff --git a/modules/sound/LICENSE b/modules/sound/LICENSE
new file mode 100644
index 00000000..46724861
--- /dev/null
+++ b/modules/sound/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright © 2026 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/sound/README.md b/modules/sound/README.md
similarity index 64%
rename from packages/sound/README.md
rename to modules/sound/README.md
index 63e83f28..f5495590 100644
--- a/packages/sound/README.md
+++ b/modules/sound/README.md
@@ -8,18 +8,18 @@
-
+
## About
-`@nanoforge-dev/sound` is a sound manager for your game.
+`@nanoforge-dev/sound` is NanoForge's built-in sound-effect library. It manages a collection of named `HTMLAudioElement` instances exposed on `Context.sound`. Unlike `@nanoforge-dev/music`, multiple sounds may overlap — there's no single-track exclusivity.
## Installation
-**Node.js 25 or newer is required.**
+**Node.js 26 or newer is required.**
```sh
npm install @nanoforge-dev/sound
@@ -30,42 +30,29 @@ bun add @nanoforge-dev/sound
## Example usage
-Initialize the library in your main file
+Register `SoundLibrary` on your client app:
```ts
-import { AssetManagerLibrary } from "@nanoforge-dev/asset-manager";
-import { type IRunClientOptions } from "@nanoforge-dev/common";
-import { NanoforgeFactory } from "@nanoforge-dev/core";
import { SoundLibrary } from "@nanoforge-dev/sound";
-export async function main(options: IRunClientOptions) {
- const app = NanoforgeFactory.createClient();
-
- const assetManager = new AssetManagerLibrary();
- const sound = new SoundLibrary();
-
- app.useAssetManager(assetManager);
- app.useSound(sound);
-
- await app.init(options);
-
- sound.load("sound", assetManager.getAssetPath("/path/to/sound.mp3").path);
-
- await app.run();
-}
+app.use(new SoundLibrary());
```
-Use to play sound
+Load and play sound effects through `Context.sound`:
```ts
-export function mySystem(registry: Registry, ctx: Context) {
- const assetManager = ctx.libs.getAssetManager();
- const sound = ctx.libs.getSound();
+public override async __init(): Promise {
+ ctx.sound.load("jump", "/assets/jump.wav");
+ ctx.sound.load("hit", "/assets/hit.wav");
+}
- sound.play("sound");
+public override async __run(ctx: Context): Promise {
+ if (playerJumped) ctx.sound.play("jump");
}
```
+Call `ctx.sound.mute()` to toggle the muted state of every loaded sound effect. `play` throws `NfNotFound` when the given key was never loaded.
+
## Links
- [GitHub][source]
@@ -83,6 +70,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/sound
+[source]: https://github.com/NanoForge-dev/Engine/tree/main/modules/sound
[npm]: https://www.npmjs.com/package/@nanoforge-dev/sound
[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md
diff --git a/packages/graphics-2d-editor/cliff.toml b/modules/sound/cliff.toml
similarity index 100%
rename from packages/graphics-2d-editor/cliff.toml
rename to modules/sound/cliff.toml
diff --git a/packages/graphics-2d/eslint.config.js b/modules/sound/eslint.config.js
similarity index 100%
rename from packages/graphics-2d/eslint.config.js
rename to modules/sound/eslint.config.js
diff --git a/packages/sound/mint-tsdocs.config.json b/modules/sound/mint-tsdocs.config.json
similarity index 100%
rename from packages/sound/mint-tsdocs.config.json
rename to modules/sound/mint-tsdocs.config.json
diff --git a/packages/sound/package.json b/modules/sound/package.json
similarity index 91%
rename from packages/sound/package.json
rename to modules/sound/package.json
index e72f2c6d..e517761f 100644
--- a/packages/sound/package.json
+++ b/modules/sound/package.json
@@ -13,8 +13,9 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips "
],
"files": [
@@ -43,7 +44,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/NanoForge-dev/Engine.git",
- "directory": "packages/sound"
+ "directory": "modules/sound"
},
"funding": "https://github.com/NanoForge-dev/Engine?sponsor",
"scripts": {
@@ -52,7 +53,7 @@
"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/sound/*'",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'modules/sound/*'",
"release": "cliff-jumper",
"docs": "mint-tsdocs generate"
},
@@ -71,9 +72,9 @@
"unrun": "catalog:build",
"vitest": "catalog:test"
},
- "packageManager": "pnpm@11.5.2",
+ "packageManager": "pnpm@12.0.0",
"engines": {
- "node": "25"
+ "node": "26"
},
"publishConfig": {
"access": "public"
diff --git a/packages/graphics-2d-editor/prettier.config.js b/modules/sound/prettier.config.js
similarity index 100%
rename from packages/graphics-2d-editor/prettier.config.js
rename to modules/sound/prettier.config.js
diff --git a/modules/sound/src/context-augmentation.ts b/modules/sound/src/context-augmentation.ts
new file mode 100644
index 00000000..25325310
--- /dev/null
+++ b/modules/sound/src/context-augmentation.ts
@@ -0,0 +1,7 @@
+import type { SoundContextApi } from "./sound-context.type";
+
+declare module "@nanoforge-dev/common" {
+ interface Context {
+ sound: SoundContextApi;
+ }
+}
diff --git a/modules/sound/src/index.ts b/modules/sound/src/index.ts
new file mode 100644
index 00000000..14d89656
--- /dev/null
+++ b/modules/sound/src/index.ts
@@ -0,0 +1,4 @@
+import "./context-augmentation";
+
+export type { SoundContextApi } from "./sound-context.type";
+export { SoundLibrary } from "./sound.library";
diff --git a/modules/sound/src/sound-context.type.ts b/modules/sound/src/sound-context.type.ts
new file mode 100644
index 00000000..d967516d
--- /dev/null
+++ b/modules/sound/src/sound-context.type.ts
@@ -0,0 +1,9 @@
+/** Public surface of `SoundLibrary`, exposed on `Context.sound`. */
+export interface SoundContextApi {
+ /** Registers a sound effect under a unique key, loaded from `file`. */
+ load(key: string, file: string): void;
+ /** Plays a previously loaded sound effect. Multiple sounds may overlap. */
+ play(key: string): void;
+ /** Toggles the muted state of every loaded sound effect. */
+ mute(): void;
+}
diff --git a/modules/sound/src/sound.library.ts b/modules/sound/src/sound.library.ts
new file mode 100644
index 00000000..5d32c713
--- /dev/null
+++ b/modules/sound/src/sound.library.ts
@@ -0,0 +1,72 @@
+import { Library, NfNotFound, defineLibraryKey } from "@nanoforge-dev/common";
+
+import type { SoundContextApi } from "./sound-context.type";
+
+/**
+ * Built-in sound-effect library.
+ *
+ * @remarks
+ * Manages a collection of named `HTMLAudioElement` instances. Load sounds
+ * with `load` and trigger playback with `play`. Unlike `MusicLibrary`,
+ * multiple sounds may overlap — there's no single-track exclusivity.
+ */
+export class SoundLibrary extends Library {
+ readonly key = defineLibraryKey("sound");
+
+ private _muted = true;
+ private _sounds?: Map;
+
+ public override async __init(): Promise {
+ this._sounds = new Map();
+ this._muted = true;
+ }
+
+ public override async __clear(): Promise {
+ for (const element of this._sounds?.values() ?? []) element.pause();
+ this._sounds?.clear();
+ }
+
+ /**
+ * Registers a sound effect under a unique key.
+ *
+ * @remarks
+ * Creates an `HTMLAudioElement` from the given URL. The sound respects
+ * the current muted state at load time.
+ */
+ public load(key: string, file: string): void {
+ if (!this._sounds) this.throwNotInitializedError();
+ const element = new Audio(file);
+ element.muted = this._muted;
+ this._sounds.set(key, element);
+ }
+
+ /**
+ * @throws `NfNotFound` When no sound is registered under the given key.
+ */
+ public play(key: string): void {
+ if (!this._sounds) this.throwNotInitializedError();
+ const element = this._sounds.get(key);
+ if (!element) throw new NfNotFound(key);
+
+ element.play().catch((error: unknown) => {
+ console.error(`Got error: ${error}`);
+ });
+ }
+
+ /** Toggles the muted state of every loaded sound effect. */
+ public mute(): void {
+ if (!this._sounds) this.throwNotInitializedError();
+ this._muted = !this._muted;
+ for (const element of this._sounds.values()) element.muted = this._muted;
+ }
+
+ public override expose(): SoundContextApi {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const library = this;
+ return {
+ load: (key, file) => library.load(key, file),
+ play: (key) => library.play(key),
+ mute: () => library.mute(),
+ };
+ }
+}
diff --git a/packages/sound/test/sound.library.spec.ts b/modules/sound/test/sound.library.spec.ts
similarity index 56%
rename from packages/sound/test/sound.library.spec.ts
rename to modules/sound/test/sound.library.spec.ts
index da5725c2..37493c67 100644
--- a/packages/sound/test/sound.library.spec.ts
+++ b/modules/sound/test/sound.library.spec.ts
@@ -23,24 +23,16 @@ const makeMockAudio = () => {
describe("SoundLibrary", () => {
describe("metadata", () => {
- it("should expose the correct library name", () => {
- expect(new SoundLibrary().__name).toBe("NfSound");
+ it("should expose the reserved 'sound' key", () => {
+ expect(new SoundLibrary().key).toBe("sound");
});
});
describe("before initialization", () => {
- it("should throw when play is called before __init", () => {
+ it("should throw when play/mute/load are called before __init", () => {
const library = new SoundLibrary();
expect(() => library.play("click")).toThrow();
- });
-
- it("should throw when mute is called before __init", () => {
- const library = new SoundLibrary();
expect(() => library.mute()).toThrow();
- });
-
- it("should throw when load is called before __init", () => {
- const library = new SoundLibrary();
expect(() => library.load("click", "click.mp3")).toThrow();
});
});
@@ -64,34 +56,67 @@ describe("SoundLibrary", () => {
expect(() => library.play("unknown")).toThrow(NfNotFound);
});
- it("should play a loaded sound without error", () => {
- library.load("click", "click.mp3");
- expect(() => library.play("click")).not.toThrow();
- });
-
it("should call play on the audio element", () => {
library.load("click", "click.mp3");
library.play("click");
expect(mock.play).toHaveBeenCalled();
});
- it("should start muted and toggle mute state", async () => {
+ it("should load multiple sounds independently, without exclusivity", () => {
library.load("click", "click.mp3");
- library.mute();
+ library.load("boom", "boom.mp3");
+ expect(() => library.play("click")).not.toThrow();
+ expect(() => library.play("boom")).not.toThrow();
+ expect(mock.pause).not.toHaveBeenCalled();
+ });
+
+ it("should apply the current muted state to newly loaded sounds", () => {
+ library.mute(); // now unmuted (starts muted)
library.load("beep", "beep.mp3");
- expect(() => library.play("beep")).not.toThrow();
+ expect(mock.AudioClass).toBeTruthy();
});
- it("should load multiple sounds independently", () => {
+ it("should toggle mute for every already-loaded sound", () => {
library.load("click", "click.mp3");
- library.load("boom", "boom.mp3");
+ library.mute();
+ library.mute();
+ // two toggles returns to the original (muted) state — no throw is the contract here.
expect(() => library.play("click")).not.toThrow();
- expect(() => library.play("boom")).not.toThrow();
});
+ });
- it("should throw NfNotFound after loading a different sound", () => {
+ describe("__clear", () => {
+ it("pauses and clears every loaded sound", async () => {
+ const mock = makeMockAudio();
+ vi.stubGlobal("Audio", mock.AudioClass);
+ const library = new SoundLibrary();
+ await library.__init();
library.load("click", "click.mp3");
- expect(() => library.play("boom")).toThrow(NfNotFound);
+
+ await library.__clear({} as any);
+
+ expect(mock.pause).toHaveBeenCalled();
+ expect(() => library.play("click")).toThrow(NfNotFound);
+ vi.unstubAllGlobals();
+ });
+
+ it("does not throw when called before __init", async () => {
+ await expect(new SoundLibrary().__clear({} as any)).resolves.toBeUndefined();
+ });
+ });
+
+ describe("expose", () => {
+ it("returns methods that behave like the instance methods", async () => {
+ const mock = makeMockAudio();
+ vi.stubGlobal("Audio", mock.AudioClass);
+ const library = new SoundLibrary();
+ await library.__init();
+
+ library.expose().load("click", "click.mp3");
+ library.expose().play("click");
+
+ expect(mock.play).toHaveBeenCalled();
+ vi.unstubAllGlobals();
});
});
});
diff --git a/packages/graphics-2d/tsconfig.json b/modules/sound/tsconfig.json
similarity index 100%
rename from packages/graphics-2d/tsconfig.json
rename to modules/sound/tsconfig.json
diff --git a/packages/graphics-2d/tsconfig.spec.json b/modules/sound/tsconfig.spec.json
similarity index 100%
rename from packages/graphics-2d/tsconfig.spec.json
rename to modules/sound/tsconfig.spec.json
diff --git a/packages/graphics-2d-editor/tsdoc.json b/modules/sound/tsdoc.json
similarity index 100%
rename from packages/graphics-2d-editor/tsdoc.json
rename to modules/sound/tsdoc.json
diff --git a/packages/ecs-lib/tsdown.config.ts b/modules/sound/tsdown.config.ts
similarity index 100%
rename from packages/ecs-lib/tsdown.config.ts
rename to modules/sound/tsdown.config.ts
diff --git a/package.json b/package.json
index acbbd933..c02f80b4 100644
--- a/package.json
+++ b/package.json
@@ -8,8 +8,9 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips "
],
"type": "module",
@@ -48,9 +49,9 @@
"typescript": "catalog:core",
"vitest": "catalog:test"
},
- "packageManager": "pnpm@11.9.0",
+ "packageManager": "pnpm@12.0.0",
"engines": {
- "node": "25"
+ "node": "26"
},
"private": true,
"lint-staged": {
diff --git a/packages/asset-manager/.cliff-jumperrc.json b/packages/asset-manager/.cliff-jumperrc.json
deleted file mode 100644
index 86143c44..00000000
--- a/packages/asset-manager/.cliff-jumperrc.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
- "name": "asset-manager",
- "org": "nanoforge-dev",
- "packagePath": "packages/asset-manager",
- "tagTemplate": "{{new-version}}",
- "identifierBase": false
-}
diff --git a/packages/asset-manager/.idea/.gitignore b/packages/asset-manager/.idea/.gitignore
deleted file mode 100644
index 13566b81..00000000
--- a/packages/asset-manager/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/packages/asset-manager/.idea/.name b/packages/asset-manager/.idea/.name
deleted file mode 100644
index 01043415..00000000
--- a/packages/asset-manager/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-[NanoForge] Engine Asset Manager
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/[NanoForge] Engine Asset Manager.iml b/packages/asset-manager/.idea/[NanoForge] Engine Asset Manager.iml
deleted file mode 100644
index 24643cc3..00000000
--- a/packages/asset-manager/.idea/[NanoForge] Engine Asset Manager.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/codeStyles/Project.xml b/packages/asset-manager/.idea/codeStyles/Project.xml
deleted file mode 100644
index a57ead77..00000000
--- a/packages/asset-manager/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,172 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/codeStyles/codeStyleConfig.xml b/packages/asset-manager/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123c..00000000
--- a/packages/asset-manager/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/editor.xml b/packages/asset-manager/.idea/editor.xml
deleted file mode 100644
index d9ec4dfd..00000000
--- a/packages/asset-manager/.idea/editor.xml
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/git_toolbox_blame.xml b/packages/asset-manager/.idea/git_toolbox_blame.xml
deleted file mode 100644
index 7dc12496..00000000
--- a/packages/asset-manager/.idea/git_toolbox_blame.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/git_toolbox_prj.xml b/packages/asset-manager/.idea/git_toolbox_prj.xml
deleted file mode 100644
index 02b915b8..00000000
--- a/packages/asset-manager/.idea/git_toolbox_prj.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/inspectionProfiles/Project_Default.xml b/packages/asset-manager/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 41ec19c1..00000000
--- a/packages/asset-manager/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/jsLinters/eslint.xml b/packages/asset-manager/.idea/jsLinters/eslint.xml
deleted file mode 100644
index 541945bb..00000000
--- a/packages/asset-manager/.idea/jsLinters/eslint.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/misc.xml b/packages/asset-manager/.idea/misc.xml
deleted file mode 100644
index 7ecbce57..00000000
--- a/packages/asset-manager/.idea/misc.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/modules.xml b/packages/asset-manager/.idea/modules.xml
deleted file mode 100644
index b320cde9..00000000
--- a/packages/asset-manager/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/prettier.xml b/packages/asset-manager/.idea/prettier.xml
deleted file mode 100644
index 0c83ac4e..00000000
--- a/packages/asset-manager/.idea/prettier.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.idea/vcs.xml b/packages/asset-manager/.idea/vcs.xml
deleted file mode 100644
index b2bdec2d..00000000
--- a/packages/asset-manager/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/asset-manager/.nvmrc b/packages/asset-manager/.nvmrc
deleted file mode 100644
index a682cfb9..00000000
--- a/packages/asset-manager/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-v25
diff --git a/packages/asset-manager/src/asset-manager.library.ts b/packages/asset-manager/src/asset-manager.library.ts
deleted file mode 100644
index a162f1dc..00000000
--- a/packages/asset-manager/src/asset-manager.library.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import {
- BaseAssetManagerLibrary,
- type InitContext,
- NfFile,
- NfNotFound,
-} from "@nanoforge-dev/common";
-
-/**
- * Built-in asset-manager library.
- *
- * @remarks
- * Stores the file map injected by the NanoForge loader (`nf start`) and
- * exposes assets via `getAsset`. Register it with the application before
- * calling `init`:
- *
- * ```ts
- * const client = NanoforgeFactory.createClient();
- * client.useAssetManager(new AssetManagerLibrary());
- * await client.init(`container, files, env `);
- * ```
- *
- * File paths are normalised on lookup: leading slashes are collapsed to a
- * single `/`, duplicate slashes are removed, and trailing slashes are stripped.
- */
-export class AssetManagerLibrary extends BaseAssetManagerLibrary {
- private _assets?: Map;
-
- /** @internal */
- get __name(): string {
- return "AssetManagerLibrary";
- }
-
- /** @internal */
- public async __init(context: InitContext): Promise {
- this._assets = context.files;
- }
-
- /**
- * Retrieve a registered file asset by its virtual path.
- *
- * @remarks
- * The path is normalised before lookup: leading and duplicate slashes are
- * collapsed and trailing slashes are removed.
- *
- * @param path - Virtual path of the asset (e.g. "/textures/hero.png").
- * @returns An `NfFile` handle for the requested asset, or `undefined` if no asset path is provided.
- * @throws `NfNotFound` When no asset is registered at the given path.
- * @throws `NfNotInitializedException` When called before `__init` has resolved.
- */
- public getAsset(path: string): NfFile;
- public getAsset(path: "" | undefined): undefined;
- public getAsset(path: string | undefined): NfFile | undefined {
- if (!this._assets) this.throwNotInitializedError();
-
- if (!path) return undefined;
-
- const res = this._assets.get(this._parsePath(path));
- if (!res) throw new NfNotFound(path, "Asset");
- return new NfFile(res);
- }
-
- private _parsePath(path: string): string {
- return path
- .replace(/^(\/*)/, "/")
- .replaceAll(/(\/+)/g, "/")
- .replace(/(\/+)$/, "");
- }
-}
diff --git a/packages/asset-manager/src/index.ts b/packages/asset-manager/src/index.ts
deleted file mode 100644
index b1ecb3a3..00000000
--- a/packages/asset-manager/src/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { AssetManagerLibrary } from "./asset-manager.library";
diff --git a/packages/asset-manager/test/asset-manager.library.spec.ts b/packages/asset-manager/test/asset-manager.library.spec.ts
deleted file mode 100644
index f43edbfb..00000000
--- a/packages/asset-manager/test/asset-manager.library.spec.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { type IConfigRegistry, InitContext, NfNotFound } from "@nanoforge-dev/common";
-import { beforeEach, describe, expect, it } from "vitest";
-
-import { EditableApplicationContext } from "../../core/src/common/context/contexts/application.editable-context";
-import { EditableLibraryManager } from "../../core/src/common/library/manager/library.manager";
-import { AssetManagerLibrary } from "../src";
-
-const makeContext = (files: Map) => {
- const libraryManager = new EditableLibraryManager();
- const appContext = new EditableApplicationContext(libraryManager);
- const configRegistry = {} as IConfigRegistry;
- return new InitContext(appContext, libraryManager, configRegistry, {
- // @ts-ignore
- container: null,
- files,
- });
-};
-
-const TEST_FILES = new Map([
- ["/test.png", "blob:http://localhost:3000/test.png"],
- ["/test.wasm", "blob:http://localhost:3000/test.wasm"],
- ["/test.wgsl", "blob:http://localhost:3000/test.wgsl"],
-]);
-
-describe("AssetManagerLibrary", () => {
- describe("metadata", () => {
- it("should expose the correct library name", () => {
- expect(new AssetManagerLibrary().__name).toBe("AssetManagerLibrary");
- });
- });
-
- describe("before initialization", () => {
- it("should throw when getAsset is called before __init", () => {
- const library = new AssetManagerLibrary();
- expect(() => library.getAsset("test.png")).toThrow();
- });
- });
-
- describe("getAsset", () => {
- let library: AssetManagerLibrary;
-
- beforeEach(async () => {
- library = new AssetManagerLibrary();
- await library.__init(makeContext(TEST_FILES));
- });
-
- it("should return the correct path for a png asset", () => {
- expect(library.getAsset("test.png").path).toBe("blob:http://localhost:3000/test.png");
- });
-
- it("should return the correct path for a wasm asset", () => {
- expect(library.getAsset("test.wasm").path).toBe("blob:http://localhost:3000/test.wasm");
- });
-
- it("should return the correct path for a wgsl asset", () => {
- expect(library.getAsset("test.wgsl").path).toBe("blob:http://localhost:3000/test.wgsl");
- });
-
- it("should normalize path with a leading slash", () => {
- expect(library.getAsset("/test.png").path).toBe("blob:http://localhost:3000/test.png");
- });
-
- it("should normalize path with multiple leading slashes", () => {
- expect(library.getAsset("//test.png").path).toBe("blob:http://localhost:3000/test.png");
- });
-
- it("should throw NfNotFound for an unknown asset", () => {
- expect(() => library.getAsset("unknown.png")).toThrow(NfNotFound);
- });
-
- it("should throw NfNotFound for an unknown wasm", () => {
- expect(() => library.getAsset("unknown.wasm")).toThrow(NfNotFound);
- });
-
- it("should throw NfNotFound for an unknown wgsl", () => {
- expect(() => library.getAsset("unknown.wgsl")).toThrow(NfNotFound);
- });
-
- it("should return undefined for an undefined path", () => {
- expect(library.getAsset(undefined)).toBe(undefined);
- expect(library.getAsset("")).toBe(undefined);
- });
- });
-});
diff --git a/packages/asset/.cliff-jumperrc.json b/packages/asset/.cliff-jumperrc.json
new file mode 100644
index 00000000..8dd6513f
--- /dev/null
+++ b/packages/asset/.cliff-jumperrc.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
+ "name": "asset",
+ "org": "nanoforge-dev",
+ "packagePath": "packages/asset",
+ "tagTemplate": "{{new-version}}",
+ "identifierBase": false
+}
diff --git a/packages/asset-manager/.gitignore b/packages/asset/.gitignore
similarity index 100%
rename from packages/asset-manager/.gitignore
rename to packages/asset/.gitignore
diff --git a/packages/network-client/.prettierignore b/packages/asset/.prettierignore
similarity index 100%
rename from packages/network-client/.prettierignore
rename to packages/asset/.prettierignore
diff --git a/packages/asset-manager/CHANGELOG.md b/packages/asset/CHANGELOG.md
similarity index 100%
rename from packages/asset-manager/CHANGELOG.md
rename to packages/asset/CHANGELOG.md
diff --git a/packages/asset/LICENSE b/packages/asset/LICENSE
new file mode 100644
index 00000000..46724861
--- /dev/null
+++ b/packages/asset/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright © 2026 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/asset-manager/README.md b/packages/asset/README.md
similarity index 98%
rename from packages/asset-manager/README.md
rename to packages/asset/README.md
index ec968ef6..a0eaefeb 100644
--- a/packages/asset-manager/README.md
+++ b/packages/asset/README.md
@@ -19,7 +19,7 @@
## Installation
-**Node.js 25 or newer is required.**
+**Node.js 26 or newer is required.**
```sh
npm install @nanoforge-dev/asset-manager
diff --git a/packages/graphics-2d/cliff.toml b/packages/asset/cliff.toml
similarity index 100%
rename from packages/graphics-2d/cliff.toml
rename to packages/asset/cliff.toml
diff --git a/packages/input/eslint.config.js b/packages/asset/eslint.config.js
similarity index 100%
rename from packages/input/eslint.config.js
rename to packages/asset/eslint.config.js
diff --git a/packages/asset-manager/mint-tsdocs.config.json b/packages/asset/mint-tsdocs.config.json
similarity index 100%
rename from packages/asset-manager/mint-tsdocs.config.json
rename to packages/asset/mint-tsdocs.config.json
diff --git a/packages/asset-manager/package.json b/packages/asset/package.json
similarity index 88%
rename from packages/asset-manager/package.json
rename to packages/asset/package.json
index 21fade03..cb390655 100644
--- a/packages/asset-manager/package.json
+++ b/packages/asset/package.json
@@ -1,8 +1,8 @@
{
"$schema": "https://json.schemastore.org/package.json",
- "name": "@nanoforge-dev/asset-manager",
+ "name": "@nanoforge-dev/asset",
"version": "1.4.2",
- "description": "NanoForge Engine - Asset Manager",
+ "description": "NanoForge Engine - Asset",
"keywords": [
"nanoforge",
"game",
@@ -13,8 +13,9 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips "
],
"files": [
@@ -43,7 +44,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/NanoForge-dev/Engine.git",
- "directory": "packages/asset-manager"
+ "directory": "packages/asset"
},
"funding": "https://github.com/NanoForge-dev/Engine?sponsor",
"scripts": {
@@ -52,7 +53,7 @@
"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/asset-manager/*'",
+ "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/asset/*'",
"release": "cliff-jumper",
"docs": "mint-tsdocs generate"
},
@@ -71,9 +72,9 @@
"unrun": "catalog:build",
"vitest": "catalog:test"
},
- "packageManager": "pnpm@11.5.2",
+ "packageManager": "pnpm@12.0.0",
"engines": {
- "node": "25"
+ "node": "26"
},
"publishConfig": {
"access": "public"
diff --git a/packages/graphics-2d/prettier.config.js b/packages/asset/prettier.config.js
similarity index 100%
rename from packages/graphics-2d/prettier.config.js
rename to packages/asset/prettier.config.js
diff --git a/packages/asset/src/asset-library.ts b/packages/asset/src/asset-library.ts
new file mode 100644
index 00000000..c0a68310
--- /dev/null
+++ b/packages/asset/src/asset-library.ts
@@ -0,0 +1,65 @@
+import {
+ type AssetContext,
+ type InitContext,
+ Library,
+ NfFile,
+ NfNotFound,
+ defineLibraryKey,
+} from "@nanoforge-dev/common";
+
+export const ASSET_LIBRARY_KEY = "assets" as const;
+
+/**
+ * Built-in asset library.
+ *
+ * @remarks
+ * Auto-registered by `@nanoforge-dev/core` — app code never calls
+ * `app.use(new AssetLibrary())` directly. Stores the virtual file map
+ * injected by the NanoForge loader and exposes it via `getAsset`, always
+ * available on `Context.assets`.
+ *
+ * File paths are normalised on lookup: leading/duplicate slashes collapse
+ * to a single `/`, and a trailing slash is stripped.
+ */
+export class AssetLibrary extends Library {
+ readonly key = defineLibraryKey(ASSET_LIBRARY_KEY);
+
+ private _assets?: Map;
+
+ public override async __init(ctx: InitContext): Promise {
+ this._assets = ctx.files;
+ }
+
+ /**
+ * Retrieve a registered file asset by its virtual path.
+ *
+ * @param path - Virtual path of the asset (e.g. "/textures/hero.png").
+ * @returns An `NfFile` handle, or `undefined` if no path is given.
+ * @throws `NfNotFound` When no asset is registered at the given path.
+ * @throws `NfNotInitializedException` When called before `__init` resolves.
+ */
+ public getAsset(path: string): NfFile;
+ public getAsset(path: "" | undefined): undefined;
+ public getAsset(path: string | undefined): NfFile | undefined {
+ if (!this._assets) this.throwNotInitializedError();
+ if (!path) return undefined;
+
+ const url = this._assets.get(this._normalize(path));
+ if (!url) throw new NfNotFound(path, "Asset");
+ return new NfFile(url);
+ }
+
+ public override expose(): AssetContext {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const library = this;
+ return {
+ getAsset: (path: any): any => library.getAsset(path),
+ };
+ }
+
+ private _normalize(path: string): string {
+ const collapsed = path.replace(/\/{2,}/g, "/");
+ const withLeadingSlash = collapsed.startsWith("/") ? collapsed : `/${collapsed}`;
+ return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/, "") : withLeadingSlash;
+ }
+}
diff --git a/packages/asset/src/index.ts b/packages/asset/src/index.ts
new file mode 100644
index 00000000..b4dbb41a
--- /dev/null
+++ b/packages/asset/src/index.ts
@@ -0,0 +1 @@
+export { ASSET_LIBRARY_KEY, AssetLibrary } from "./asset-library";
diff --git a/packages/asset/test/asset-library.spec.ts b/packages/asset/test/asset-library.spec.ts
new file mode 100644
index 00000000..25869576
--- /dev/null
+++ b/packages/asset/test/asset-library.spec.ts
@@ -0,0 +1,76 @@
+import { type InitContext, NfNotFound, NfNotInitializedException } from "@nanoforge-dev/common";
+import { beforeEach, describe, expect, it } from "vitest";
+
+import { AssetLibrary } from "../src";
+
+const TEST_FILES = new Map([
+ ["/test.png", "blob:http://localhost:3000/test.png"],
+ ["/test.wasm", "blob:http://localhost:3000/test.wasm"],
+ ["/test.wgsl", "blob:http://localhost:3000/test.wgsl"],
+]);
+
+const makeInitContext = (files: Map): InitContext => ({
+ vars: { get: () => undefined, set: () => {} },
+ env: {},
+ files,
+});
+
+describe("AssetLibrary", () => {
+ describe("metadata", () => {
+ it("should expose the reserved 'assets' key", () => {
+ expect(new AssetLibrary().key).toBe("assets");
+ });
+ });
+
+ describe("before initialization", () => {
+ it("should throw NfNotInitializedException when getAsset is called before __init", () => {
+ const library = new AssetLibrary();
+ expect(() => library.getAsset("test.png")).toThrow(NfNotInitializedException);
+ });
+ });
+
+ describe("getAsset", () => {
+ let library: AssetLibrary;
+
+ beforeEach(async () => {
+ library = new AssetLibrary();
+ await library.__init(makeInitContext(TEST_FILES));
+ });
+
+ it("should return the correct path for a registered asset", () => {
+ expect(library.getAsset("test.png").path).toBe("blob:http://localhost:3000/test.png");
+ });
+
+ it("should normalize a path with a leading slash", () => {
+ expect(library.getAsset("/test.png").path).toBe("blob:http://localhost:3000/test.png");
+ });
+
+ it("should normalize a path with multiple leading slashes", () => {
+ expect(library.getAsset("//test.png").path).toBe("blob:http://localhost:3000/test.png");
+ });
+
+ it("should normalize a path with a trailing slash", () => {
+ expect(library.getAsset("test.wasm/").path).toBe("blob:http://localhost:3000/test.wasm");
+ });
+
+ it("should throw NfNotFound for an unknown asset", () => {
+ expect(() => library.getAsset("unknown.png")).toThrow(NfNotFound);
+ });
+
+ it("should return undefined for an empty or undefined path", () => {
+ expect(library.getAsset(undefined)).toBeUndefined();
+ expect(library.getAsset("")).toBeUndefined();
+ });
+ });
+
+ describe("expose", () => {
+ it("should return a narrow object whose getAsset matches the instance method", async () => {
+ const library = new AssetLibrary();
+ await library.__init(makeInitContext(TEST_FILES));
+
+ const exposed = library.expose();
+ expect(exposed.getAsset("test.png").path).toBe("blob:http://localhost:3000/test.png");
+ expect(Object.keys(exposed)).toEqual(["getAsset"]);
+ });
+ });
+});
diff --git a/packages/input/tsconfig.json b/packages/asset/tsconfig.json
similarity index 100%
rename from packages/input/tsconfig.json
rename to packages/asset/tsconfig.json
diff --git a/packages/network-server/tsconfig.spec.json b/packages/asset/tsconfig.spec.json
similarity index 100%
rename from packages/network-server/tsconfig.spec.json
rename to packages/asset/tsconfig.spec.json
diff --git a/packages/graphics-2d/tsdoc.json b/packages/asset/tsdoc.json
similarity index 100%
rename from packages/graphics-2d/tsdoc.json
rename to packages/asset/tsdoc.json
diff --git a/packages/ecs-server/tsdown.config.ts b/packages/asset/tsdown.config.ts
similarity index 100%
rename from packages/ecs-server/tsdown.config.ts
rename to packages/asset/tsdown.config.ts
diff --git a/packages/common/.idea/.gitignore b/packages/common/.idea/.gitignore
deleted file mode 100644
index 13566b81..00000000
--- a/packages/common/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/packages/common/.idea/.name b/packages/common/.idea/.name
deleted file mode 100644
index e68a4537..00000000
--- a/packages/common/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-[NanoForge] Engine Common
\ No newline at end of file
diff --git a/packages/common/.idea/[NanoForge] Engine Common.iml b/packages/common/.idea/[NanoForge] Engine Common.iml
deleted file mode 100644
index 24643cc3..00000000
--- a/packages/common/.idea/[NanoForge] Engine Common.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/common/.idea/codeStyles/Project.xml b/packages/common/.idea/codeStyles/Project.xml
deleted file mode 100644
index b70d7533..00000000
--- a/packages/common/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/common/.idea/codeStyles/codeStyleConfig.xml b/packages/common/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123c..00000000
--- a/packages/common/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/common/.idea/git_toolbox_blame.xml b/packages/common/.idea/git_toolbox_blame.xml
deleted file mode 100644
index 7dc12496..00000000
--- a/packages/common/.idea/git_toolbox_blame.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/common/.idea/git_toolbox_prj.xml b/packages/common/.idea/git_toolbox_prj.xml
deleted file mode 100644
index 02b915b8..00000000
--- a/packages/common/.idea/git_toolbox_prj.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/common/.idea/inspectionProfiles/Project_Default.xml b/packages/common/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 03d9549e..00000000
--- a/packages/common/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/common/.idea/jsLinters/eslint.xml b/packages/common/.idea/jsLinters/eslint.xml
deleted file mode 100644
index 541945bb..00000000
--- a/packages/common/.idea/jsLinters/eslint.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/common/.idea/modules.xml b/packages/common/.idea/modules.xml
deleted file mode 100644
index 78a78280..00000000
--- a/packages/common/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/common/.idea/prettier.xml b/packages/common/.idea/prettier.xml
deleted file mode 100644
index 0c83ac4e..00000000
--- a/packages/common/.idea/prettier.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/common/.idea/vcs.xml b/packages/common/.idea/vcs.xml
deleted file mode 100644
index b2bdec2d..00000000
--- a/packages/common/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/common/.nvmrc b/packages/common/.nvmrc
deleted file mode 100644
index a682cfb9..00000000
--- a/packages/common/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-v25
diff --git a/packages/common/LICENSE b/packages/common/LICENSE
index 62c6400b..46724861 100644
--- a/packages/common/LICENSE
+++ b/packages/common/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright © 2025 NanoForge
+Copyright © 2026 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
diff --git a/packages/common/README.md b/packages/common/README.md
index a874391b..00b64f28 100644
--- a/packages/common/README.md
+++ b/packages/common/README.md
@@ -19,7 +19,7 @@
## Installation
-**Node.js 25 or newer is required.**
+**Node.js 26 or newer is required.**
```sh
npm install @nanoforge-dev/common
diff --git a/packages/common/package.json b/packages/common/package.json
index 0d0ef331..9341306b 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -13,8 +13,9 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips "
],
"files": [
@@ -68,9 +69,9 @@
"unrun": "catalog:build",
"vitest": "catalog:test"
},
- "packageManager": "pnpm@11.5.2",
+ "packageManager": "pnpm@12.0.0",
"engines": {
- "node": "25"
+ "node": "26"
},
"publishConfig": {
"access": "public"
diff --git a/packages/common/src/common/file.ts b/packages/common/src/common/file.ts
index 376ac68d..d545570b 100644
--- a/packages/common/src/common/file.ts
+++ b/packages/common/src/common/file.ts
@@ -4,8 +4,8 @@ import { NfFetchException } from "../exception";
* Represents a file asset that has been registered with the engine.
*
* @remarks
- * Instances are returned by `AssetManagerLibrary.getAsset`. Each method
- * lazily fetches the file from the engine's virtual file system and returns the
+ * Instances are returned by `AssetContext.getAsset`. Each method lazily
+ * fetches the file from the engine's virtual file system and returns the
* content in the requested format.
*
* All read methods throw `NfFetchException` when the underlying `fetch`
@@ -13,7 +13,7 @@ import { NfFetchException } from "../exception";
*
* @example
* ```ts
- * const file = assetManager.getAsset("/textures/hero.png");
+ * const file = asset.getAsset("/textures/hero.png");
* const buffer = await file.arrayBuffer();
* ```
*/
diff --git a/packages/common/src/context/context.type.ts b/packages/common/src/context/context.type.ts
new file mode 100644
index 00000000..b4bd95cc
--- /dev/null
+++ b/packages/common/src/context/context.type.ts
@@ -0,0 +1,71 @@
+import type { NfFile } from "../common/file";
+
+/**
+ * Read-only application state plus the actions available to affect it.
+ *
+ * @remarks
+ * `AppContext` deliberately exposes no setters — mutation only happens
+ * internally, inside `@nanoforge-dev/core`. Game code and libraries can read
+ * state and call actions, but can never reach into and directly overwrite
+ * engine-owned state.
+ */
+export interface AppContext {
+ /** Whether the engine's tick loop is currently running. */
+ readonly isRunning: boolean;
+ /** Whether the tick loop is currently paused (no library's `__run` is called while paused). */
+ readonly isPaused: boolean;
+ /** Elapsed time in milliseconds since the last tick. Does not advance while paused. */
+ readonly delta: number;
+ /** Configured ticks-per-second for the run loop. */
+ readonly tickRate: number;
+ /** Requests that the tick loop stop after the current tick. */
+ requestStop(): void;
+ /** Pauses the tick loop: every library's `__run` is skipped until resumed. */
+ requestPause(): void;
+ /** Resumes a paused tick loop. */
+ requestResume(): void;
+}
+
+/**
+ * A typed, dev-editable key/value bag shared across all libraries and game
+ * code for the lifetime of a run.
+ *
+ * @typeParam TVars - Shape of the variables bag. Defaults to an untyped
+ * `Record` when no shape is declared.
+ */
+export interface VarsContext = Record> {
+ get(key: K): TVars[K];
+ set(key: K, value: TVars[K]): void;
+}
+
+/**
+ * Public surface of the engine's built-in asset library, always present on
+ * `Context.assets` regardless of which libraries an app registers.
+ */
+export interface AssetContext {
+ getAsset(path: string): NfFile;
+ getAsset(path: "" | undefined): undefined;
+}
+
+/**
+ * The context object handed to every library's `__run`/`__clear` hooks and
+ * to game code.
+ *
+ * @remarks
+ * `app`, `vars` and `assets` are always present (core-provided). Every other
+ * key is contributed by a registered `Library` via its `expose()` method.
+ * Library packages add their own key by augmenting this interface:
+ *
+ * ```ts
+ * declare module "@nanoforge-dev/common" {
+ * interface Context {
+ * ecs: EcsContextApi;
+ * }
+ * }
+ * ```
+ */
+export interface Context {
+ readonly app: AppContext;
+ readonly vars: VarsContext;
+ readonly assets: AssetContext;
+}
diff --git a/packages/common/src/context/contexts/application.context.ts b/packages/common/src/context/contexts/application.context.ts
deleted file mode 100644
index 96a7f12f..00000000
--- a/packages/common/src/context/contexts/application.context.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Holds runtime state that is shared across all libraries during the game loop.
- *
- * @remarks
- * An instance of this class is available to every library via
- * `BaseContext.application`. Libraries may read `isRunning` and `delta`
- * to make frame-rate-independent decisions.
- */
-export abstract class ApplicationContext {
- private _isRunning: boolean = false;
- /** @internal */
- protected _delta!: number;
-
- /**
- * Whether the engine game loop is currently executing.
- */
- get isRunning(): boolean {
- return this._isRunning;
- }
-
- /**
- * Elapsed time in milliseconds since the last frame.
- */
- get delta(): number {
- return this._delta;
- }
-
- /** @internal */
- setIsRunning(value: boolean): void {
- this._isRunning = value;
- }
-
- /** @internal */
- abstract muteSoundLibraries(): void;
-}
diff --git a/packages/common/src/context/contexts/client.context.ts b/packages/common/src/context/contexts/client.context.ts
deleted file mode 100644
index a491a93b..00000000
--- a/packages/common/src/context/contexts/client.context.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { type ClientLibraryManager } from "../../library";
-import type { ApplicationContext } from "./application.context";
-
-export class Context {
- private readonly _applicationContext: ApplicationContext;
- private readonly _libraryManager: ClientLibraryManager;
-
- constructor(applicationContext: ApplicationContext, libraryManager: ClientLibraryManager) {
- this._applicationContext = applicationContext;
- this._libraryManager = libraryManager;
- }
-
- get app(): ApplicationContext {
- return this._applicationContext;
- }
-
- get libs(): ClientLibraryManager {
- return this._libraryManager;
- }
-}
diff --git a/packages/common/src/context/contexts/executions/base.context.ts b/packages/common/src/context/contexts/executions/base.context.ts
deleted file mode 100644
index d5ab9e24..00000000
--- a/packages/common/src/context/contexts/executions/base.context.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { type LibraryManager } from "../../../library/manager/managers/library.manager";
-import { type ApplicationContext } from "../application.context";
-
-/**
- * Base context passed to library lifecycle hooks.
- *
- * @remarks
- * All execution context types (`InitContext`, `ClearContext`,
- * `ExecutionContext`) extend this class and expose `application` and
- * `libraries`.
- */
-export class BaseContext {
- private readonly _applicationContext: ApplicationContext;
- private readonly _libraryManager: LibraryManager;
-
- /**
- * @param applicationContext - Shared application-level runtime state.
- * @param libraryManager - Manager giving access to all registered libraries.
- */
- constructor(applicationContext: ApplicationContext, libraryManager: LibraryManager) {
- this._applicationContext = applicationContext;
- this._libraryManager = libraryManager;
- }
-
- /**
- * Shared application-level runtime state (running flag, delta time, …).
- */
- get application(): ApplicationContext {
- return this._applicationContext;
- }
-
- /**
- * Manager that provides typed access to every library registered with the
- * current application.
- */
- get libraries(): LibraryManager {
- return this._libraryManager;
- }
-}
diff --git a/packages/common/src/context/contexts/executions/clear.context.ts b/packages/common/src/context/contexts/executions/clear.context.ts
deleted file mode 100644
index ba8889a4..00000000
--- a/packages/common/src/context/contexts/executions/clear.context.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { BaseContext } from "./base.context";
-
-/**
- * Context passed to a library's `__clear` lifecycle hook during application shutdown.
- *
- * @remarks
- * Use the inherited `application` and `libraries` accessors to access shared
- * state and other libraries while tearing down resources.
- */
-export class ClearContext extends BaseContext {}
diff --git a/packages/common/src/context/contexts/executions/execution.context.ts b/packages/common/src/context/contexts/executions/execution.context.ts
deleted file mode 100644
index eae4a5cd..00000000
--- a/packages/common/src/context/contexts/executions/execution.context.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { BaseContext } from "./base.context";
-
-/**
- * Context passed to a library's `__run` hook on every game-loop tick.
- *
- * @remarks
- * Use the inherited `application` and `libraries` accessors to read the current
- * delta time and interact with other libraries during the frame update.
- */
-export class ExecutionContext extends BaseContext {}
diff --git a/packages/common/src/context/contexts/executions/index.ts b/packages/common/src/context/contexts/executions/index.ts
deleted file mode 100644
index 25d298ba..00000000
--- a/packages/common/src/context/contexts/executions/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { ClearContext } from "./clear.context";
-export { ExecutionContext } from "./execution.context";
-export { InitContext } from "./init.context";
diff --git a/packages/common/src/context/contexts/executions/init.context.ts b/packages/common/src/context/contexts/executions/init.context.ts
deleted file mode 100644
index 328771c8..00000000
--- a/packages/common/src/context/contexts/executions/init.context.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { type IConfigRegistry, type LibraryManager } from "../../../library";
-import { type IRunClientOptions, type IRunOptions } from "../../../options";
-import { type ApplicationContext } from "../application.context";
-import { BaseContext } from "./base.context";
-
-/**
- * Context object passed to every library's `__init` lifecycle hook.
- *
- * @remarks
- * Provides libraries with all the information they need during start-up:
- * the canvas container, pre-loaded asset files, environment variables, and a
- * config registry for loading structured configuration classes.
- *
- * Access it in a library's `__init` override:
- * ```ts
- * override async __init(ctx: InitContext) {
- * const config = await ctx.config.registerConfig(MyConfig);
- * const file = ctx.libraries.getAssetManager().library.getAsset("/data.json");
- * }
- * ```
- */
-export class InitContext extends BaseContext {
- private readonly _container: IRunClientOptions["container"] | undefined;
- private readonly _files: IRunOptions["files"];
- private readonly _env: Record;
- private readonly _config: IConfigRegistry;
-
- /**
- * @param context - Shared application-level runtime state.
- * @param libraryManager - Manager giving access to other registered libraries.
- * @param configRegistry - Registry used to resolve typed configuration objects.
- * @param options - Run options forwarded from NanoforgeApplication.init.
- */
- constructor(
- context: ApplicationContext,
- libraryManager: LibraryManager,
- configRegistry: IConfigRegistry,
- options: IRunOptions,
- ) {
- super(context, libraryManager);
-
- this._container = (options as IRunClientOptions)["container"];
- this._files = options.files;
- this._env = options.env;
- this._config = configRegistry;
- }
-
- /**
- * The `HTMLDivElement` that should host the game canvas.
- *
- * @remarks
- * Only available in client contexts; `undefined` on the server.
- */
- get container(): IRunClientOptions["container"] | undefined {
- return this._container;
- }
-
- /**
- * Map of virtual file paths to their URL strings, pre-loaded by the engine bundler.
- */
- get files(): IRunOptions["files"] {
- return this._files;
- }
-
- /**
- * Key/value map of environment variables made available at runtime.
- */
- get env(): Record {
- return this._env;
- }
-
- /**
- * Registry that resolves class-validator/class-transformer decorated
- * configuration classes from the runtime environment.
- *
- * @remarks
- * Call `ctx.config.registerConfig(MyConfigClass)` to get a validated and
- * transformed instance populated from `ctx.env`.
- */
- get config(): IConfigRegistry {
- return this._config;
- }
-}
diff --git a/packages/common/src/context/contexts/index.ts b/packages/common/src/context/contexts/index.ts
deleted file mode 100644
index 19e85e08..00000000
--- a/packages/common/src/context/contexts/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from "./executions";
-export { ApplicationContext } from "./application.context";
-export { Context } from "./client.context";
-export { LibraryContext, LibraryStatusEnum } from "./library.context";
diff --git a/packages/common/src/context/contexts/library.context.ts b/packages/common/src/context/contexts/library.context.ts
deleted file mode 100644
index 1b58eb06..00000000
--- a/packages/common/src/context/contexts/library.context.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * Lifecycle states a library can be in.
- */
-export enum LibraryStatusEnum {
- /** The library has not been initialised yet. */
- UNLOADED = "UNLOADED",
- /** The library has been successfully initialised and is ready to use. */
- LOADED = "LOADED",
- /** The library has been cleared and its resources released. */
- CLEAR = "CLEAR",
-}
-
-/**
- * Tracks the current lifecycle state of a single library registration.
- */
-export class LibraryContext {
- /** @internal */
- protected _status: LibraryStatusEnum = LibraryStatusEnum.UNLOADED;
-
- /**
- * Current lifecycle state of the associated library.
- */
- get status() {
- return this._status;
- }
-}
diff --git a/packages/common/src/context/index.ts b/packages/common/src/context/index.ts
index 315ff978..c28b0258 100644
--- a/packages/common/src/context/index.ts
+++ b/packages/common/src/context/index.ts
@@ -1 +1,2 @@
-export * from "./contexts";
+export type { AppContext, AssetContext, Context, VarsContext } from "./context.type";
+export type { ClientRunOptions, InitContext, RunOptions } from "./init-context.type";
diff --git a/packages/common/src/context/init-context.type.ts b/packages/common/src/context/init-context.type.ts
new file mode 100644
index 00000000..796a4038
--- /dev/null
+++ b/packages/common/src/context/init-context.type.ts
@@ -0,0 +1,49 @@
+import { type EventEmitter } from "../editor";
+import type { VarsContext } from "./context.type";
+
+/**
+ * Options every NanoForge application is started with.
+ */
+export interface RunOptions {
+ /** Virtual file system, injected by the NanoForge loader. */
+ files: Map;
+ /** Raw, unvalidated environment variables. */
+ env: Record;
+
+ /**
+ * Raw editor bridge, supplied by whoever starts the app under an editor
+ * host. `EditorLibrary` transforms this pair into the single
+ * `Context.editor` facade every library sees.
+ */
+ editor?: {
+ /** Engine → editor channel. */
+ toEditor: EventEmitter;
+ /** Editor → engine channel. */
+ fromEditor: EventEmitter;
+ };
+}
+
+/**
+ * Options a client application is started with.
+ */
+export interface ClientRunOptions extends RunOptions {
+ /** DOM element the client should render into. */
+ container: HTMLDivElement;
+}
+
+/**
+ * Narrow context available to a library's `__init` hook.
+ *
+ * @remarks
+ * `app` and `assets` are intentionally absent here: `app` doesn't exist yet
+ * (the tick loop hasn't started), and `assets` isn't populated until every
+ * library has finished initializing. Libraries that need raw files during
+ * `__init` (e.g. the built-in asset library) read `files` directly.
+ */
+export interface InitContext {
+ vars: VarsContext;
+ env: Record;
+ files: Map;
+ container?: HTMLDivElement;
+ editor?: RunOptions["editor"];
+}
diff --git a/packages/common/src/editor/event-emitter.type.ts b/packages/common/src/editor/event-emitter.type.ts
new file mode 100644
index 00000000..e9d4502f
--- /dev/null
+++ b/packages/common/src/editor/event-emitter.type.ts
@@ -0,0 +1,23 @@
+/**
+ * A generic, queued event emitter.
+ *
+ * @remarks
+ * "Queued" means `emit` doesn't invoke listeners synchronously — it enqueues
+ * the event, and `runEvents()` drains the queue, invoking listeners once per
+ * call. This lets an event arriving mid-tick (e.g. from a WebSocket handler)
+ * be applied at a controlled point instead of interrupting whatever else is
+ * running.
+ *
+ * @typeParam Events - Union of event name literals.
+ * @typeParam EventsMap - Maps each event name to its listener argument tuple.
+ */
+export interface EventEmitter<
+ Events extends string = string,
+ EventsMap extends Record = Record,
+> {
+ on(event: K, listener: (...args: EventsMap[K]) => void): void;
+ off(event: K, listener: (...args: EventsMap[K]) => void): void;
+ emit(event: K, ...args: EventsMap[K]): void;
+ /** Drains the queue, synchronously invoking listeners for each queued event. */
+ runEvents(): void;
+}
diff --git a/packages/common/src/editor/index.ts b/packages/common/src/editor/index.ts
new file mode 100644
index 00000000..49d31358
--- /dev/null
+++ b/packages/common/src/editor/index.ts
@@ -0,0 +1 @@
+export type { EventEmitter } from "./event-emitter.type";
diff --git a/packages/common/src/exception/abstracts/exception.abstract.ts b/packages/common/src/exception/abstracts/exception.abstract.ts
index 2e6e4985..63053fa9 100644
--- a/packages/common/src/exception/abstracts/exception.abstract.ts
+++ b/packages/common/src/exception/abstracts/exception.abstract.ts
@@ -1,32 +1,16 @@
-import type { INfException } from "../interfaces/exception.type";
-
/**
* Base class for all NanoForge engine exceptions.
*
* @remarks
* All errors thrown by the engine extend this class so that game code can
* differentiate engine errors from unrelated runtime errors.
- *
- * @example
- * ```ts
- * try {
- * assetManager.getAsset("/missing.png");
- * } catch (err) {
- * if (err instanceof NfException) {
- * console.error(err.code, err.message);
- * }
- * }
- * ```
*/
-export abstract class NfException extends Error implements INfException {
+export abstract class NfException extends Error {
/**
* HTTP-style numeric code identifying the error category.
*/
abstract get code(): number;
- /**
- * @param message - Optional human-readable description of the error.
- */
protected constructor(message?: string) {
super(
message ? `[NANOFORGE] ${message}` : "[NANOFORGE] An error occurred (Unknown exception).",
diff --git a/packages/common/src/exception/exceptions/config.exception.ts b/packages/common/src/exception/exceptions/config.exception.ts
deleted file mode 100644
index 3f4f4b9e..00000000
--- a/packages/common/src/exception/exceptions/config.exception.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { NfException } from "../abstracts/exception.abstract";
-
-/**
- * Thrown when a library configuration is invalid or incomplete.
- *
- * @remarks
- * Typically raised during `__init` when required environment variables are
- * missing or fail validation. For example, `NetworkClientLibrary` throws this
- * when neither `SERVER_TCP_PORT` nor `SERVER_UDP_PORT` is provided.
- */
-export class NfConfigException extends NfException {
- /** Always `400`. */
- get code(): number {
- return 400;
- }
-
- /**
- * @param message - Description of the configuration error.
- * @param library - Optional library name that detected the error.
- */
- constructor(message: string, library?: string) {
- super(`Config Exception ${library ? `(${library}) ` : ""}- ${message}`);
- }
-}
diff --git a/packages/common/src/exception/exceptions/duplicate-library.exception.ts b/packages/common/src/exception/exceptions/duplicate-library.exception.ts
new file mode 100644
index 00000000..cbe25ef4
--- /dev/null
+++ b/packages/common/src/exception/exceptions/duplicate-library.exception.ts
@@ -0,0 +1,19 @@
+import { NfException } from "../abstracts/exception.abstract";
+
+/**
+ * Thrown when two libraries are registered under the same context key, or
+ * when a library tries to register under a reserved key (`"app"`, `"vars"`,
+ * `"assets"`).
+ */
+export class NfDuplicateLibraryException extends NfException {
+ get code(): number {
+ return 409;
+ }
+
+ /**
+ * @param key - The context key that was already taken or is reserved.
+ */
+ constructor(key: string) {
+ super(`Library key "${key}" is already registered or reserved.`);
+ }
+}
diff --git a/packages/common/src/exception/exceptions/fetch.exception.ts b/packages/common/src/exception/exceptions/fetch.exception.ts
index 35409e6e..3aa3f608 100644
--- a/packages/common/src/exception/exceptions/fetch.exception.ts
+++ b/packages/common/src/exception/exceptions/fetch.exception.ts
@@ -4,15 +4,12 @@ import { NfException } from "../abstracts/exception.abstract";
* Thrown when an HTTP fetch performed internally by the engine fails.
*
* @remarks
- * Raised by `NfFile` methods (e.g. `arrayBuffer`, `text`) when the
- * underlying `fetch` call returns a non-OK HTTP response.
+ * Raised by `NfFile` methods when the underlying `fetch` call returns a
+ * non-OK HTTP response.
*/
export class NfFetchException extends NfException {
private readonly _code: number;
- /**
- * The HTTP status code returned by the failed request (e.g. `404`, `500`).
- */
get code(): number {
return this._code;
}
diff --git a/packages/common/src/exception/exceptions/not-found.exception.ts b/packages/common/src/exception/exceptions/not-found.exception.ts
index 57f313e8..a467fa2a 100644
--- a/packages/common/src/exception/exceptions/not-found.exception.ts
+++ b/packages/common/src/exception/exceptions/not-found.exception.ts
@@ -1,30 +1,17 @@
import { NfException } from "../abstracts/exception.abstract";
/**
- * Thrown when a requested resource (asset, sound, music track, etc.) cannot
- * be found in the engine's registry.
- *
- * @example
- * ```ts
- * // Thrown automatically by AssetManagerLibrary.getAsset, SoundLibrary.play, etc.
- * try {
- * const file = assetManager.getAsset("/textures/hero.png");
- * } catch (err) {
- * if (err instanceof NfNotFound) {
- * console.error("Missing resource:", err.message);
- * }
- * }
- * ```
+ * Thrown when a requested resource (asset, library-contributed context key,
+ * etc.) cannot be found.
*/
export class NfNotFound extends NfException {
- /** Always `404`. */
get code(): number {
return 404;
}
/**
* @param item - Name or path of the missing resource.
- * @param type - Optional category label (e.g. "Asset", "Sound").
+ * @param type - Optional category label (e.g. "Asset").
*/
constructor(item: string, type?: string) {
super(`${type ? `${type} - ` : ""}${item} not found.`);
diff --git a/packages/common/src/exception/exceptions/not-initialized.exception.ts b/packages/common/src/exception/exceptions/not-initialized.exception.ts
index 61deec33..b5cf5404 100644
--- a/packages/common/src/exception/exceptions/not-initialized.exception.ts
+++ b/packages/common/src/exception/exceptions/not-initialized.exception.ts
@@ -1,23 +1,19 @@
import { NfException } from "../abstracts/exception.abstract";
/**
- * Thrown when a method is called on a library or engine component before it
- * has been initialised via `__init`.
+ * Thrown when a library is used before its `__init` lifecycle hook has
+ * resolved.
*
* @remarks
- * This exception is raised automatically by the helper method
- * `Library.throwNotInitializedError()`. Ensure that
- * `NanoforgeApplication.init` has resolved before interacting with any
- * library.
+ * Raised automatically by `Library.throwNotInitializedError()`.
*/
export class NfNotInitializedException extends NfException {
- /** Always `404`. */
get code(): number {
return 404;
}
/**
- * @param item - Name of the uninitialised component (e.g. the library's __name).
+ * @param item - Key of the uninitialised library.
* @param type - Optional category label (e.g. "Library").
*/
constructor(item: string, type?: string) {
diff --git a/packages/common/src/exception/index.ts b/packages/common/src/exception/index.ts
index ae691faa..5864df39 100644
--- a/packages/common/src/exception/index.ts
+++ b/packages/common/src/exception/index.ts
@@ -1,4 +1,5 @@
-export { NfConfigException } from "./exceptions/config.exception";
+export { NfException } from "./abstracts/exception.abstract";
+export { NfDuplicateLibraryException } from "./exceptions/duplicate-library.exception";
export { NfFetchException } from "./exceptions/fetch.exception";
export { NfNotFound } from "./exceptions/not-found.exception";
export { NfNotInitializedException } from "./exceptions/not-initialized.exception";
diff --git a/packages/common/src/exception/interfaces/exception.type.ts b/packages/common/src/exception/interfaces/exception.type.ts
deleted file mode 100644
index 33ea46db..00000000
--- a/packages/common/src/exception/interfaces/exception.type.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export interface INfException extends Error {
- get code(): number;
-}
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index 5adc5c5b..426c83ba 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -1,5 +1,5 @@
export * from "./common";
export * from "./context";
-export * from "./library";
-export * from "./options";
+export * from "./editor";
export * from "./exception";
+export * from "./library";
diff --git a/packages/common/src/library/config/index.ts b/packages/common/src/library/config/index.ts
deleted file mode 100644
index b838944c..00000000
--- a/packages/common/src/library/config/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./interfaces/config-registry.type";
diff --git a/packages/common/src/library/config/interfaces/config-registry.type.ts b/packages/common/src/library/config/interfaces/config-registry.type.ts
deleted file mode 100644
index bbd4e24e..00000000
--- a/packages/common/src/library/config/interfaces/config-registry.type.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Resolves and validates typed configuration objects from the runtime
- * environment.
- *
- * @remarks
- * An instance is available in `InitContext.config`. Configuration
- * classes must be decorated with `class-transformer` (`@Expose`, `@Default`)
- * and `class-validator` (`@IsPort`, `@IsIpOrURL`, etc.) decorators from
- * `@nanoforge-dev/config`.
- *
- * @example
- * ```ts
- * import `Expose, IsPort ` from "@nanoforge-dev/config";
- *
- * class MyConfig {
- * \@Expose() \@IsPort()
- * PORT!: string;
- * }
- *
- * // Inside __init:
- * const cfg = await ctx.config.registerConfig(MyConfig);
- * console.log(cfg.PORT);
- * ```
- */
-export interface IConfigRegistry {
- /**
- * Instantiate, populate from the runtime environment, transform, and
- * validate a configuration class.
- *
- * @typeParam T - Configuration class type.
- * @param config - Constructor of the configuration class.
- * @returns A validated, populated instance of the configuration class.
- * @throws `NfConfigException` When validation fails.
- */
- registerConfig(config: new () => T): Promise;
-}
diff --git a/packages/common/src/library/define-library-key.ts b/packages/common/src/library/define-library-key.ts
new file mode 100644
index 00000000..72b29bfc
--- /dev/null
+++ b/packages/common/src/library/define-library-key.ts
@@ -0,0 +1,28 @@
+import type { Context } from "../context";
+
+/**
+ * Declares a library's context key, tying its runtime key to the type-level
+ * `Context` augmentation so the two can't silently desync.
+ *
+ * @remarks
+ * Only accepts a key that is already a member of `Context` at the call
+ * site — which means the library's own `declare module "@nanoforge-dev/common"
+ * { interface Context { ... } }` augmentation must be part of the same
+ * compilation unit. As a side effect, two libraries whose augmentations
+ * declare the same key with incompatible shapes will fail to compile
+ * together, catching accidental key collisions at build time.
+ *
+ * @example
+ * ```ts
+ * class MyLibrary extends Library {
+ * readonly key = defineLibraryKey("my");
+ * }
+ *
+ * declare module "@nanoforge-dev/common" {
+ * interface Context {
+ * my: MyContextApi;
+ * }
+ * }
+ * ```
+ */
+export const defineLibraryKey = (key: K): K => key;
diff --git a/packages/common/src/library/index.ts b/packages/common/src/library/index.ts
index 089b3727..5278df51 100644
--- a/packages/common/src/library/index.ts
+++ b/packages/common/src/library/index.ts
@@ -1,3 +1,3 @@
-export * from "./config";
-export * from "./libraries";
-export * from "./manager";
+export { defineLibraryKey } from "./define-library-key";
+export { Library } from "./library";
+export { DEFAULT_LIBRARY_RELATIONSHIPS, type LibraryRelationships } from "./relationship.type";
diff --git a/packages/common/src/library/libraries/abstracts/asset-manager.library.abstract.ts b/packages/common/src/library/libraries/abstracts/asset-manager.library.abstract.ts
deleted file mode 100644
index 23973afb..00000000
--- a/packages/common/src/library/libraries/abstracts/asset-manager.library.abstract.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { type NfFile } from "../../../common";
-import { type InitContext } from "../../../context";
-import { type IAssetManagerLibrary } from "../interfaces";
-import { Library } from "../library";
-
-/**
- * Abstract base class for asset-manager libraries.
- *
- * @remarks
- * Extend this class to implement a custom asset-manager and register it with
- * `NanoforgeApplication.useAssetManager`. The built-in implementation
- * is `AssetManagerLibrary`.
- */
-export abstract class BaseAssetManagerLibrary extends Library implements IAssetManagerLibrary {
- /** @internal */
- public abstract override __init(context: InitContext): Promise;
-
- /**
- * Retrieve a file asset by its virtual path.
- *
- * @param path - Virtual path as registered in the engine's file map (e.g. "/textures/hero.png").
- * @returns An `NfFile` handle that lazily fetches the asset content.
- * @throws `NfNotFound` When no asset is registered at the given path.
- * @throws `NfNotInitializedException` When called before `__init` has resolved.
- */
- public abstract getAsset(path: string): NfFile;
- public abstract getAsset(path: "" | undefined): undefined;
-}
diff --git a/packages/common/src/library/libraries/abstracts/component-system.library.abstract.ts b/packages/common/src/library/libraries/abstracts/component-system.library.abstract.ts
deleted file mode 100644
index a00f9015..00000000
--- a/packages/common/src/library/libraries/abstracts/component-system.library.abstract.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { type Context } from "../../../context";
-import { type IComponentSystemLibrary } from "../interfaces";
-import { Library } from "../library";
-
-/**
- * Abstract base class for component-system (ECS) libraries.
- *
- * @remarks
- * Extend this class (or the higher-level `AbstractECSLibrary`) to
- * implement a custom ECS back-end and register it with
- * `NanoforgeApplication.useComponentSystem`. The built-in
- * implementations are `ECSClientLibrary` and `ECSServerLibrary`.
- */
-export abstract class BaseComponentSystemLibrary
- extends Library
- implements IComponentSystemLibrary
-{
- /** @internal */
- abstract __run(context: Context): Promise;
-}
diff --git a/packages/common/src/library/libraries/abstracts/graphics.library.abstract.ts b/packages/common/src/library/libraries/abstracts/graphics.library.abstract.ts
deleted file mode 100644
index ed1baa9b..00000000
--- a/packages/common/src/library/libraries/abstracts/graphics.library.abstract.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { type Context } from "../../../context";
-import { type IGraphicsLibrary } from "../interfaces";
-import { Library } from "../library";
-
-/**
- * Abstract base class for graphics libraries.
- *
- * @remarks
- * Extend this class to implement a custom rendering backend and register it
- * with `NanoforgeClient.useGraphics`. The built-in implementation is
- * `Graphics2DLibrary`.
- */
-export abstract class BaseGraphicsLibrary extends Library implements IGraphicsLibrary {
- /** @internal */
- abstract __run(context: Context): Promise;
-}
diff --git a/packages/common/src/library/libraries/abstracts/index.ts b/packages/common/src/library/libraries/abstracts/index.ts
deleted file mode 100644
index b31b3db0..00000000
--- a/packages/common/src/library/libraries/abstracts/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export { BaseAssetManagerLibrary } from "./asset-manager.library.abstract";
-export { BaseComponentSystemLibrary } from "./component-system.library.abstract";
-export { BaseGraphicsLibrary } from "./graphics.library.abstract";
-export { BaseInputLibrary } from "./input.library.abstract";
-export { BaseSoundLibrary } from "./sound.library.abstract";
-export { BaseNetworkLibrary } from "./network.library.abstract";
-export { BaseMusicLibrary } from "./music.library.abstract";
diff --git a/packages/common/src/library/libraries/abstracts/input.library.abstract.ts b/packages/common/src/library/libraries/abstracts/input.library.abstract.ts
deleted file mode 100644
index 69256a41..00000000
--- a/packages/common/src/library/libraries/abstracts/input.library.abstract.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { type Context } from "../../../context";
-import { type IInputLibrary } from "../interfaces";
-import { Library } from "../library";
-
-/**
- * Abstract base class for input libraries.
- *
- * @remarks
- * Extend this class to implement a custom input back-end and register it with
- * `NanoforgeClient.useInput`. The built-in implementation is
- * `InputLibrary`.
- */
-export abstract class BaseInputLibrary extends Library implements IInputLibrary {
- /** @internal */
- public abstract __run(_context: Context): Promise;
-}
diff --git a/packages/common/src/library/libraries/abstracts/music.library.abstract.ts b/packages/common/src/library/libraries/abstracts/music.library.abstract.ts
deleted file mode 100644
index 71614876..00000000
--- a/packages/common/src/library/libraries/abstracts/music.library.abstract.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { type InitContext } from "../../../context";
-import { type IMusicLibrary } from "../interfaces";
-import { Library } from "../library";
-
-/**
- * Abstract base class for music libraries.
- *
- * @remarks
- * Extend this class to implement a custom music library and register it with
- * `NanoforgeApplication.use` using the `MUSIC_LIBRARY` symbol.
- * The built-in implementation is `MusicLibrary`.
- */
-export abstract class BaseMusicLibrary extends Library implements IMusicLibrary {
- /** @internal */
- public abstract override __init(context: InitContext): Promise;
-
- /**
- * Stop the currently playing track (if any) and start playing the requested one.
- *
- * @param sound - Key that identifies the music track (as passed to load).
- * @throws `NfNotFound` When no track is registered under the given key.
- * @throws `NfNotInitializedException` When called before `__init` has resolved.
- */
- public abstract play(sound: string): void;
-
- /**
- * Toggle the muted state of all loaded music tracks.
- *
- * @throws `NfNotInitializedException` When called before `__init` has resolved.
- */
- public abstract mute(): void;
-}
diff --git a/packages/common/src/library/libraries/abstracts/network.library.abstract.ts b/packages/common/src/library/libraries/abstracts/network.library.abstract.ts
deleted file mode 100644
index 1e793150..00000000
--- a/packages/common/src/library/libraries/abstracts/network.library.abstract.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { type INetworkLibrary } from "../interfaces";
-import { Library } from "../library";
-
-/**
- * Abstract base class for network libraries.
- *
- * @remarks
- * Extend this class to implement a custom network library and register it with
- * `NanoforgeApplication.useNetwork`. The built-in implementations are
- * `NetworkClientLibrary` and `NetworkServerLibrary`.
- */
-export abstract class BaseNetworkLibrary extends Library implements INetworkLibrary {}
diff --git a/packages/common/src/library/libraries/abstracts/sound.library.abstract.ts b/packages/common/src/library/libraries/abstracts/sound.library.abstract.ts
deleted file mode 100644
index 7e4ea5cb..00000000
--- a/packages/common/src/library/libraries/abstracts/sound.library.abstract.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { type InitContext } from "../../../context";
-import { type ISoundLibrary } from "../interfaces";
-import { Library } from "../library";
-
-/**
- * Abstract base class for sound libraries.
- *
- * @remarks
- * Extend this class to implement a custom sound library and register it with
- * `NanoforgeClient.useSound`. The built-in implementation is
- * `SoundLibrary`.
- */
-export abstract class BaseSoundLibrary extends Library implements ISoundLibrary {
- /** @internal */
- public abstract override __init(context: InitContext): Promise;
-
- /**
- * Play a previously loaded sound effect.
- *
- * @param sound - Key that identifies the sound (as passed to load).
- * @throws `NfNotFound` When no sound is registered under the given key.
- * @throws `NfNotInitializedException` When called before `__init` has resolved.
- */
- public abstract play(sound: string): void;
-
- /**
- * Toggle the muted state of all loaded sound effects.
- *
- * @throws `NfNotInitializedException` When called before `__init` has resolved.
- */
- public abstract mute(): void;
-}
diff --git a/packages/common/src/library/libraries/consts/index.ts b/packages/common/src/library/libraries/consts/index.ts
deleted file mode 100644
index e8609d9a..00000000
--- a/packages/common/src/library/libraries/consts/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./library-label.const";
diff --git a/packages/common/src/library/libraries/consts/library-label.const.ts b/packages/common/src/library/libraries/consts/library-label.const.ts
deleted file mode 100644
index d99b0150..00000000
--- a/packages/common/src/library/libraries/consts/library-label.const.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Registration symbol for the component-system (ECS) library slot.
- *
- * @remarks
- * Pass this symbol to `NanoforgeApplication.use` when registering a
- * custom component-system library, or use the shorthand
- * `NanoforgeApplication.useComponentSystem`.
- */
-export const COMPONENT_SYSTEM_LIBRARY = Symbol("COMPONENT_SYSTEM_LIBRARY");
-
-/**
- * Registration symbol for the graphics library slot.
- *
- * @remarks
- * Pass this symbol to `NanoforgeClient.useGraphics` or use it as a
- * dependency symbol in a custom library's `ILibraryOptions.dependencies`.
- */
-export const GRAPHICS_LIBRARY = Symbol("GRAPHICS_LIBRARY");
-
-/**
- * Registration symbol for the network library slot.
- *
- * @remarks
- * Pass this symbol to `NanoforgeApplication.useNetwork` or use it as a
- * dependency symbol in a custom library's `ILibraryOptions.dependencies`.
- */
-export const NETWORK_LIBRARY = Symbol("NETWORK_LIBRARY");
-
-/**
- * Registration symbol for the sound library slot.
- *
- * @remarks
- * Pass this symbol to `NanoforgeClient.useSound` or use it as a
- * dependency symbol in a custom library's `ILibraryOptions.dependencies`.
- */
-export const SOUND_LIBRARY = Symbol("SOUND_LIBRARY");
-
-/**
- * Registration symbol for the music library slot.
- *
- * @remarks
- * Pass this symbol to `NanoforgeApplication.use` with a custom music
- * library or use it as a dependency symbol in
- * `ILibraryOptions.dependencies`.
- */
-export const MUSIC_LIBRARY = Symbol("MUSIC_LIBRARY");
-
-/**
- * Registration symbol for the asset-manager library slot.
- *
- * @remarks
- * Pass this symbol to `NanoforgeApplication.useAssetManager` or use it
- * as a dependency in a custom library's `ILibraryOptions.dependencies`.
- */
-export const ASSET_MANAGER_LIBRARY = Symbol("ASSET_MANAGER_LIBRARY");
-
-/**
- * Registration symbol for the input library slot.
- *
- * @remarks
- * Pass this symbol to `NanoforgeClient.useInput` or use it as a
- * dependency symbol in a custom library's `ILibraryOptions.dependencies`.
- */
-export const INPUT_LIBRARY = Symbol("INPUT_LIBRARY");
diff --git a/packages/common/src/library/libraries/consts/library-options-default.const.ts b/packages/common/src/library/libraries/consts/library-options-default.const.ts
deleted file mode 100644
index 01698c15..00000000
--- a/packages/common/src/library/libraries/consts/library-options-default.const.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { type ILibraryOptions } from "../library.type";
-
-export const DEFAULT_LIBRARY_OPTIONS: ILibraryOptions = {
- dependencies: [],
- runBefore: [],
- runAfter: [],
-};
diff --git a/packages/common/src/library/libraries/index.ts b/packages/common/src/library/libraries/index.ts
deleted file mode 100644
index 8ce9096f..00000000
--- a/packages/common/src/library/libraries/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export type { ILibrary } from "./library.type";
-export { Library } from "./library";
-
-export * from "./abstracts";
-export * from "./consts";
-export * from "./interfaces";
diff --git a/packages/common/src/library/libraries/interfaces/bases/exposed.library.type.ts b/packages/common/src/library/libraries/interfaces/bases/exposed.library.type.ts
deleted file mode 100644
index fb2d18b6..00000000
--- a/packages/common/src/library/libraries/interfaces/bases/exposed.library.type.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { type ILibrary } from "../../library.type";
-
-/**
- * Marker interface for libraries that are directly accessible to game code via
- * the `ClientLibraryManager`.
- *
- * @remarks
- * All built-in typed library interfaces (e.g. `IAssetManagerLibrary`,
- * `ISoundLibrary`) extend this interface.
- */
-export interface IExposedLibrary extends ILibrary {}
diff --git a/packages/common/src/library/libraries/interfaces/bases/mutable.library.type.ts b/packages/common/src/library/libraries/interfaces/bases/mutable.library.type.ts
deleted file mode 100644
index aa5aee08..00000000
--- a/packages/common/src/library/libraries/interfaces/bases/mutable.library.type.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { type ILibrary } from "../../library.type";
-
-/**
- * Interface for libraries that support muting/unmuting audio playback.
- *
- * @remarks
- * Implemented by `ISoundLibrary` and `IMusicLibrary`.
- */
-export interface IMutableLibrary extends ILibrary {
- /**
- * Toggle the muted state. Mutes when currently unmuted, and vice versa.
- */
- mute(): void;
-}
diff --git a/packages/common/src/library/libraries/interfaces/bases/runner.library.type.ts b/packages/common/src/library/libraries/interfaces/bases/runner.library.type.ts
deleted file mode 100644
index ac6df49e..00000000
--- a/packages/common/src/library/libraries/interfaces/bases/runner.library.type.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { type Context } from "../../../../context";
-import { type ILibrary } from "../../library.type";
-
-/**
- * Interface for libraries that participate in the per-frame game loop.
- *
- * @remarks
- * The engine calls `__run` on every tick for all registered runner libraries
- * in the order determined by their `ILibraryOptions.runBefore` /
- * `ILibraryOptions.runAfter` relationships.
- */
-export interface IRunnerLibrary extends ILibrary {
- /** @internal */
- __run(context: Context): Promise;
-}
diff --git a/packages/common/src/library/libraries/interfaces/finals/asset-manager.library.type.ts b/packages/common/src/library/libraries/interfaces/finals/asset-manager.library.type.ts
deleted file mode 100644
index 216708bb..00000000
--- a/packages/common/src/library/libraries/interfaces/finals/asset-manager.library.type.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { type NfFile } from "../../../../common";
-import { type IExposedLibrary } from "../bases/exposed.library.type";
-
-/**
- * Interface for asset-manager libraries.
- *
- * @remarks
- * Implemented by `BaseAssetManagerLibrary` and its concrete subclass
- * `AssetManagerLibrary`. Access an instance via
- * `ctx.libraries.getAssetManager().library`.
- */
-export interface IAssetManagerLibrary extends IExposedLibrary {
- /**
- * Retrieve a registered file asset by its virtual path.
- *
- * @param path - Virtual path of the asset (e.g. "/textures/hero.png").
- * @returns An `NfFile` handle for the requested asset, or `undefined` if no asset path is provided.
- * @throws `NfNotFound` When no asset is registered at the given path.
- */
- getAsset(path: string): NfFile;
- getAsset(path: "" | undefined): undefined;
-}
diff --git a/packages/common/src/library/libraries/interfaces/finals/component-system.library.type.ts b/packages/common/src/library/libraries/interfaces/finals/component-system.library.type.ts
deleted file mode 100644
index 8208b886..00000000
--- a/packages/common/src/library/libraries/interfaces/finals/component-system.library.type.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { type IExposedLibrary } from "../bases/exposed.library.type";
-import { type IRunnerLibrary } from "../bases/runner.library.type";
-
-/**
- * Interface for component-system (ECS) libraries.
- *
- * @remarks
- * Implemented by `BaseComponentSystemLibrary` and the higher-level
- * `AbstractECSLibrary`, with concrete subclasses
- * `ECSClientLibrary` and `ECSServerLibrary`. Access an instance
- * via `ctx.libraries.getComponentSystem().library`.
- */
-export interface IComponentSystemLibrary extends IExposedLibrary, IRunnerLibrary {}
diff --git a/packages/common/src/library/libraries/interfaces/finals/graphics.library.type.ts b/packages/common/src/library/libraries/interfaces/finals/graphics.library.type.ts
deleted file mode 100644
index 9509a9dd..00000000
--- a/packages/common/src/library/libraries/interfaces/finals/graphics.library.type.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { type IExposedLibrary } from "../bases/exposed.library.type";
-import { type IRunnerLibrary } from "../bases/runner.library.type";
-
-/**
- * Interface for graphics libraries.
- *
- * @remarks
- * Implemented by `BaseGraphicsLibrary` and its concrete subclass
- * `Graphics2DLibrary`. Access an instance via
- * `ctx.libraries.getGraphics().library`.
- */
-export interface IGraphicsLibrary extends IExposedLibrary, IRunnerLibrary {}
diff --git a/packages/common/src/library/libraries/interfaces/finals/input.library.type.ts b/packages/common/src/library/libraries/interfaces/finals/input.library.type.ts
deleted file mode 100644
index 9dc79452..00000000
--- a/packages/common/src/library/libraries/interfaces/finals/input.library.type.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { type IExposedLibrary } from "../bases/exposed.library.type";
-import { type IRunnerLibrary } from "../bases/runner.library.type";
-
-/**
- * Interface for input libraries.
- *
- * @remarks
- * Implemented by `BaseInputLibrary` and its concrete subclass
- * `InputLibrary`. Access an instance via
- * `ctx.libraries.getInput().library`.
- */
-export interface IInputLibrary extends IExposedLibrary, IRunnerLibrary {}
diff --git a/packages/common/src/library/libraries/interfaces/finals/music.library.type.ts b/packages/common/src/library/libraries/interfaces/finals/music.library.type.ts
deleted file mode 100644
index 653e918d..00000000
--- a/packages/common/src/library/libraries/interfaces/finals/music.library.type.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { type IExposedLibrary } from "../bases/exposed.library.type";
-import { type IMutableLibrary } from "../bases/mutable.library.type";
-
-/**
- * Interface for music libraries.
- *
- * @remarks
- * Implemented by `BaseMusicLibrary` and its concrete subclass
- * `MusicLibrary`. Access an instance via
- * `ctx.libraries.getMusic().library`.
- */
-export interface IMusicLibrary extends IExposedLibrary, IMutableLibrary {}
diff --git a/packages/common/src/library/libraries/interfaces/finals/network.library.type.ts b/packages/common/src/library/libraries/interfaces/finals/network.library.type.ts
deleted file mode 100644
index 80d0bca5..00000000
--- a/packages/common/src/library/libraries/interfaces/finals/network.library.type.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { type IExposedLibrary } from "../bases/exposed.library.type";
-
-/**
- * Interface for network libraries.
- *
- * @remarks
- * Implemented by `BaseNetworkLibrary` and its concrete subclasses
- * `NetworkClientLibrary` and `NetworkServerLibrary`. Access an
- * instance via `ctx.libraries.getNetwork().library`.
- */
-export interface INetworkLibrary extends IExposedLibrary {}
diff --git a/packages/common/src/library/libraries/interfaces/finals/sound.library.type.ts b/packages/common/src/library/libraries/interfaces/finals/sound.library.type.ts
deleted file mode 100644
index 8ce0fb79..00000000
--- a/packages/common/src/library/libraries/interfaces/finals/sound.library.type.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { type IExposedLibrary } from "../bases/exposed.library.type";
-import { type IMutableLibrary } from "../bases/mutable.library.type";
-
-/**
- * Interface for sound-effect libraries.
- *
- * @remarks
- * Implemented by `BaseSoundLibrary` and its concrete subclass
- * `SoundLibrary`. Access an instance via
- * `ctx.libraries.getSound().library`.
- */
-export interface ISoundLibrary extends IExposedLibrary, IMutableLibrary {}
diff --git a/packages/common/src/library/libraries/interfaces/index.ts b/packages/common/src/library/libraries/interfaces/index.ts
deleted file mode 100644
index 226fe953..00000000
--- a/packages/common/src/library/libraries/interfaces/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export type { IExposedLibrary } from "./bases/exposed.library.type";
-export type { IRunnerLibrary } from "./bases/runner.library.type";
-export type { IAssetManagerLibrary } from "./finals/asset-manager.library.type";
-export type { IComponentSystemLibrary } from "./finals/component-system.library.type";
-export type { IGraphicsLibrary } from "./finals/graphics.library.type";
-export type { IInputLibrary } from "./finals/input.library.type";
-export type { ISoundLibrary } from "./finals/sound.library.type";
-export type { INetworkLibrary } from "./finals/network.library.type";
-export type { IMusicLibrary } from "./finals/music.library.type";
-export type { IMutableLibrary } from "./bases/mutable.library.type";
diff --git a/packages/common/src/library/libraries/library.ts b/packages/common/src/library/libraries/library.ts
deleted file mode 100644
index e7624558..00000000
--- a/packages/common/src/library/libraries/library.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { type ClearContext, type InitContext } from "../../context";
-import { NfNotInitializedException } from "../../exception";
-import { RelationshipHandler } from "../relationship/relationship-handler";
-import { DEFAULT_LIBRARY_OPTIONS } from "./consts/library-options-default.const";
-import { type ILibrary, type ILibraryOptions } from "./library.type";
-
-/**
- * Base class for all NanoForge libraries.
- *
- * @remarks
- * Extend this class (or one of its typed subclasses such as
- * `BaseAssetManagerLibrary`, `BaseSoundLibrary`, etc.) to create a
- * custom library. At minimum you must implement the `ILibrary.__name`
- * getter and register the library with the engine before calling
- * `NanoforgeApplication.init`.
- *
- * @example
- * ```ts
- * import `Library, type InitContext ` from "@nanoforge-dev/common";
- *
- * const MY_LIBRARY = Symbol("MY_LIBRARY");
- *
- * class MyLibrary extends Library {
- * get __name() `return "MyLibrary"; `
- *
- * override async __init(ctx: InitContext) {
- * // one-time setup here
- * }
- * }
- *
- * const client = NanoforgeFactory.createClient();
- * client.use(MY_LIBRARY, new MyLibrary());
- * await client.init(`container, files, env `);
- * ```
- */
-export abstract class Library implements ILibrary {
- protected _relationship: RelationshipHandler;
-
- /**
- * @param rawOptions - Optional dependency and execution-order overrides.
- */
- constructor(rawOptions?: Partial) {
- const options = {
- ...DEFAULT_LIBRARY_OPTIONS,
- ...rawOptions,
- };
-
- this._relationship = new RelationshipHandler(
- options.dependencies,
- options.runBefore,
- options.runAfter,
- );
- }
-
- /** @internal */
- get __relationship(): RelationshipHandler {
- return this._relationship;
- }
-
- /** @internal */
- abstract get __name(): string;
-
- /** @internal */
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- public async __init(_context: InitContext): Promise {}
-
- /** @internal */
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- public async __clear(_context: ClearContext): Promise {}
-
- /** @internal */
- protected throwNotInitializedError(): never {
- throw new NfNotInitializedException(this.__name, "Library");
- }
-}
diff --git a/packages/common/src/library/libraries/library.type.ts b/packages/common/src/library/libraries/library.type.ts
deleted file mode 100644
index 35591bf2..00000000
--- a/packages/common/src/library/libraries/library.type.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { type ClearContext, type InitContext } from "../../context";
-import { type RelationshipHandler } from "../relationship/relationship-handler";
-
-/**
- * Core interface that every NanoForge library must satisfy.
- *
- * @remarks
- * Implement this interface (or extend an abstract base such as `BaseAssetManagerLibrary`)
- * to create a custom library and register it with the engine via
- * `NanoforgeApplication.use`.
- */
-export interface ILibrary {
- /** @internal */
- get __name(): string;
-
- /** @internal */
- get __relationship(): RelationshipHandler;
-
- /** @internal */
- __init(context: InitContext): Promise;
-
- /** @internal */
- __clear(context: ClearContext): Promise;
-}
-
-/**
- * Options that control how a library participates in the engine's
- * initialisation and execution ordering.
- */
-export interface ILibraryOptions {
- /**
- * Symbols of libraries that must be fully initialised before this library
- * can be used.
- *
- * @remarks
- * The engine will reject configurations where a dependency is missing.
- */
- dependencies: symbol[];
-
- /**
- * Symbols of libraries whose `__run` method should be called *after* this
- * library's `__run`.
- */
- runBefore: symbol[];
-
- /**
- * Symbols of libraries whose `__run` method must complete *before* this
- * library's `__run` is invoked.
- */
- runAfter: symbol[];
-}
diff --git a/packages/common/src/library/library.ts b/packages/common/src/library/library.ts
new file mode 100644
index 00000000..189857b0
--- /dev/null
+++ b/packages/common/src/library/library.ts
@@ -0,0 +1,92 @@
+import type { Context, InitContext } from "../context";
+import { NfNotInitializedException } from "../exception";
+import { DEFAULT_LIBRARY_RELATIONSHIPS, type LibraryRelationships } from "./relationship.type";
+
+/**
+ * Base class for all NanoForge libraries.
+ *
+ * @remarks
+ * Extend this class to create a custom library, then register an instance
+ * with `app.use(new MyLibrary())`. A library owns its own context key (see
+ * `defineLibraryKey`), which is simultaneously its registry identity, its
+ * slot on `Context`, and the identifier used in `dependencies`/`runBefore`/
+ * `runAfter`.
+ *
+ * @example
+ * ```ts
+ * import { Library, defineLibraryKey, type InitContext } from "@nanoforge-dev/common";
+ *
+ * class MyLibrary extends Library {
+ * readonly key = defineLibraryKey("my");
+ *
+ * override async __init(ctx: InitContext) {
+ * // one-time setup here
+ * }
+ * }
+ *
+ * app.use(new MyLibrary());
+ * ```
+ */
+export abstract class Library {
+ /**
+ * This library's identity: its slot on `Context`, its registry key, and
+ * the identifier used to reference it in ordering relationships.
+ */
+ abstract readonly key: string;
+
+ private readonly _relationships: LibraryRelationships;
+
+ /**
+ * @param relationships - Optional ordering overrides, keyed by other
+ * libraries' `key`.
+ */
+ constructor(relationships?: Partial) {
+ this._relationships = {
+ ...DEFAULT_LIBRARY_RELATIONSHIPS,
+ ...relationships,
+ };
+ }
+
+ /** @internal */
+ get relationships(): LibraryRelationships {
+ return this._relationships;
+ }
+
+ /** Called once, in dependency order, before the tick loop starts. */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public async __init(_ctx: InitContext): Promise {}
+
+ /**
+ * Called every tick, in run order, whether or not the tick loop is
+ * paused — unlike `__run`. Use this for event/message draining that must
+ * not be affected by pause (e.g. an editor bridge listening for the
+ * "resume" command itself), not for gameplay simulation.
+ */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public async __events(_ctx: Context): Promise {}
+
+ /** Called every tick, in run order, while the tick loop is active and not paused. */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public async __run(_ctx: Context): Promise {}
+
+ /** Called once after the tick loop stops. */
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public async __clear(_ctx: Context): Promise {}
+
+ /**
+ * Returns this library's public contribution to `Context[this.key]`.
+ *
+ * @remarks
+ * Return a plain object exposing only what game code should be able to
+ * reach — never `this` — so internal state and lifecycle hooks never leak
+ * onto `Context`. Libraries with nothing to expose (headless libraries)
+ * can leave this unimplemented; they simply won't appear on `Context`.
+ */
+ expose(): unknown {
+ return undefined;
+ }
+
+ protected throwNotInitializedError(): never {
+ throw new NfNotInitializedException(this.key, "Library");
+ }
+}
diff --git a/packages/common/src/library/manager/handle/library.handle.ts b/packages/common/src/library/manager/handle/library.handle.ts
deleted file mode 100644
index 32bf268c..00000000
--- a/packages/common/src/library/manager/handle/library.handle.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { type LibraryContext } from "../../../context";
-import { type ILibrary } from "../../libraries";
-
-/**
- * An opaque handle returned by `LibraryManager` accessors.
- *
- * @remarks
- * Bundles the library instance together with its registration symbol and
- * lifecycle context. Prefer using the strongly-typed accessor methods on
- * `LibraryManager` or `ClientLibraryManager` rather than
- * constructing handles directly.
- *
- * @typeParam T - Concrete library type (defaults to `ILibrary`).
- */
-export class LibraryHandle {
- private readonly _symbol: symbol;
- private readonly _library: T;
- private readonly _context: LibraryContext;
-
- /**
- * @param sym - Registration symbol for this library.
- * @param library - The library instance.
- * @param defaultContext - Initial lifecycle context.
- */
- constructor(sym: symbol, library: T, defaultContext: LibraryContext) {
- this._symbol = sym;
- this._library = library;
- this._context = defaultContext;
- }
-
- /**
- * The registration symbol used to look up this library.
- */
- get symbol(): symbol {
- return this._symbol;
- }
-
- /**
- * The underlying library instance.
- */
- get library(): T {
- return this._library;
- }
-
- /**
- * Current lifecycle context tracking the library's status.
- */
- get context(): LibraryContext {
- return this._context;
- }
-}
diff --git a/packages/common/src/library/manager/index.ts b/packages/common/src/library/manager/index.ts
deleted file mode 100644
index e44f1e38..00000000
--- a/packages/common/src/library/manager/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { LibraryHandle } from "./handle/library.handle";
-export { ClientLibraryManager } from "./managers/client-library.manager";
-export { DefaultLibrariesEnum, LibraryManager } from "./managers/library.manager";
diff --git a/packages/common/src/library/manager/managers/base-library.manager.ts b/packages/common/src/library/manager/managers/base-library.manager.ts
deleted file mode 100644
index fe8be8cc..00000000
--- a/packages/common/src/library/manager/managers/base-library.manager.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { type LibraryContext } from "../../../context";
-import { type ILibrary } from "../../libraries";
-import { LibraryHandle } from "../handle/library.handle";
-import { DefaultLibrariesEnum } from "./library.manager";
-
-export class BaseLibraryManager {
- protected _libraries: LibraryHandle[] = [];
- private _librariesIndex: Record = {};
-
- /**
- * @todo Add error management
- */
- public get(sym: symbol): LibraryHandle {
- const index = this._librariesIndex[sym];
- if (!index) throw new Error(`Library not found: ${Symbol.keyFor(sym)}`);
- return this._get(index, sym);
- }
-
- protected setNewLibrary(sym: symbol, library: ILibrary, context: LibraryContext): void {
- const index = Math.max(this._libraries.length, Object.keys(DefaultLibrariesEnum).length);
- this._setIndex(sym, index);
- this._set(index, sym, library, context);
- }
-
- protected _get(index: number, sym?: symbol): LibraryHandle {
- const result = this._libraries[index];
- if (!result) throw new Error(`Library not found: ${sym ? Symbol.keyFor(sym) : index}`);
- return result as LibraryHandle;
- }
-
- protected _set(index: number, sym: symbol, library: ILibrary, context: LibraryContext): void {
- this._libraries[index] = new LibraryHandle(sym, library, context);
- }
-
- protected _setIndex(sym: symbol, index: number): void {
- if (Symbol.keyFor(sym)) this._librariesIndex[sym] = index;
- }
-}
diff --git a/packages/common/src/library/manager/managers/client-library.manager.ts b/packages/common/src/library/manager/managers/client-library.manager.ts
deleted file mode 100644
index 44127935..00000000
--- a/packages/common/src/library/manager/managers/client-library.manager.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import {
- type IAssetManagerLibrary,
- type IComponentSystemLibrary,
- type IGraphicsLibrary,
- type IInputLibrary,
- type ILibrary,
- type IMusicLibrary,
- type INetworkLibrary,
- type ISoundLibrary,
-} from "../../libraries";
-import { type LibraryManager } from "./library.manager";
-
-/**
- * Convenience wrapper around `LibraryManager` that unwraps
- * `LibraryHandle` values and returns the library instance directly.
- *
- * @remarks
- * Typically accessed inside a library's lifecycle hooks through the execution
- * context's `libraries` property after being constructed with a concrete
- * `LibraryManager`.
- */
-export class ClientLibraryManager {
- private readonly _libraryManager: LibraryManager;
-
- /**
- * @param libraryManager - The underlying manager to delegate lookups to.
- */
- constructor(libraryManager: LibraryManager) {
- this._libraryManager = libraryManager;
- }
-
- /**
- * Retrieve a library registered under a custom symbol.
- *
- * @typeParam T - Expected library type.
- * @param sym - The symbol the library was registered with.
- * @returns The unwrapped library instance.
- */
- public get(sym: symbol): T {
- return this._libraryManager.get(sym).library;
- }
-
- /**
- * Retrieve the registered component-system (ECS) library instance.
- *
- * @typeParam T - Concrete component-system library type.
- */
- public getComponentSystem(): T {
- return this._libraryManager.getComponentSystem().library;
- }
-
- /**
- * Retrieve the registered graphics library instance.
- *
- * @typeParam T - Concrete graphics library type.
- */
- public getGraphics(): T {
- return this._libraryManager.getGraphics().library;
- }
-
- /**
- * Retrieve the registered network library instance.
- *
- * @typeParam T - Concrete network library type.
- */
- public getNetwork(): T {
- return this._libraryManager.getNetwork().library;
- }
-
- /**
- * Retrieve the registered input library instance.
- *
- * @typeParam T - Concrete input library type.
- */
- public getInput(): T {
- return this._libraryManager.getInput().library;
- }
-
- /**
- * Retrieve the registered asset-manager library instance.
- *
- * @typeParam T - Concrete asset-manager library type.
- */
- public getAssetManager(): T {
- return this._libraryManager.getAssetManager().library;
- }
-
- /**
- * Retrieve the registered sound library instance.
- *
- * @typeParam T - Concrete sound library type.
- */
- public getSound(): T {
- return this._libraryManager.getSound().library;
- }
-
- /**
- * Retrieve the registered music library instance.
- *
- * @typeParam T - Concrete music library type.
- */
- public getMusic(): T {
- return this._libraryManager.getMusic().library;
- }
-}
diff --git a/packages/common/src/library/manager/managers/library.manager.ts b/packages/common/src/library/manager/managers/library.manager.ts
deleted file mode 100644
index 22c7df83..00000000
--- a/packages/common/src/library/manager/managers/library.manager.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import {
- ASSET_MANAGER_LIBRARY,
- COMPONENT_SYSTEM_LIBRARY,
- GRAPHICS_LIBRARY,
- type IAssetManagerLibrary,
- type IComponentSystemLibrary,
- type IGraphicsLibrary,
- type IInputLibrary,
- type IMusicLibrary,
- type INetworkLibrary,
- type ISoundLibrary,
- MUSIC_LIBRARY,
- NETWORK_LIBRARY,
- SOUND_LIBRARY,
-} from "../../libraries";
-import { type LibraryHandle } from "../handle/library.handle";
-import { BaseLibraryManager } from "./base-library.manager";
-
-/**
- * Numeric index keys for the built-in library slots.
- *
- * @remarks
- * Used internally by `LibraryManager` to keep O(1) access to the well-known
- * library positions. Game code should not reference these values directly —
- * use the typed accessor methods on `LibraryManager` instead.
- */
-export enum DefaultLibrariesEnum {
- ASSET_MANAGER,
- COMPONENT_SYSTEM,
- GRAPHICS,
- INPUT,
- NETWORK,
- SOUND,
- MUSIC,
-}
-
-const DEFAULT_LIBRARIES: { index: DefaultLibrariesEnum; sym: symbol }[] = [
- { index: DefaultLibrariesEnum.ASSET_MANAGER, sym: ASSET_MANAGER_LIBRARY },
- { index: DefaultLibrariesEnum.COMPONENT_SYSTEM, sym: COMPONENT_SYSTEM_LIBRARY },
- { index: DefaultLibrariesEnum.GRAPHICS, sym: GRAPHICS_LIBRARY },
- { index: DefaultLibrariesEnum.NETWORK, sym: NETWORK_LIBRARY },
- { index: DefaultLibrariesEnum.SOUND, sym: SOUND_LIBRARY },
- { index: DefaultLibrariesEnum.MUSIC, sym: MUSIC_LIBRARY },
-];
-
-/**
- * Central registry for all libraries registered with a NanoForge application.
- *
- * @remarks
- * An instance is available inside every lifecycle context via
- * `BaseContext.libraries`. Use the typed accessor methods to retrieve
- * any built-in library, or call `BaseLibraryManager.get` with a custom
- * symbol for user-defined libraries.
- *
- * @example
- * ```ts
- * // Inside a library's __init hook:
- * override async __init(ctx: InitContext) {
- * const assetMgr = ctx.libraries.getAssetManager().library;
- * const file = assetMgr.getAsset("/data.json");
- * }
- * ```
- */
-export class LibraryManager extends BaseLibraryManager {
- constructor() {
- super();
-
- for (const { index, sym } of DEFAULT_LIBRARIES) {
- this._setIndex(sym, index);
- }
- }
-
- /**
- * Retrieve the registered component-system (ECS) library.
- *
- * @typeParam T - Concrete component-system library type.
- * @returns A `LibraryHandle` wrapping the library instance.
- */
- public getComponentSystem<
- T extends IComponentSystemLibrary = IComponentSystemLibrary,
- >(): LibraryHandle {
- return this._get(DefaultLibrariesEnum.COMPONENT_SYSTEM);
- }
-
- /**
- * Retrieve the registered graphics library.
- *
- * @typeParam T - Concrete graphics library type.
- * @returns A `LibraryHandle` wrapping the library instance.
- */
- public getGraphics(): LibraryHandle {
- return this._get(DefaultLibrariesEnum.GRAPHICS);
- }
-
- /**
- * Retrieve the registered network library.
- *
- * @typeParam T - Concrete network library type.
- * @returns A `LibraryHandle` wrapping the library instance.
- */
- public getNetwork(): LibraryHandle {
- return this._get(DefaultLibrariesEnum.NETWORK);
- }
-
- /**
- * Retrieve the registered input library.
- *
- * @typeParam T - Concrete input library type.
- * @returns A `LibraryHandle` wrapping the library instance.
- */
- public getInput(): LibraryHandle {
- return this._get(DefaultLibrariesEnum.INPUT);
- }
-
- /**
- * Retrieve the registered asset-manager library.
- *
- * @typeParam T - Concrete asset-manager library type.
- * @returns A `LibraryHandle` wrapping the library instance.
- */
- public getAssetManager<
- T extends IAssetManagerLibrary = IAssetManagerLibrary,
- >(): LibraryHandle {
- return this._get(DefaultLibrariesEnum.ASSET_MANAGER);
- }
-
- /**
- * Retrieve the registered sound library.
- *
- * @typeParam T - Concrete sound library type.
- * @returns A `LibraryHandle` wrapping the library instance.
- */
- public getSound(): LibraryHandle {
- return this._get(DefaultLibrariesEnum.SOUND);
- }
-
- /**
- * Retrieve the registered music library.
- *
- * @typeParam T - Concrete music library type.
- * @returns A `LibraryHandle` wrapping the library instance.
- */
- public getMusic(): LibraryHandle {
- return this._get(DefaultLibrariesEnum.MUSIC);
- }
-}
diff --git a/packages/common/src/library/relationship.type.ts b/packages/common/src/library/relationship.type.ts
new file mode 100644
index 00000000..2d68b80e
--- /dev/null
+++ b/packages/common/src/library/relationship.type.ts
@@ -0,0 +1,18 @@
+/**
+ * Ordering relationships between a library and other libraries, referenced
+ * by their `key`.
+ */
+export interface LibraryRelationships {
+ /** Keys of libraries that must be initialized before this one. */
+ dependencies: string[];
+ /** Keys of libraries this one must run before, each tick. */
+ runBefore: string[];
+ /** Keys of libraries this one must run after, each tick. */
+ runAfter: string[];
+}
+
+export const DEFAULT_LIBRARY_RELATIONSHIPS: LibraryRelationships = {
+ dependencies: [],
+ runBefore: [],
+ runAfter: [],
+};
diff --git a/packages/common/src/library/relationship/relationship-handler.ts b/packages/common/src/library/relationship/relationship-handler.ts
deleted file mode 100644
index 4c5ae313..00000000
--- a/packages/common/src/library/relationship/relationship-handler.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-export class RelationshipHandler {
- private readonly _dependencies: symbol[];
- private readonly _runBefore: symbol[];
- private readonly _runAfter: symbol[];
-
- /**
- * Constructor for RelationshipHandler
- *
- * @param dependencies - Dependencies of the library
- * @param runBefore - Libraries needed to run before this one
- * @param runAfter - Libraries needed to run after this one
- */
- constructor(dependencies: symbol[], runBefore: symbol[], runAfter: symbol[]) {
- this._dependencies = dependencies;
- this._runBefore = runBefore;
- this._runAfter = runAfter;
- }
-
- get dependencies(): symbol[] {
- return this._dependencies;
- }
-
- get runBefore(): symbol[] {
- return this._runBefore;
- }
-
- get runAfter(): symbol[] {
- return this._runAfter;
- }
-}
diff --git a/packages/common/src/options/index.ts b/packages/common/src/options/index.ts
deleted file mode 100644
index 32ab2502..00000000
--- a/packages/common/src/options/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-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
deleted file mode 100644
index b24150ec..00000000
--- a/packages/common/src/options/types/options.type.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Union of client and server run options accepted by
- * `NanoforgeApplication.init`.
- */
-export type IRunOptions = IRunClientOptions | IRunServerOptions;
-
-/**
- * Run options for a client-side NanoForge application.
- *
- * @remarks
- * Pass an instance to `NanoforgeApplication.init` when running in the
- * browser. The engine bundler (`nf build`) automatically generates and
- * injects `files` and `env` at build time.
- */
-export interface IRunClientOptions {
- /**
- * DOM element that will host the game canvas.
- *
- * @remarks
- * The element's current `offsetWidth` and `offsetHeight` are used to set
- * the initial canvas dimensions.
- */
- container: HTMLDivElement;
-
- /**
- * Map of virtual file paths to their resolved URL strings.
- *
- * @remarks
- * Populated automatically by the NanoForge bundler. Keys are normalised
- * paths such as `"/textures/hero.png"`.
- */
- files: Map;
-
- /**
- * Runtime environment variables available to all libraries via
- * `InitContext.env`.
- */
- env: Record;
-}
-
-/**
- * Run options for a server-side NanoForge application.
- *
- * @remarks
- * Pass an instance to `NanoforgeApplication.init` when running in a
- * Node.js server process.
- */
-export interface IRunServerOptions {
- /**
- * Map of virtual file paths to their resolved URL strings.
- *
- * @remarks
- * Populated automatically by the NanoForge bundler. Keys are normalised
- * paths such as `"/data/config.json"`.
- */
- files: Map;
-
- /**
- * Runtime environment variables available to all libraries via
- * `InitContext.env`.
- */
- env: Record;
-}
diff --git a/packages/common/test/define-library-key.spec-d.ts b/packages/common/test/define-library-key.spec-d.ts
new file mode 100644
index 00000000..71896803
--- /dev/null
+++ b/packages/common/test/define-library-key.spec-d.ts
@@ -0,0 +1,16 @@
+import { describe, it } from "vitest";
+
+import { defineLibraryKey } from "../src";
+
+describe("defineLibraryKey (type-level)", () => {
+ it("accepts keys already present on Context", () => {
+ defineLibraryKey("assets");
+ defineLibraryKey("app");
+ defineLibraryKey("vars");
+ });
+
+ it("rejects keys that are not part of Context", () => {
+ // @ts-expect-error "unknown-key" is not a member of Context
+ defineLibraryKey("unknown-key");
+ });
+});
diff --git a/packages/common/test/define-library-key.spec.ts b/packages/common/test/define-library-key.spec.ts
new file mode 100644
index 00000000..e7a2f900
--- /dev/null
+++ b/packages/common/test/define-library-key.spec.ts
@@ -0,0 +1,9 @@
+import { describe, expect, it } from "vitest";
+
+import { defineLibraryKey } from "../src";
+
+describe("defineLibraryKey", () => {
+ it("returns the given key unchanged", () => {
+ expect(defineLibraryKey("assets")).toBe("assets");
+ });
+});
diff --git a/packages/common/test/exceptions.spec.ts b/packages/common/test/exceptions.spec.ts
index 4f82f289..36866bb9 100644
--- a/packages/common/test/exceptions.spec.ts
+++ b/packages/common/test/exceptions.spec.ts
@@ -1,6 +1,11 @@
import { describe, expect, it } from "vitest";
-import { NfConfigException, NfFetchException, NfNotFound, NfNotInitializedException } from "../src";
+import {
+ NfDuplicateLibraryException,
+ NfFetchException,
+ NfNotFound,
+ NfNotInitializedException,
+} from "../src";
describe("NfNotInitializedException", () => {
it("should have code 404", () => {
@@ -78,28 +83,20 @@ describe("NfFetchException", () => {
});
});
-describe("NfConfigException", () => {
- it("should have code 400", () => {
- expect(new NfConfigException("bad value").code).toBe(400);
+describe("NfDuplicateLibraryException", () => {
+ it("should have code 409", () => {
+ expect(new NfDuplicateLibraryException("ecs").code).toBe(409);
});
- it("should include message in output", () => {
- expect(new NfConfigException("bad value").message).toContain("bad value");
- });
-
- it("should include library name when provided", () => {
- expect(new NfConfigException("bad value", "MyLib").message).toContain("(MyLib)");
- });
-
- it("should omit library name when not provided", () => {
- expect(new NfConfigException("bad value").message).not.toContain("(");
+ it("should include the key in message", () => {
+ expect(new NfDuplicateLibraryException("ecs").message).toContain('"ecs"');
});
it("should have [NANOFORGE] prefix", () => {
- expect(new NfConfigException("bad value").message).toMatch(/^\[NANOFORGE]/);
+ expect(new NfDuplicateLibraryException("ecs").message).toMatch(/^\[NANOFORGE]/);
});
it("should be an instance of Error", () => {
- expect(new NfConfigException("bad value")).toBeInstanceOf(Error);
+ expect(new NfDuplicateLibraryException("ecs")).toBeInstanceOf(Error);
});
});
diff --git a/packages/common/test/library.spec.ts b/packages/common/test/library.spec.ts
index 9f8c3489..7c8900a3 100644
--- a/packages/common/test/library.spec.ts
+++ b/packages/common/test/library.spec.ts
@@ -1,103 +1,44 @@
-import { beforeEach, describe, expect, it } from "vitest";
+import { describe, expect, it } from "vitest";
-import {
- LibraryContext,
- LibraryHandle,
- LibraryStatusEnum,
- NfNotInitializedException,
-} from "../src";
-import { Library } from "../src/library/libraries/library";
-import { BaseLibraryManager } from "../src/library/manager/managers/base-library.manager";
-import { RelationshipHandler } from "../src/library/relationship/relationship-handler";
+import { Library, NfNotInitializedException, defineLibraryKey } from "../src";
+import type { Context, InitContext } from "../src";
class TestLibrary extends Library {
- private readonly _name: string;
-
- constructor(name = "TestLibrary", options?: ConstructorParameters[0]) {
- super(options);
- this._name = name;
- }
-
- get __name(): string {
- return this._name;
- }
+ readonly key = defineLibraryKey("assets");
public expose_throwNotInitializedError(): never {
return this.throwNotInitializedError();
}
}
-class TestableBaseLibraryManager extends BaseLibraryManager {
- public expose_setNewLibrary(sym: symbol, library: TestLibrary, context: LibraryContext): void {
- this.setNewLibrary(sym, library, context);
- }
-}
-
-describe("RelationshipHandler", () => {
- it("should store dependencies", () => {
- const dep = Symbol("dep");
- const handler = new RelationshipHandler([dep], [], []);
- expect(handler.dependencies).toContain(dep);
- });
-
- it("should store runBefore", () => {
- const before = Symbol("before");
- const handler = new RelationshipHandler([], [before], []);
- expect(handler.runBefore).toContain(before);
- });
-
- it("should store runAfter", () => {
- const after = Symbol("after");
- const handler = new RelationshipHandler([], [], [after]);
- expect(handler.runAfter).toContain(after);
- });
-
- it("should return empty arrays when initialised with empty arrays", () => {
- const handler = new RelationshipHandler([], [], []);
- expect(handler.dependencies).toEqual([]);
- expect(handler.runBefore).toEqual([]);
- expect(handler.runAfter).toEqual([]);
- });
-
- it("should store multiple symbols", () => {
- const a = Symbol("a");
- const b = Symbol("b");
- const handler = new RelationshipHandler([a, b], [], []);
- expect(handler.dependencies).toEqual([a, b]);
- });
-});
-
describe("Library", () => {
describe("defaults", () => {
it("should have empty relationship arrays when no options are given", () => {
const lib = new TestLibrary();
- expect(lib.__relationship.dependencies).toEqual([]);
- expect(lib.__relationship.runBefore).toEqual([]);
- expect(lib.__relationship.runAfter).toEqual([]);
+ expect(lib.relationships.dependencies).toEqual([]);
+ expect(lib.relationships.runBefore).toEqual([]);
+ expect(lib.relationships.runAfter).toEqual([]);
});
- it("should expose the correct __name", () => {
- expect(new TestLibrary("MyLib").__name).toBe("MyLib");
+ it("should default expose() to undefined", () => {
+ expect(new TestLibrary().expose()).toBeUndefined();
});
});
describe("options", () => {
it("should use partial dependencies option", () => {
- const dep = Symbol("dep");
- const lib = new TestLibrary("TestLibrary", { dependencies: [dep] });
- expect(lib.__relationship.dependencies).toContain(dep);
+ const lib = new TestLibrary({ dependencies: ["other"] });
+ expect(lib.relationships.dependencies).toEqual(["other"]);
});
it("should use partial runBefore option", () => {
- const before = Symbol("before");
- const lib = new TestLibrary("TestLibrary", { runBefore: [before] });
- expect(lib.__relationship.runBefore).toContain(before);
+ const lib = new TestLibrary({ runBefore: ["other"] });
+ expect(lib.relationships.runBefore).toEqual(["other"]);
});
it("should use partial runAfter option", () => {
- const after = Symbol("after");
- const lib = new TestLibrary("TestLibrary", { runAfter: [after] });
- expect(lib.__relationship.runAfter).toContain(after);
+ const lib = new TestLibrary({ runAfter: ["other"] });
+ expect(lib.relationships.runAfter).toEqual(["other"]);
});
});
@@ -107,78 +48,23 @@ describe("Library", () => {
expect(() => lib.expose_throwNotInitializedError()).toThrow(NfNotInitializedException);
});
- it("should include the library name in the error message", () => {
- const lib = new TestLibrary("MySpecialLib");
- expect(() => lib.expose_throwNotInitializedError()).toThrow(/MySpecialLib/);
+ it("should include the library key in the error message", () => {
+ const lib = new TestLibrary();
+ expect(() => lib.expose_throwNotInitializedError()).toThrow(/assets/);
});
});
- describe("__init and __clear", () => {
+ describe("__init, __run and __clear", () => {
it("__init should resolve without error by default", async () => {
- const lib = new TestLibrary();
- await expect(lib.__init({} as any)).resolves.toBeUndefined();
+ await expect(new TestLibrary().__init({} as InitContext)).resolves.toBeUndefined();
});
- it("__clear should resolve without error by default", async () => {
- const lib = new TestLibrary();
- await expect(lib.__clear({} as any)).resolves.toBeUndefined();
+ it("__run should resolve without error by default", async () => {
+ await expect(new TestLibrary().__run({} as Context)).resolves.toBeUndefined();
});
- });
-});
-
-describe("LibraryContext", () => {
- it("should start with UNLOADED status", () => {
- const ctx = new LibraryContext();
- expect(ctx.status).toBe(LibraryStatusEnum.UNLOADED);
- });
-});
-
-describe("LibraryHandle", () => {
- it("should expose the symbol", () => {
- const sym = Symbol("test");
- const handle = new LibraryHandle(sym, new TestLibrary(), new LibraryContext());
- expect(handle.symbol).toBe(sym);
- });
-
- it("should expose the library", () => {
- const lib = new TestLibrary();
- const handle = new LibraryHandle(Symbol("test"), lib, new LibraryContext());
- expect(handle.library).toBe(lib);
- });
-
- it("should expose the context", () => {
- const ctx = new LibraryContext();
- const handle = new LibraryHandle(Symbol("test"), new TestLibrary(), ctx);
- expect(handle.context).toBe(ctx);
- });
-});
-describe("BaseLibraryManager", () => {
- let manager: TestableBaseLibraryManager;
-
- beforeEach(() => {
- manager = new TestableBaseLibraryManager();
- });
-
- it("should retrieve a library by symbol after setting it", () => {
- const sym = Symbol.for("myLib");
- const lib = new TestLibrary();
- const ctx = new LibraryContext();
-
- manager.expose_setNewLibrary(Symbol.for("filler"), new TestLibrary(), ctx);
- manager.expose_setNewLibrary(sym, lib, ctx);
-
- const handle = manager.get(sym);
- expect(handle.library).toBe(lib);
- expect(handle.symbol).toBe(sym);
- });
-
- it("should throw when getting a library that was not set", () => {
- const sym = Symbol.for("unknown");
- expect(() => manager.get(sym)).toThrow();
- });
-
- it("should throw when getting a library by unknown index", () => {
- expect(() => (manager as any)._get(99)).toThrow();
+ it("__clear should resolve without error by default", async () => {
+ await expect(new TestLibrary().__clear({} as Context)).resolves.toBeUndefined();
+ });
});
});
diff --git a/packages/common/tsconfig.spec.json b/packages/common/tsconfig.spec.json
index 8270caba..bcc1ebcb 100644
--- a/packages/common/tsconfig.spec.json
+++ b/packages/common/tsconfig.spec.json
@@ -5,6 +5,6 @@
"noEmit": true,
"skipLibCheck": true
},
- "include": ["test/**/*.spec.ts"],
+ "include": ["test/**/*.spec.ts", "test/**/*.spec-d.ts"],
"exclude": ["node_modules"]
}
diff --git a/packages/config/.idea/.gitignore b/packages/config/.idea/.gitignore
deleted file mode 100644
index 13566b81..00000000
--- a/packages/config/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/packages/config/.idea/.name b/packages/config/.idea/.name
deleted file mode 100644
index 90621f27..00000000
--- a/packages/config/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-[NanoForge] Engine Config
\ No newline at end of file
diff --git a/packages/config/.idea/[NanoForge] Engine Config.iml b/packages/config/.idea/[NanoForge] Engine Config.iml
deleted file mode 100644
index 24643cc3..00000000
--- a/packages/config/.idea/[NanoForge] Engine Config.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/config/.idea/codeStyles/Project.xml b/packages/config/.idea/codeStyles/Project.xml
deleted file mode 100644
index b70d7533..00000000
--- a/packages/config/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/config/.idea/codeStyles/codeStyleConfig.xml b/packages/config/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123c..00000000
--- a/packages/config/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/config/.idea/git_toolbox_blame.xml b/packages/config/.idea/git_toolbox_blame.xml
deleted file mode 100644
index 7dc12496..00000000
--- a/packages/config/.idea/git_toolbox_blame.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/config/.idea/git_toolbox_prj.xml b/packages/config/.idea/git_toolbox_prj.xml
deleted file mode 100644
index 02b915b8..00000000
--- a/packages/config/.idea/git_toolbox_prj.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/config/.idea/inspectionProfiles/Project_Default.xml b/packages/config/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 03d9549e..00000000
--- a/packages/config/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/config/.idea/jsLinters/eslint.xml b/packages/config/.idea/jsLinters/eslint.xml
deleted file mode 100644
index 541945bb..00000000
--- a/packages/config/.idea/jsLinters/eslint.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/config/.idea/modules.xml b/packages/config/.idea/modules.xml
deleted file mode 100644
index d418d5e0..00000000
--- a/packages/config/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/config/.idea/prettier.xml b/packages/config/.idea/prettier.xml
deleted file mode 100644
index 0c83ac4e..00000000
--- a/packages/config/.idea/prettier.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/config/.idea/vcs.xml b/packages/config/.idea/vcs.xml
deleted file mode 100644
index b2bdec2d..00000000
--- a/packages/config/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/config/.nvmrc b/packages/config/.nvmrc
deleted file mode 100644
index a682cfb9..00000000
--- a/packages/config/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-v25
diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md
deleted file mode 100644
index f464d3a5..00000000
--- a/packages/config/CHANGELOG.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# Changelog
-
-All notable changes to this project will be documented in this file.
-
-# [1.4.2](https://github.com/NanoForge-dev/Engine/compare/1.4.1...1.4.2) - (2026-07-06)
-
-## Bug Fixes
-
-- Change url protocols validation on isIpOrURL validator in network (#398) ([541dd16](https://github.com/NanoForge-dev/Engine/commit/541dd16e307e355288ec49ec7ae75566bb7fe2b5)) by @Exeloo
-
-# [1.4.1](https://github.com/NanoForge-dev/Engine/compare/1.4.0...1.4.1) - (2026-07-06)
-
-## Bug Fixes
-
-- Change check for address from fqdn to url (#394) ([8823c59](https://github.com/NanoForge-dev/Engine/commit/8823c5911f8c5a673a96c32df9888b2dea4d6370)) by @Exeloo
-
-# [1.4.0](https://github.com/NanoForge-dev/Engine/compare/1.3.1...1.4.0) - (2026-07-01)
-
-## Documentation
-
-- Add in code docs (#370) ([0146a0e](https://github.com/NanoForge-dev/Engine/commit/0146a0e3e7783c8f3bed9640ad3a452531791e0c)) by @Exeloo
-
-# [1.3.1](https://github.com/NanoForge-dev/Engine/compare/1.3.0...1.3.1) - (2026-06-04)
-
-# [@nanoforge-dev/config@1.3.0](https://github.com/NanoForge-dev/Engine/compare/@nanoforge-dev/config@1.1.0...@nanoforge-dev/config@1.3.0) - (2026-05-26)
-
-## Refactor
-
-- **docs:** Migrate to mdx (#343) ([66f66a7](https://github.com/NanoForge-dev/Engine/commit/66f66a71fa0a50cf528645c0929702e973ba6178)) by @MartinFillon
-
-# [@nanoforge-dev/config@1.1.0](https://github.com/NanoForge-dev/Engine/compare/@nanoforge-dev/config@1.0.1...@nanoforge-dev/config@1.1.0) - (2026-04-02)
-
-## Features
-
-- Add core editor contribution ressources (#263) ([6c27bf0](https://github.com/NanoForge-dev/Engine/commit/6c27bf08b7d894d96940d2c572e224c01c48e374)) by @Exeloo
-- Add loader given env to config registry (#246) ([dce12b7](https://github.com/NanoForge-dev/Engine/commit/dce12b7601b613e850987da1e0766634a821bdfc)) by @Exeloo
-
-# [@nanoforge-dev/config@1.0.1](https://github.com/NanoForge-dev/Engine/compare/@nanoforge-dev/config@1.0.0...@nanoforge-dev/config@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/config@1.0.0](https://github.com/NanoForge-dev/Engine/tree/@nanoforge-dev/config@1.0.0) - (2026-01-09)
-
-## 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
-- **common, core, config:** Introduce configuration registry and validation system ([4fafb82](https://github.com/NanoForge-dev/Engine/commit/4fafb82576fec6866fc281ad5b10321d2ac430df)) by @Exeloo
-- **config:** Add initial configuration for libraries ([57d910d](https://github.com/NanoForge-dev/Engine/commit/57d910d100489a45ea597e777fe530f018a8313e)) by @Exeloo
-
-## Refactor
-
-- Migrate namespaces to `@nanoforge-dev` and update related imports ([c84c927](https://github.com/NanoForge-dev/Engine/commit/c84c927ead941d914e5a9fd752fd3a5ac969f981)) by @Exeloo
-
diff --git a/packages/config/README.md b/packages/config/README.md
deleted file mode 100644
index 470d71de..00000000
--- a/packages/config/README.md
+++ /dev/null
@@ -1,155 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-## About
-
-`@nanoforge-dev/config` is a wrapper of [class-validator][class-validator] and [class-transformer][class-transformer] to imports validation and transformation decorators.
-
-## Installation
-
-**Node.js 25 or newer is required.**
-
-```sh
-npm install @nanoforge-dev/config
-yarn add @nanoforge-dev/config
-pnpm add @nanoforge-dev/config
-bun add @nanoforge-dev/config
-```
-
-## Warning
-
-This library is of exclusive usage for other libraries. To put variables in the environment to allow libraries to use it through this config library, you must put it in `.env` file at the root of your project (or in your environment) :
-
-```dotenv
-NANOFORGE_CLIENT_SERVER_TCP_PORT=4445
-NANOFORGE_CLIENT_SERVER_UDP_PORT=4444
-NANOFORGE_CLIENT_SERVER_ADDRESS=127.0.0.1
-```
-
-You must prefix your variables according to this table :
-
-| Prefix | Availability |
-| :------------------ | :----------------------- |
-| `NANOFORGE_CLIENT_` | Available in client only |
-| `NANOFORGE_SERVER_` | Available in server only |
-| `NANOFORGE_` | Available in both apps |
-
-⚠️ Prefixs are removed in libs
-
-You can also put it in init options (every value must be string or undefined) :
-
-```ts
-import { type IRunOptions } from "@nanoforge-dev/common";
-import { NanoforgeFactory } from "@nanoforge-dev/core";
-import { NetworkLibrary } from "@nanoforge-dev/network";
-
-export async function main(options: IRunOptions) {
- const app = NanoforgeFactory.createClient();
-
- const network = new NetworkLibrary();
-
- app.useNetwork(network);
-
- await app.init({
- ...options,
- env: {
- ...options.env,
- SERVER_TCP_PORT: "4445",
- SERVER_UDP_PORT: "4444",
- SERVER_ADDRESS: "127.0.0.1",
- },
- });
-
- await app.run();
-}
-```
-
-## Example usage
-
-Initialize the library in your main file.
-
-```ts
-export class NetworkClientLibrary extends BaseNetworkLibrary {
- get __name(): string {
- return "NetworkClientLibrary";
- }
-
- public override async __init(context: InitContext): Promise {
- const config = await context.config.registerConfig(ClientConfigNetwork);
-
- // Do something with config
- }
-}
-```
-
-Using this config
-
-```ts
-import {
- Default,
- Expose,
- IsByteLength,
- IsIpOrURL,
- IsOptional,
- IsPort,
-} from "@nanoforge-dev/config";
-
-export class ClientConfigNetwork {
- // This var must be a string port (ex: "4444") but can be undefined
- @Expose()
- @IsOptional()
- @IsPort()
- SERVER_TCP_PORT?: string;
-
- @Expose()
- @IsOptional()
- @IsPort()
- SERVER_UDP_PORT?: string;
-
- // This var must be ip address or fqdn (it cannot be undefined)
- @Expose()
- @IsIpOrURL()
- SERVER_ADDRESS?: string;
-
- // This var must be a byte length between 2 and 64. It can be undefined as it as a default value.
- @Expose()
- @Default("PACKET_END")
- @IsByteLength(2, 64)
- MAGIC_VALUE!: string;
-}
-```
-
-## 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/config
-[npm]: https://www.npmjs.com/package/@nanoforge-dev/config
-[contributing]: https://github.com/NanoForge-dev/Engine/blob/main/.github/CONTRIBUTING.md
-[class-validator]: https://github.com/typestack/class-validator
-[class-transformer]: https://github.com/typestack/class-transformer
diff --git a/packages/config/package.json b/packages/config/package.json
deleted file mode 100644
index f81b0c34..00000000
--- a/packages/config/package.json
+++ /dev/null
@@ -1,88 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/package.json",
- "name": "@nanoforge-dev/config",
- "version": "1.4.2",
- "description": "NanoForge Engine - Config",
- "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/config"
- },
- "funding": "https://github.com/NanoForge-dev/Engine?sponsor",
- "scripts": {
- "build": "tsc --noEmit && tsdown --config-loader unrun",
- "lint": "prettier --check . && eslint --format=pretty src",
- "format": "prettier --write . && eslint --fix --format=pretty src",
- "prepack": "pnpm run build && pnpm run lint",
- "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/config/*'",
- "release": "cliff-jumper",
- "docs": "mint-tsdocs generate"
- },
- "dependencies": {
- "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",
- "tsdown": "catalog:build",
- "typescript": "catalog:core",
- "unrun": "catalog:build"
- },
- "packageManager": "pnpm@11.5.2",
- "engines": {
- "node": "25"
- },
- "publishConfig": {
- "access": "public"
- },
- "lint-staged": {
- "**": [
- "prettier --ignore-unknown --write"
- ],
- "src/**/*.ts": [
- "eslint --fix"
- ]
- }
-}
diff --git a/packages/config/src/default.ts b/packages/config/src/default.ts
deleted file mode 100644
index ed2a78c3..00000000
--- a/packages/config/src/default.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Transform } from "class-transformer";
-import { type TransformFnParams } from "class-transformer/types/interfaces/metadata/transform-fn-params.interface";
-
-/**
- * Property decorator that supplies a fallback value when the environment
- * variable is `undefined` or `null`.
- *
- * @remarks
- * Pairs with `class-transformer`'s `@Expose` and `class-validator` decorators.
- * The default is deep-cloned via `JSON.parse(JSON.stringify(…))` so objects
- * are not shared between instances.
- *
- * @param defaultValue - The value to use when the property is absent.
- *
- * @example
- * ```ts
- * class MyConfig {
- * \@Expose()
- * \@Default("localhost")
- * HOST!: string;
- * }
- * ```
- */
-export function Default(defaultValue: unknown): PropertyDecorator {
- return Transform(
- ({ value }: TransformFnParams) => value ?? JSON.parse(JSON.stringify(defaultValue)),
- );
-}
diff --git a/packages/config/src/exports/index.ts b/packages/config/src/exports/index.ts
deleted file mode 100644
index 0b143852..00000000
--- a/packages/config/src/exports/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./class-transformer";
-export * from "./class-validator";
diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts
deleted file mode 100644
index eccc35fa..00000000
--- a/packages/config/src/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from "./exports";
-export * from "./default";
-export * from "./transformers";
-export * from "./validators";
diff --git a/packages/config/src/transformers/index.ts b/packages/config/src/transformers/index.ts
deleted file mode 100644
index 77363e3a..00000000
--- a/packages/config/src/transformers/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { TransformToBoolean } from "./boolean.transformer";
diff --git a/packages/config/src/validators/index.ts b/packages/config/src/validators/index.ts
deleted file mode 100644
index 3887243a..00000000
--- a/packages/config/src/validators/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { IsIpOrURL } from "./is-ip-or-url.validator";
diff --git a/packages/core-editor/.cliff-jumperrc.json b/packages/core-editor/.cliff-jumperrc.json
deleted file mode 100644
index fce3ee21..00000000
--- a/packages/core-editor/.cliff-jumperrc.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json",
- "name": "core-editor",
- "org": "nanoforge-dev",
- "packagePath": "packages/core-editor",
- "tagTemplate": "{{new-version}}",
- "identifierBase": false
-}
diff --git a/packages/core-editor/.idea/.name b/packages/core-editor/.idea/.name
deleted file mode 100644
index b5da13ff..00000000
--- a/packages/core-editor/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-[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
deleted file mode 100644
index 24643cc3..00000000
--- a/packages/core-editor/.idea/[NanoForge] Engine Core Editor.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core-editor/.idea/codeStyles/Project.xml b/packages/core-editor/.idea/codeStyles/Project.xml
deleted file mode 100644
index b70d7533..00000000
--- a/packages/core-editor/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core-editor/.idea/codeStyles/codeStyleConfig.xml b/packages/core-editor/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123c..00000000
--- a/packages/core-editor/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ 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
deleted file mode 100644
index 7dc12496..00000000
--- a/packages/core-editor/.idea/git_toolbox_blame.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ 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
deleted file mode 100644
index 02b915b8..00000000
--- a/packages/core-editor/.idea/git_toolbox_prj.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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
deleted file mode 100644
index 03d9549e..00000000
--- a/packages/core-editor/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core-editor/.idea/jsLinters/eslint.xml b/packages/core-editor/.idea/jsLinters/eslint.xml
deleted file mode 100644
index 541945bb..00000000
--- a/packages/core-editor/.idea/jsLinters/eslint.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core-editor/.idea/modules.xml b/packages/core-editor/.idea/modules.xml
deleted file mode 100644
index 529602fb..00000000
--- a/packages/core-editor/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core-editor/.idea/prettier.xml b/packages/core-editor/.idea/prettier.xml
deleted file mode 100644
index 0c83ac4e..00000000
--- a/packages/core-editor/.idea/prettier.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core-editor/.idea/vcs.xml b/packages/core-editor/.idea/vcs.xml
deleted file mode 100644
index b2bdec2d..00000000
--- a/packages/core-editor/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core-editor/.nvmrc b/packages/core-editor/.nvmrc
deleted file mode 100644
index a682cfb9..00000000
--- a/packages/core-editor/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-v25
diff --git a/packages/core-editor/CHANGELOG.md b/packages/core-editor/CHANGELOG.md
deleted file mode 100644
index 73068e2b..00000000
--- a/packages/core-editor/CHANGELOG.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# Changelog
-
-All notable changes to this project will be documented in this file.
-
-# [1.4.2](https://github.com/NanoForge-dev/Engine/compare/1.4.1...1.4.2) - (2026-07-06)
-
-## Bug Fixes
-
-- **core-editor:** Reload event searching for entities (#396) ([eb07c7f](https://github.com/NanoForge-dev/Engine/commit/eb07c7f2fa7ea834c40c7f78517ee7104c85c9b0)) by @Tchips46
-
-# [1.4.1](https://github.com/NanoForge-dev/Engine/compare/1.4.0...1.4.1) - (2026-07-06)
-
-# [1.4.0](https://github.com/NanoForge-dev/Engine/compare/1.3.1...1.4.0) - (2026-07-01)
-
-## Bug Fixes
-
-- **core-editor:** Run when don't paused (#377) ([09147d2](https://github.com/NanoForge-dev/Engine/commit/09147d2e71488c7028e1f5983f43d5ff724769ee)) by @Tchips46
-
-## Documentation
-
-- Add in code docs (#370) ([0146a0e](https://github.com/NanoForge-dev/Engine/commit/0146a0e3e7783c8f3bed9640ad3a452531791e0c)) by @Exeloo
-
-## Features
-
-- Add assets handling (#383) ([cda797a](https://github.com/NanoForge-dev/Engine/commit/cda797a793ee5074cd43503e55a8dd348a516305)) by @Exeloo
-- Add drag and drop on graphics (#380) ([afffa7a](https://github.com/NanoForge-dev/Engine/commit/afffa7afd1f51eba2d2cd0d96fe47e5db04ac534)) by @Exeloo
-- **core-editor:** Event listenner not runned if main loop is paused (#379) ([25053db](https://github.com/NanoForge-dev/Engine/commit/25053db388e277bc7e1181315f07249f121fcc94)) by @Tchips46
-- **graphic-2d:** Destroy konva js in the lib clear (#372) ([e1ae678](https://github.com/NanoForge-dev/Engine/commit/e1ae678e7420eb20a2152fc110550b786877e7a5)) by @Tchips46
-- **core-editor:** Implement events (#369) ([4ebf791](https://github.com/NanoForge-dev/Engine/commit/4ebf7911521b9114c9e8f4482bd59175fb35cf06)) by @Tchips46
-
-# [1.3.1](https://github.com/NanoForge-dev/Engine/compare/1.3.0...1.3.1) - (2026-06-04)
-
-# [@nanoforge-dev/core-editor@1.3.0](https://github.com/NanoForge-dev/Engine/compare/@nanoforge-dev/core-editor@1.0.2...@nanoforge-dev/core-editor@1.3.0) - (2026-05-26)
-
-## Refactor
-
-- **docs:** Migrate to mdx (#343) ([66f66a7](https://github.com/NanoForge-dev/Engine/commit/66f66a71fa0a50cf528645c0929702e973ba6178)) by @MartinFillon
-
-# [@nanoforge-dev/core-editor@1.0.2](https://github.com/NanoForge-dev/Engine/compare/@nanoforge-dev/core-editor@1.0.1...@nanoforge-dev/core-editor@1.0.2) - (2026-04-29)
-
-## Bug Fixes
-
-- **render:** Change specific container canva for general html div (#323) ([6ea75f4](https://github.com/NanoForge-dev/Engine/commit/6ea75f4f3c1aa3d2f46bdbea1467f580cb9000bc)) by @Tchips46
-
-# [@nanoforge-dev/core-editor@1.0.1](https://github.com/NanoForge-dev/Engine/compare/@nanoforge-dev/core-editor@1.0.0...@nanoforge-dev/core-editor@1.0.1) - (2026-04-21)
-
-## Bug Fixes
-
-- **core-editor:** Bind events listeners (#315) ([cb80b4c](https://github.com/NanoForge-dev/Engine/commit/cb80b4cd9a6649e793bb7fe1b52f05427eebc6d9)) by @Tchips46
-
-# [@nanoforge-dev/core-editor@1.0.0](https://github.com/NanoForge-dev/Engine/tree/@nanoforge-dev/core-editor@1.0.0) - (2026-04-02)
-
-## Features
-
-- Add core editor contribution ressources (#263) ([6c27bf0](https://github.com/NanoForge-dev/Engine/commit/6c27bf08b7d894d96940d2c572e224c01c48e374)) by @Exeloo
-- **core-editor:** Hot reload entity component from save (#253) ([71e3699](https://github.com/NanoForge-dev/Engine/commit/71e36998d9f510a874a5c3022aa0cdfa6c6bdfac)) by @Tchips46
-
diff --git a/packages/core-editor/README.md b/packages/core-editor/README.md
deleted file mode 100644
index 89278938..00000000
--- a/packages/core-editor/README.md
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-## 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 25 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/mint-tsdocs.config.json b/packages/core-editor/mint-tsdocs.config.json
deleted file mode 100644
index 02fa5cc0..00000000
--- a/packages/core-editor/mint-tsdocs.config.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "$schema": "./node_modules/mint-tsdocs/lib/schemas/config.schema.json",
- "entryPoint": "dist/index.d.ts",
- "outputFolder": "../../docs/references/core-editor",
- "lint": {
- "eslint": {
- "enabled": true
- }
- },
- "templates": {
- "userTemplateDir": "../../docs/templates"
- }
-}
diff --git a/packages/core-editor/package.json b/packages/core-editor/package.json
deleted file mode 100644
index 2e9267aa..00000000
--- a/packages/core-editor/package.json
+++ /dev/null
@@ -1,94 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/package.json",
- "name": "@nanoforge-dev/core-editor",
- "version": "1.4.2",
- "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 && tsdown --config-loader unrun",
- "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",
- "docs": "mint-tsdocs generate"
- },
- "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",
- "tsdown": "catalog:build",
- "typescript": "catalog:core",
- "unrun": "catalog:build",
- "vitest": "catalog:test"
- },
- "packageManager": "pnpm@11.5.2",
- "engines": {
- "node": "25"
- },
- "publishConfig": {
- "access": "public"
- },
- "lint-staged": {
- "**": [
- "prettier --ignore-unknown --write"
- ],
- "src/**/*.ts": [
- "eslint --fix"
- ]
- }
-}
diff --git a/packages/core-editor/src/application/nanoforge-application.ts b/packages/core-editor/src/application/nanoforge-application.ts
deleted file mode 100644
index c015675f..00000000
--- a/packages/core-editor/src/application/nanoforge-application.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import {
- type IAssetManagerLibrary,
- type IComponentSystemLibrary,
- type ILibrary,
- type INetworkLibrary,
- type IRunOptions,
- 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 { Core } from "../core/core";
-
-/**
- * Base class for editor-mode NanoForge applications.
- *
- * @remarks
- * Mirror of `NanoforgeApplication` from `@nanoforge-dev/core` with an
- * extended `init` signature that accepts `IEditorRunOptions`. Use
- * `NanoforgeFactory` from this package to create instances.
- */
-export abstract class NanoforgeApplication {
- protected applicationConfig: ApplicationConfig;
- private _core?: Core;
- private readonly _options: IApplicationOptions;
-
- /**
- * @param options - Optional application-level settings such as tickRate.
- */
- constructor(options?: Partial) {
- this.applicationConfig = new ApplicationConfig();
-
- this._options = {
- tickRate: 60,
- ...(options ?? {}),
- };
- }
-
- /**
- * Register a library under a custom symbol.
- *
- * @param sym - Unique symbol identifying the library slot.
- * @param library - Library instance to register.
- */
- public use(sym: symbol, library: ILibrary): void {
- this.applicationConfig.useLibrary(sym, library);
- }
-
- /**
- * Register the component-system (ECS) library.
- *
- * @param library - ECS library instance.
- */
- public useComponentSystem(library: IComponentSystemLibrary) {
- this.applicationConfig.useComponentSystemLibrary(library);
- }
-
- /**
- * Register the network library.
- *
- * @param library - Network library instance.
- */
- public useNetwork(library: INetworkLibrary) {
- this.applicationConfig.useNetworkLibrary(library);
- }
-
- /**
- * Register the asset-manager library.
- *
- * @param library - Asset manager instance.
- */
- public useAssetManager(library: IAssetManagerLibrary) {
- this.applicationConfig.useAssetManagerLibrary(library);
- }
-
- /**
- * Initialise all registered libraries and prepare the engine for the game loop.
- *
- * @param options - Editor run options including save/events callbacks and canvas container.
- */
- public init(options: IRunOptions): Promise {
- this._core = new Core(
- this.applicationConfig,
- new EditableApplicationContext(this.applicationConfig.libraryManager),
- );
- return this._core.init(options, this._options);
- }
-
- /**
- * Start the game loop.
- *
- * @throws `NfNotInitializedException` When called before `init`.
- */
- 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
deleted file mode 100644
index 8f799fde..00000000
--- a/packages/core-editor/src/application/nanoforge-client.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import {
- type IGraphicsLibrary,
- type IInputLibrary,
- type ISoundLibrary,
-} from "@nanoforge-dev/common";
-
-import { NanoforgeApplication } from "./nanoforge-application";
-
-/**
- * Client-side NanoForge application for editor mode.
- *
- * @remarks
- * Extends `NanoforgeApplication` with graphics, input, and sound library
- * slots. Create via the `core-editor` `NanoforgeFactory`.
- */
-export class NanoforgeClient extends NanoforgeApplication {
- /**
- * Register the graphics library.
- *
- * @param library - Graphics library instance (e.g. Graphics2DLibrary).
- */
- public useGraphics(library: IGraphicsLibrary) {
- this.applicationConfig.useGraphicsLibrary(library);
- }
-
- /**
- * Register the input library.
- *
- * @param library - Input library instance (e.g. InputLibrary).
- */
- public useInput(library: IInputLibrary) {
- this.applicationConfig.useInputLibrary(library);
- }
-
- /**
- * Register the sound-effect library.
- *
- * @param library - Sound library instance (e.g. SoundLibrary).
- */
- 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
deleted file mode 100644
index b0599cbf..00000000
--- a/packages/core-editor/src/application/nanoforge-factory.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { type IApplicationOptions } from "../../../core/src/application/application-options.type";
-import { NanoforgeClient } from "./nanoforge-client";
-import { NanoforgeServer } from "./nanoforge-server";
-
-class NanoforgeFactoryStatic {
- /**
- * Create a new editor-mode client application.
- *
- * @param options - Optional application settings (e.g. tickRate).
- * @returns A pre-configured `NanoforgeClient` instance.
- */
- createClient(options?: Partial): NanoforgeClient {
- return new NanoforgeClient(options);
- }
-
- /**
- * Create a new editor-mode server application.
- *
- * @param options - Optional application settings (e.g. tickRate).
- * @returns A pre-configured `NanoforgeServer` instance.
- */
- createServer(options?: Partial): NanoforgeServer {
- return new NanoforgeServer(options);
- }
-}
-
-/**
- * Singleton factory for creating editor-mode NanoForge client and server
- * applications.
- *
- * @remarks
- * Use `NanoforgeFactory.createClient` or
- * `NanoforgeFactory.createServer` to obtain an application instance that
- * accepts `IEditorRunOptions` on `init`.
- *
- * @example
- * ```ts
- * import `NanoforgeFactory ` from "@nanoforge-dev/core-editor";
- *
- * const client = NanoforgeFactory.createClient();
- * ```
- */
-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
deleted file mode 100644
index 5dfdb20d..00000000
--- a/packages/core-editor/src/application/nanoforge-server.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { NanoforgeApplication } from "./nanoforge-application";
-
-/**
- * Server-side NanoForge application for editor mode.
- *
- * @remarks
- * Extends `NanoforgeApplication` for use in server processes during
- * editor sessions. Create via the `core-editor` `NanoforgeFactory`.
- */
-export class NanoforgeServer extends NanoforgeApplication {}
diff --git a/packages/core-editor/src/common/context/contexts/init.context.ts b/packages/core-editor/src/common/context/contexts/init.context.ts
deleted file mode 100644
index 599e89aa..00000000
--- a/packages/core-editor/src/common/context/contexts/init.context.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import {
- type ApplicationContext,
- type IConfigRegistry,
- type IRunOptions,
- InitContext,
- type LibraryManager,
-} from "@nanoforge-dev/common";
-
-import { type EventEmitter } from "../event-emitter";
-
-export class EditorInitContext extends InitContext {
- private readonly _eventEmitter: EventEmitter;
-
- constructor(
- context: ApplicationContext,
- libraryManager: LibraryManager,
- configRegistry: IConfigRegistry,
- options: IRunOptions,
- eventEmitter: EventEmitter,
- ) {
- super(context, libraryManager, configRegistry, options);
- this._eventEmitter = eventEmitter;
- }
-
- override get eventEmitter(): EventEmitter {
- return this._eventEmitter;
- }
-}
diff --git a/packages/core-editor/src/common/context/event-emitter.ts b/packages/core-editor/src/common/context/event-emitter.ts
deleted file mode 100644
index ebd0d575..00000000
--- a/packages/core-editor/src/common/context/event-emitter.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { IRunOptions } from "@nanoforge-dev/common";
-
-import type { IEventEmitter, ListenerType } from "./event-emitter.type";
-import type { CoreEvents, CoreEventsMap } from "./events/core-events";
-import type { EditorEvents, EditorEventsMap } from "./events/editor-events";
-
-export class EventEmitter {
- private coreEvents: IEventEmitter;
- private editorEvents: IEventEmitter;
-
- constructor(opts: IRunOptions["editor"]) {
- this.coreEvents = opts.coreEvents;
- this.editorEvents = opts.editorEvents;
- }
-
- runEvents(): void {
- this.coreEvents.runEvents();
- }
-
- emit(event: K, ...args: EditorEventsMap[K]): void {
- this.editorEvents.emitEvent(event, ...args);
- }
-
- on(
- event: K,
- listener: ListenerType,
- ): void {
- this.coreEvents.on(event, listener);
- }
-}
diff --git a/packages/core-editor/src/common/context/event-emitter.type.ts b/packages/core-editor/src/common/context/event-emitter.type.ts
deleted file mode 100644
index 15cb504a..00000000
--- a/packages/core-editor/src/common/context/event-emitter.type.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/** Callback signature for event listeners. */
-export type ListenerType<
- Events extends string,
- EventsMap extends Record,
- K extends keyof EventsMap,
-> = (...args: EventsMap[K]) => void;
-
-/** Signature of the waiting for execution events. */
-export type QueuedEvent = {
- event: K;
- args: EventsMap[K];
-};
-
-/**
- * Simple event emitter interface used for communication between the NanoForge
- * editor and the running engine.
- *
- * @remarks
- * Listeners are queued and processed via `runEvents` to keep the engine
- * loop deterministic.
- */
-export interface IEventEmitter> {
- /** Map of event names to their registered listeners. */
- listeners: {
- [K in keyof EventsMap]?: ListenerType[];
- };
-
- /** Queue of events waiting to be dispatched by `runEvents`. */
- eventQueue: QueuedEvent[];
-
- /** Drain the event queue and invoke all matching listeners. */
- runEvents(): void;
-
- /**
- * Enqueue an event for dispatching on the next `runEvents` call.
- *
- * @param event - Event name or EventTypeEnum value.
- * @param args - Optional arguments forwarded to listeners.
- */
- emitEvent(event: K, ...args: EventsMap[K]): void;
-
- /**
- * Register a listener for an event. Alias: `on`.
- *
- * @param event - Event name to subscribe to.
- * @param listener - Callback invoked when the event fires.
- */
- addListener(
- event: K,
- listener: ListenerType,
- ): void;
- /** Alias for `addListener`. */
- on(event: K, listener: ListenerType): void;
-
- /**
- * Remove a previously registered listener. Alias: `off`.
- *
- * @param event - Event name to unsubscribe from.
- * @param listener - The exact listener function to remove.
- */
- removeListener(
- event: K,
- listener: ListenerType,
- ): void;
- /** Alias for `removeListener`. */
- off(event: K, listener: ListenerType): void;
-
- /**
- * Remove all listeners registered for a specific event.
- *
- * @param event - Event name whose listeners should be cleared.
- */
- removeListenersForEvent(event: keyof EventsMap): void;
- /** Remove every registered listener across all events. */
- removeAllListeners(): void;
-}
diff --git a/packages/core-editor/src/common/context/events/core-events.ts b/packages/core-editor/src/common/context/events/core-events.ts
deleted file mode 100644
index 03d041f0..00000000
--- a/packages/core-editor/src/common/context/events/core-events.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { type Save } from "../save.type";
-
-/**
- * Events that the NanoForge editor can emit to the running engine.
- */
-export enum CoreEvents {
- /** Reload only changed entity components params (live-patch) */
- HOT_RELOAD = "hot-reload",
- /** Reload all entities component params */
- HARD_RELOAD = "hard-reload",
- /** Don't execute the run function until UNPAUSE_GAME */
- PAUSE_GAME = "pause-game",
- /** End main loop and clear */
- STOP_GAME = "stop-game",
- /** resume executing the run function */
- UNPAUSE_GAME = "unpause-game",
-}
-
-export interface CoreEventsMap {
- [CoreEvents.HOT_RELOAD]: [save: Save];
- [CoreEvents.HARD_RELOAD]: [save: Save];
- [CoreEvents.PAUSE_GAME]: [];
- [CoreEvents.STOP_GAME]: [];
- [CoreEvents.UNPAUSE_GAME]: [];
-}
diff --git a/packages/core-editor/src/common/context/events/editor-events.ts b/packages/core-editor/src/common/context/events/editor-events.ts
deleted file mode 100644
index d9b72fd6..00000000
--- a/packages/core-editor/src/common/context/events/editor-events.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export enum EditorEvents {
- MOVE_COMPONENT = "move-component",
-}
-
-export interface EditorEventsMap {
- [EditorEvents.MOVE_COMPONENT]: [
- entityId: string,
- componentId: string,
- position: { x: number; y: number },
- ];
-}
diff --git a/packages/core-editor/src/common/context/index.ts b/packages/core-editor/src/common/context/index.ts
deleted file mode 100644
index 8b92a342..00000000
--- a/packages/core-editor/src/common/context/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { CoreEvents } from "./events/core-events";
-export { EditorEvents } from "./events/editor-events";
-export type { EventEmitter } from "./event-emitter";
diff --git a/packages/core-editor/src/common/context/options.type.ts b/packages/core-editor/src/common/context/options.type.ts
deleted file mode 100644
index 5b121a5d..00000000
--- a/packages/core-editor/src/common/context/options.type.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { type IEventEmitter } from "./event-emitter.type";
-import { type CoreEvents, type CoreEventsMap } from "./events/core-events";
-import { type EditorEvents, type EditorEventsMap } from "./events/editor-events";
-import { type Save } from "./save.type";
-
-/**
- * Union of client and server editor run options accepted by
- * `NanoforgeApplication.init` in editor mode.
- *
- * @deprecated Use `IRunOptions` instead.
- */
-export type IEditorRunOptions = IEditorRunClientOptions | IEditorRunServerOptions;
-
-/**
- * Run options for a client-side editor application.
- *
- * @deprecated Use `IRunClientOptions` instead.
- * @remarks
- * Extends the base run options with editor-specific communication channels.
- */
-export interface IEditorRunClientOptions {
- /** DOM element that will host the game canvas. */
- container: HTMLDivElement;
- /** Map of virtual file paths to their resolved URL strings. */
- files: Map;
- /** Runtime environment variables available to all libraries. */
- env: Record;
- /** Editor integration hooks. */
- editor: {
- /** Serialised scene state loaded or saved by the editor. */
- save: Save;
- /** Event emitter for core-to-editor communication. */
- coreEvents: IEventEmitter;
- /** Event emitter for editor-to-core communication. */
- editorEvents: IEventEmitter;
- };
-}
-
-/**
- * Run options for a server-side editor application.
- *
- * @deprecated Use `IRunServerOptions` instead.
- * @remarks
- * Extends the base server run options with editor-specific communication channels.
- */
-export interface IEditorRunServerOptions {
- /** Map of virtual file paths to their resolved URL strings. */
- files: Map;
- /** Runtime environment variables available to all libraries. */
- env: Record;
- /** Editor integration hooks. */
- editor: {
- /** Serialised scene state loaded or saved by the editor. */
- save: Save;
- /** Event emitter for core-to-editor communication. */
- coreEvents: IEventEmitter;
- /** Event emitter for editor-to-core communication. */
- editorEvents: IEventEmitter;
- };
-}
diff --git a/packages/core-editor/src/common/context/save.type.ts b/packages/core-editor/src/common/context/save.type.ts
deleted file mode 100644
index d29f2987..00000000
--- a/packages/core-editor/src/common/context/save.type.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Identifies the role a library plays in a saved scene.
- */
-export enum SaveLibraryTypeEnum {
- COMPONENT_SYSTEM = "component-system",
- GRAPHICS = "graphics",
- ASSET_MANAGER = "asset-manager",
- NETWORK = "network",
- INPUT = "input",
- SOUND = "sound",
-}
-
-/**
- * Serialised representation of a registered library in a saved scene.
- */
-export interface SaveLibrary {
- /** Unique identifier for this library instance in the scene. */
- id: string;
- /** Library role (one of `SaveLibraryTypeEnum` or a custom string). */
- type: SaveLibraryTypeEnum | string;
- /** Human-readable name of the library. */
- name: string;
- /** Module path used to import the library at runtime. */
- path: string;
-}
-
-/**
- * Serialised representation of an ECS component type in a saved scene.
- */
-export interface SaveComponent {
- /** Component class name. */
- name: string;
- /** Module path used to import the component at runtime. */
- path: string;
- /** Names of the constructor parameters exposed in the editor. */
- paramsNames: string[];
-}
-
-/**
- * Serialised representation of an ECS system in a saved scene.
- */
-export interface SaveSystem {
- /** System class name. */
- name: string;
- /** Module path used to import the system at runtime. */
- path: string;
-}
-
-/**
- * Serialised representation of an entity and its component data.
- */
-export interface SaveEntity {
- /** Unique identifier for this entity instance. */
- id: string;
- /** Map of component name to its property values. */
- components: Record>;
-}
-
-/**
- * Root serialized scene state exchanged between the editor and the engine.
- */
-export interface Save {
- /** All libraries registered in the scene. */
- libraries: SaveLibrary[];
- /** All component types available in the scene. */
- components: SaveComponent[];
- /** All systems active in the scene. */
- systems: SaveSystem[];
- /** All spawned entities and their component data. */
- entities: SaveEntity[];
-}
diff --git a/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts
deleted file mode 100644
index cbaddad7..00000000
--- a/packages/core-editor/src/core/core.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import {
- ClearContext,
- ClientLibraryManager,
- Context,
- type IRunnerLibrary,
- type InitContext,
- type LibraryHandle,
- LibraryStatusEnum,
- NfNotInitializedException,
-} from "@nanoforge-dev/common";
-import { type IRunOptions } from "@nanoforge-dev/common";
-
-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 { EditorInitContext } from "../common/context/contexts/init.context";
-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: IRunOptions, appOptions: IApplicationOptions): Promise {
- this.options = appOptions;
- this._configRegistry = new ConfigRegistry(options.env);
- this.editor = new CoreEditor(this, options.editor, this.config);
- await this.runInit(this.getInitContext(options));
- }
-
- public async run(): Promise {
- if (!this.options) throw new NfNotInitializedException("Core Editor");
-
- 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();
- this.editor?.runEvents();
- if (!this.editor?.isPaused) await runner(tickStart - previousTick);
- previousTick = tickStart;
- setTimeout(render, tickLengthMs + tickStart - Date.now());
- };
-
- context.application.setIsRunning(true);
- setTimeout(render);
- }
-
- public getExecutionContext(): EditableExecutionContext {
- return new EditableExecutionContext(this.context, this.config.libraryManager);
- }
-
- private getInitContext(options: IRunOptions): InitContext {
- if (!this._configRegistry || !this.editor) throw new NfNotInitializedException("Core Editor");
-
- return new EditorInitContext(
- this.context,
- this.config.libraryManager,
- this._configRegistry,
- options,
- this.editor.eventEmitter,
- );
- }
-
- 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
deleted file mode 100644
index b15f3a59..00000000
--- a/packages/core-editor/src/editor/core-editor.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { type IAssetManagerLibrary, type IRunOptions, NfNotFound } from "@nanoforge-dev/common";
-import type { ECSClientLibrary, Entity } from "@nanoforge-dev/ecs-client";
-
-import type { ApplicationConfig } from "../../../core/src/application/application-config";
-import { EventEmitter } from "../common/context/event-emitter";
-import { CoreEvents } from "../common/context/events/core-events";
-import type { Save } from "../common/context/save.type";
-import type { Core } from "../core/core";
-
-export class CoreEditor {
- public eventEmitter: EventEmitter;
- private ecsLibrary: ECSClientLibrary;
- private assetLibrary: IAssetManagerLibrary;
- private lastLoadedSave: Save;
- private core: Core;
- private _isPaused: boolean = false;
-
- constructor(core: Core, editor: IRunOptions["editor"], config: ApplicationConfig) {
- this.eventEmitter = new EventEmitter(editor);
- this.lastLoadedSave = JSON.parse(JSON.stringify(editor.save));
-
- this.ecsLibrary = config.getComponentSystemLibrary().library;
- this.assetLibrary = config.getAssetManagerLibrary().library;
-
- this.eventEmitter.on(CoreEvents.HOT_RELOAD, this.hotReloadEvent.bind(this));
- this.eventEmitter.on(CoreEvents.HARD_RELOAD, this.hardReloadEvent.bind(this));
- this.eventEmitter.on(CoreEvents.PAUSE_GAME, this.pauseGameEvent.bind(this));
- this.eventEmitter.on(CoreEvents.STOP_GAME, this.stopGameEvent.bind(this));
- this.eventEmitter.on(CoreEvents.UNPAUSE_GAME, this.unpauseGameEvent.bind(this));
-
- this.core = core;
- }
-
- get isPaused(): boolean {
- return this._isPaused;
- }
-
- public runEvents() {
- this.eventEmitter.runEvents();
- }
-
- public hotReloadEvent(save: Save): void {
- this.reloadEvent(save, false);
- }
-
- public hardReloadEvent(save: Save): void {
- this.reloadEvent(save, true);
- }
-
- public pauseGameEvent(): void {
- this._isPaused = true;
- }
- public unpauseGameEvent(): void {
- this._isPaused = false;
- }
-
- public stopGameEvent(): void {
- this.core.getExecutionContext().application.setIsRunning(false);
- }
-
- private reloadEvent(save: Save, hard: boolean): void {
- const reg = this.ecsLibrary.registry;
- save.entities.forEach(({ id, components }) => {
- Object.entries(components).forEach(([componentName, params]) => {
- const ogComponent = save.components.find(({ name }) => name === 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]) => {
- if (!hard) {
- const lastLoadedParam = this.lastLoadedSave.entities.find((e) => e.id === id)
- ?.components[componentName]?.[paramName];
- if (lastLoadedParam === paramValue) return;
- }
-
- const ogParam = ogComponent.paramsNames.find(
- (param) => param === paramName || param === `__RESERVED_ASSET_${paramName}`,
- );
- if (!ogParam) return;
-
- ecsComponent[paramName] = ogParam.startsWith("__RESERVED_ASSET_")
- ? this.assetLibrary.getAsset(paramValue)
- : paramValue;
- });
- reg.addComponent(ecsEntity, ecsComponent);
- });
- });
- this.lastLoadedSave = JSON.parse(JSON.stringify(save));
- }
-
- private getEntityFromEntityId(entityId: string): Entity {
- const reg = this.ecsLibrary.registry;
- return reg.entityFromIndex(
- reg
- .getIndexedZipper([{ name: `__RESERVED_entityId` }])
- .find(({ __RESERVED_entityId }) => __RESERVED_entityId.entityId === entityId).id,
- );
- }
-}
diff --git a/packages/core-editor/src/index.ts b/packages/core-editor/src/index.ts
deleted file mode 100644
index b6bd2792..00000000
--- a/packages/core-editor/src/index.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { EventEmitter } from "./common/context/event-emitter";
-import type { IEventEmitter } from "./common/context/event-emitter.type";
-import type { CoreEvents, CoreEventsMap } from "./common/context/events/core-events";
-import type { EditorEvents, EditorEventsMap } from "./common/context/events/editor-events";
-import type { Save } from "./common/context/save.type";
-
-declare module "@nanoforge-dev/common" {
- interface IRunClientOptions {
- /** Editor integration hooks. */
- editor: {
- /** Serialised scene state loaded or saved by the editor. */
- save: Save;
- /** Event emitter for core-to-editor communication. */
- coreEvents: IEventEmitter;
- /** Event emitter for editor-to-core communication. */
- editorEvents: IEventEmitter;
- };
- }
-
- interface IRunServerOptions {
- /** Editor integration hooks. */
- editor: {
- /** Serialised scene state loaded or saved by the editor. */
- save: Save;
- /** Event emitter for core-to-editor communication. */
- coreEvents: IEventEmitter;
- /** Event emitter for editor-to-core communication. */
- editorEvents: IEventEmitter;
- };
- }
-
- interface InitContext {
- eventEmitter: EventEmitter;
- }
-}
-
-export * from "./application/nanoforge-factory";
-
-export type { NanoforgeClient } from "./application/nanoforge-client";
-export type { NanoforgeServer } from "./application/nanoforge-server";
-export { type EventEmitter, CoreEvents, EditorEvents } from "./common/context";
diff --git a/packages/core-editor/test/editor-feature.spec.ts b/packages/core-editor/test/editor-feature.spec.ts
deleted file mode 100644
index c63fc5d1..00000000
--- a/packages/core-editor/test/editor-feature.spec.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import { type IRunOptions } from "@nanoforge-dev/common";
-import { afterEach, describe, expect, it, vi } from "vitest";
-
-import { type ApplicationConfig } from "../../core/src/application/application-config";
-import { CoreEvents } from "../src";
-import { type Save, type SaveComponent, type SaveEntity } from "../src/common/context/save.type";
-import { type Core } from "../src/core/core";
-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(CoreEvents.HOT_RELOAD);
- events.emitEvent(CoreEvents.HOT_RELOAD);
- const spyHotReload = vi
- .spyOn(CoreEditor.prototype, "hotReloadEvent")
- .mockImplementation(() => {});
- new CoreEditor(
- {} as unknown as Core,
- { coreEvents: events, save: { libraries: [] } } as unknown as IRunOptions["editor"],
- {
- getAssetManagerLibrary: () => ({ library: {} }),
- getComponentSystemLibrary: () => ({ library: {} }),
- } as ApplicationConfig,
- ).runEvents();
- expect(spyHotReload).toHaveBeenCalledTimes(2);
- });
- });
-
- describe("askEntitiesHotReload", () => {
- it("should reload entities with new save variables", async () => {
- const FakeRegistry = vi.fn(
- class {
- addComponent = vi.fn();
- getIndexedZipper = vi.fn(() => [
- {
- id: 2,
- __RESERVED_entityId: {
- entityId: "ent2",
- name: "__RESERVED_entityId",
- },
- },
- {
- id: 3,
- __RESERVED_entityId: {
- entityId: "ent3",
- name: "__RESERVED_entityId",
- },
- },
- ]);
- getEntityComponent = vi.fn((entity: number, component) => {
- return (
- {
- 2: {
- Position: {
- name: "Position",
- x: 3,
- y: 4,
- },
- Bullets: {
- name: "Bullets",
- number: 4,
- bulletTypes: ["9mm"],
- },
- __RESERVED_entityId: {
- entityId: "ent2",
- },
- },
- 3: {
- Position: {
- name: "Position",
- x: 7,
- y: 8,
- },
- __RESERVED_entityId: {
- 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();
- const events = new EventEmitter();
- new CoreEditor(
- {} as unknown as Core,
- {
- save: {
- components,
- entities,
- } as any as Save,
- coreEvents: events,
- } as any as IRunOptions["editor"],
- {
- getAssetManagerLibrary: () => ({
- library: { getAsset: vi.fn((name: string) => ({ path: name })) },
- }),
- getComponentSystemLibrary: () => ({ library: { registry: fakeReg } }),
- } as unknown as ApplicationConfig,
- ).hotReloadEvent({ components, entities } as any as Save);
- expect(fakeReg.getIndexedZipper).toHaveBeenCalledWith([{ name: "__RESERVED_entityId" }]);
- 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: 3, y: 4 });
- expect(fakeReg.addComponent).toHaveBeenNthCalledWith(2, 2, {
- name: "Bullets",
- bulletTypes: ["fire", "water", "rocket"],
- number: 4,
- });
- expect(fakeReg.addComponent).toHaveBeenNthCalledWith(3, 3, { name: "Position", x: 7, y: 8 });
- });
- });
-});
diff --git a/packages/core-editor/test/helpers/event-emitter.ts b/packages/core-editor/test/helpers/event-emitter.ts
deleted file mode 100644
index a06695f2..00000000
--- a/packages/core-editor/test/helpers/event-emitter.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import {
- type IEventEmitter,
- type ListenerType,
- type QueuedEvent,
-} from "../../src/common/context/event-emitter.type";
-
-export class EventEmitter<
- Events extends string,
- EventsMap extends Record,
-> implements IEventEmitter {
- public listeners: {
- [K in keyof EventsMap]?: ListenerType[];
- } = {};
-
- public eventQueue: QueuedEvent[] = [];
- private readonly _dequeueOnEmit: boolean;
-
- constructor(dequeueOnEmit = false) {
- this._dequeueOnEmit = dequeueOnEmit;
- }
-
- runEvents(): void {
- this.eventQueue.forEach((e) => {
- this._executeEvent(e);
- });
-
- this.eventQueue = [];
- }
-
- emitEvent(event: K, ...args: EventsMap[K]): void {
- this.eventQueue.push({
- event,
- args,
- });
- if (this._dequeueOnEmit) this.runEvents();
- }
- addListener(
- event: K,
- listener: ListenerType,
- ): void {
- if (!this.listeners[event]) this.listeners[event] = [];
- this.listeners[event].push(listener);
- }
- on(event: K, listener: ListenerType): void {
- this.addListener(event, listener);
- }
-
- removeListener(
- event: K,
- listener: ListenerType,
- ): void {
- if (!this.listeners[event]) return;
- const index = this.listeners[event].indexOf(listener);
- if (index >= 0) {
- this.listeners[event].splice(index, 1);
- }
- }
- off(event: K, listener: ListenerType): void {
- this.removeListener(event, listener);
- }
-
- removeListenersForEvent(event: keyof EventsMap): void {
- if (!this.listeners[event]) return;
- this.listeners[event] = [];
- }
- removeAllListeners(): void {
- this.listeners = {};
- }
-
- private _executeEvent({
- event,
- args,
- }: QueuedEvent): void {
- this.listeners[event]?.forEach((listener) => {
- try {
- listener(...args);
- } catch (error) {
- console.error(`Error handling event [${String(event)}]:`, error);
- }
- });
- }
-}
diff --git a/packages/core/.idea/.name b/packages/core/.idea/.name
deleted file mode 100644
index 3d3e7caa..00000000
--- a/packages/core/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-[NanoForge] Engine Core
\ No newline at end of file
diff --git a/packages/core/.idea/[NanoForge] Engine Core.iml b/packages/core/.idea/[NanoForge] Engine Core.iml
deleted file mode 100644
index 24643cc3..00000000
--- a/packages/core/.idea/[NanoForge] Engine Core.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/.idea/codeStyles/Project.xml b/packages/core/.idea/codeStyles/Project.xml
deleted file mode 100644
index b70d7533..00000000
--- a/packages/core/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/.idea/codeStyles/codeStyleConfig.xml b/packages/core/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 79ee123c..00000000
--- a/packages/core/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/.idea/git_toolbox_blame.xml b/packages/core/.idea/git_toolbox_blame.xml
deleted file mode 100644
index 7dc12496..00000000
--- a/packages/core/.idea/git_toolbox_blame.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/.idea/git_toolbox_prj.xml b/packages/core/.idea/git_toolbox_prj.xml
deleted file mode 100644
index 02b915b8..00000000
--- a/packages/core/.idea/git_toolbox_prj.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/.idea/inspectionProfiles/Project_Default.xml b/packages/core/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 03d9549e..00000000
--- a/packages/core/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/.idea/jsLinters/eslint.xml b/packages/core/.idea/jsLinters/eslint.xml
deleted file mode 100644
index 541945bb..00000000
--- a/packages/core/.idea/jsLinters/eslint.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/.idea/modules.xml b/packages/core/.idea/modules.xml
deleted file mode 100644
index 99922e22..00000000
--- a/packages/core/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/.idea/prettier.xml b/packages/core/.idea/prettier.xml
deleted file mode 100644
index 0c83ac4e..00000000
--- a/packages/core/.idea/prettier.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/.idea/vcs.xml b/packages/core/.idea/vcs.xml
deleted file mode 100644
index b2bdec2d..00000000
--- a/packages/core/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/core/.nvmrc b/packages/core/.nvmrc
deleted file mode 100644
index a682cfb9..00000000
--- a/packages/core/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-v25
diff --git a/packages/core/LICENSE b/packages/core/LICENSE
index 62c6400b..46724861 100644
--- a/packages/core/LICENSE
+++ b/packages/core/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright © 2025 NanoForge
+Copyright © 2026 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
diff --git a/packages/core/README.md b/packages/core/README.md
index c6579dfe..0833b3b1 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -19,7 +19,7 @@
## Installation
-**Node.js 25 or newer is required.**
+**Node.js 26 or newer is required.**
```sh
npm install @nanoforge-dev/core
diff --git a/packages/core/package.json b/packages/core/package.json
index 94a308f8..b8003f3f 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -13,8 +13,9 @@
"license": "MIT",
"contributors": [
"Bill ",
- "Exelo ",
+ "Exelo ",
"Fexkoser ",
+ "Josephine ",
"Tchips