openvibers/openvibe.games Commits

ADR-0007: rewrite the moddable base (the base is the product, Scraplandia its first pack; Rapier; places and instances; PostgreSQL + Valkey with a write-behind flusher; declarative, QuickJS and Worker mods; phones a real target; OpenVibe.Play for achievements, stats, inventories and lobbies; milestones M1-M6). Nothing on openvibe.games is kept or restored, so M1 starts by deleting the legacy surface, with no shims and no redirects:

OP OpenVibers committed 29 Sept 2026 · cfaf83c3a3 · 418 files, +506 −10892

- the Live legacy import (liveLegacyImport, importLiveLegacy, legacy_live_rows, docs/legacy-import.md);
- identity adoption of ovn: keys (migrateIdentity, identityMigration, the ovn: prefix; sign-in keys on the token's subject and a token without one is refused) and IP-bound guest recovery (guest_ips, GUEST_IP_BINDING); the once-only ledger that account merge and deletion still use is renamed identity_adoptions;
- the /api/texture alias and the /game and /canvas redirects;
- map v1 (migrateV1ToV2, MAIN_TERRAIN_MIGRATED_ID, migrateLegacyMix, the client's legacyLayer, TerrainPatchData.mix): a v1 document is invalid, and the editor e2e checks that it opens empty;
- the committed dist-types build output (366 files, now ignored) and the stale gameplay status doc.
The guest cookie keys hq_token/hq_reload_ts became openvibe.token/openvibe.reload_ts. ADR-0006 notes that its identity adoption is superseded.
The SQLite schema version went 13 -> 11: this build refuses the old world.db on purpose and is not deployed; PostgreSQL replaces the store next (M1), on a fresh database.
Reviewed independently; its findings are fixed here (e2e v1 section, ADR notes) or in Live (links to /game and /canvas). pnpm typecheck, lint, test 843/843.

Changes

ddiff --git a/.gitignore b/.gitignore
iindex da02019..2ef0f72 100644
---- a/.gitignore
++++ b/.gitignore
@@@ -1,5 +1,6 @@
node_modules/
dist/
++dist-types/
*.tsbuildinfo
.env
*.log
ddiff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
iindex 65265e6..09d444c 100644
---- a/ARCHITECTURE.md
++++ b/ARCHITECTURE.md
@@@ -95,12 +95,9 @@ transform, inventory JSON), `meta` (schema version, world-seeded flag).
Dirty tracking batches writes; flush on interval, on settle, and on shutdown.
Each flush is one transaction, together with the outbox events describing
it. First boot seeds from the world definition; afterwards the DB is the
--world's source of truth. Platform tables (schema 12): `identity_legacy_map`,
--`mods`, `mod_grants`, `mod_placements`, `mod_audit`, `media_mirrors`, and the
--SDK's `event_outbox`. Schema 13 adds `legacy_live_rows`: OpenVibe.Live's
--HoboQuest rows, archived verbatim under Network subjects by
--`scripts/importLiveLegacy.ts` ([docs/legacy-import.md](docs/legacy-import.md)).
--The simulation never reads it.
++world's source of truth. Platform tables: `identity_adoptions`, `mods`,
++`mod_grants`, `mod_placements`, `mod_audit`, `media_mirrors`, and the SDK's
++`event_outbox`. The simulation never reads them.
## Platform boundary
ddiff --git a/README.md b/README.md
iindex 5e9b6a2..f86ce2c 100644
---- a/README.md
++++ b/README.md
@@@ -14,7 +14,7 @@ WebSocket protocol · SQLite persistence · pnpm monorepo.
## Owns
- the game: the authoritative simulation, the world, characters, inventories, skills, blueprints and
-- props, in `world.db` (SQLite, forward-only migrations; `SCHEMA_VERSION` 13)
++ props, in `world.db` (SQLite, forward-only migrations; `SCHEMA_VERSION` 11)
- the map editor and its assets, the mod registry (`games-content@1` packs, grants, audit) and the
`games.*` events
- the game WebSocket protocol and the portal, `/play` and `/editor` pages
@@@ -109,8 +109,7 @@ STATIC_DIR=apps/client/dist PORT=8000 DB_PATH=data/world.db pnpm --filter @openv
The server serves the built client, `/healthz` (liveness), `/api/ready`, `/metrics`, and the game
WebSocket on one port. Pages are `/` (portal), `/play` and `/editor`; on play.openvibe.games `/` is
--the game and `/play` redirects there. `/game` and `/canvas` (the URLs from when the game lived on
--OpenVibe.Live) redirect to `/play` and `/`. Any other path that is neither a file of the build nor a route
++the game and `/play` redirects there. Any other path that is neither a file of the build nor a route
answers 404 (an HTML page with links to those pages; `{"error":"not_found"}` under `/api`; plain text
for a missing asset), never the portal with a 200. `/api/ready` is 200 only while `world.db` answers a query and the simulation
has ticked within the last 5 s, otherwise 503 naming the failed check (`status`
@@@ -124,8 +123,7 @@ play.openvibe.games Host-routes to the same process).
Deploy on the host with `sudo /opt/openvibe.games/deploy/scripts/deploy.sh`, which runs
`ovhost deploy games` (OpenVibe.Host, strategy `pnpm-build`; roadmap WS-N task 11): a fast-forward pull as
--the checkout owner (the tracked `dist-types/` a previous build rewrote is restored first),
--`pnpm install --frozen-lockfile`, every workspace package's dependencies checked, `pnpm build`,
++the checkout owner, `pnpm install --frozen-lockfile`, every workspace package's dependencies checked, `pnpm build`,
better-sqlite3 loaded under the host's Node, the restart, `/api/ready`, and on failure the checkout
restored, reinstalled and rebuilt and the server restarted again. Players online are reported and
reconnect; `--wait-idle` holds the restart until nobody plays, `--rollback` runs `ovhost rollback games`,
@@@ -146,10 +144,9 @@ Games integrates with the rest of OpenVibe at its service boundary only
do not know the platform exists. See [ADR-0006](docs/adr/0006-canonical-subjects-and-platform-boundary.md).
- **Identity.** Signed-in players are keyed by their canonical
-- openvibe.network subject (`usr_…`/`gst_…`, the token's `subject_id`).
-- Characters created under the old `ovn:<network id>` key are adopted on the
-- next sign-in, or in bulk with `apps/server/scripts/migrateIdentity.ts`
-- (`--dry-run` first). Guest tokens that look like account keys (`ovn:…`, `usr_…`) are refused.
++ openvibe.network subject (`usr_…`/`gst_…`, the token's `subject_id`); a
++ token without one cannot name an account. Guest tokens that look like
++ account keys (`usr_…`) are refused.
- **Service principal.** Calls to Events, Media and Network identity carry a
client-credentials token of the `games` OAuth client (same client id and
secret as SSO), one per audience. There are no shared keys. The SSO calls
@@@ -188,10 +185,6 @@ do not know the platform exists. See [ADR-0006](docs/adr/0006-canonical-subjects
- **In production (2026-09-23, release `f11e21c`):** events and the Media
mirror are on. `games.world.saved` events reach OpenVibe.Events; no mod is
installed, no grant exists and no asset has been mirrored to Media yet.
-- Player progress and canvas rows from Live's old HoboQuest tables have not
-- been imported yet. The decisions, the importer
-- (`apps/server/scripts/importLiveLegacy.ts`, dry run by default) and the
-- host commands are in [docs/legacy-import.md](docs/legacy-import.md).
- **Shared chrome.** The portal, `/play` and `/editor` load the OpenVibe Frame
(navbar, footer, theme loader) from this server's own pinned copy at `/shared/`
(`apps/server/src/net/sharedAssets.ts`), so they keep their frame while Network is down.
ddiff --git a/ROADMAP.md b/ROADMAP.md
iindex e880d1a..7017f85 100644
---- a/ROADMAP.md
++++ b/ROADMAP.md
@@@ -5,8 +5,6 @@ incomplete, **next** = current development target, **future** = not started.
A system is never "done" because a schema exists — it must have at least one
real playable vertical implementation.
--See `docs/GAMEPLAY_DEVELOPMENT_STATUS.md` for the detailed per-system audit.
--
## Phase 0 — Vertical slice ✅ done
Proved every major system cooperates end-to-end (verified by
@@@ -30,7 +28,7 @@ vitals with eating/drinking, food items + burn-barrel cooking, fall damage,
death/respawn; melee PvP with weapon capability, knockback, zone gating;
container props (storage box) with trusted access; hinged doors; planter
farming prototype (berry seeds, timestamp growth); merchant trades (bottle
--caps); supply-drop crates (hardcoded event v1); day/night clock; guest IP
++caps); supply-drop crates (hardcoded event v1); day/night clock; guest
identity + openvibe.network SSO ranks; map editor as a mature authoring subsystem
with live world reconciliation.
@@@ -153,15 +151,11 @@ with live world reconciliation.
## Platform integration (network roadmap Wave 12) — partial
--- done: canonical Network subjects as account keys + legacy adoption;
-- `games` service principal; durable events via outbox; Media copies of
-- map-editor assets; mod manifest validation, registry (install / grant /
-- enable / disable / revoke, audit) and the capability-checked runtime seam
-- for declarative `games-content@1` packs
--- migration gate: Live's legacy HoboQuest/canvas tables are decided and
-- reconciled ([docs/legacy-import.md](docs/legacy-import.md)): per-player
-- HoboQuest rows are archived under subjects, and the canvas and expired
-- rows are excluded. Dry run on production copies done; apply pending review
++- done: canonical Network subjects as account keys; `games` service
++ principal; durable events via outbox; Media copies of map-editor assets;
++ mod manifest validation, registry (install / grant / enable / disable /
++ revoke, audit) and the capability-checked runtime seam for declarative
++ `games-content@1` packs
- waiting on the platform: mod principals and grants in OpenVibe.Network;
sandboxed executable mods (OpenVibe.Host Stage C); `games.progress.summary`
user-module writes are granted but not implemented
ddiff --git a/apps/client/dist-types/constants.d.ts b/apps/client/dist-types/constants.d.ts
ddeleted file mode 100644
iindex 598eca5..0000000
---- a/apps/client/dist-types/constants.d.ts
++++ /dev/null
@@@ -1,3 +0,0 @@
--/** Mirrors the server's hotbar size (inventory layout arrives via wire, but the HUD needs it before connect). */
--export declare const HOTBAR_SLOTS = 6;
--//# sourceMappingURL=constants.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/constants.d.ts.map b/apps/client/dist-types/constants.d.ts.map
ddeleted file mode 100644
iindex 48ac451..0000000
---- a/apps/client/dist-types/constants.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,iHAAiH;AACjH,eAAO,MAAM,YAAY,IAAI,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/architectureAudit.audit.d.ts b/apps/client/dist-types/editor/architectureAudit.audit.d.ts
ddeleted file mode 100644
iindex 1cfd4ed..0000000
---- a/apps/client/dist-types/editor/architectureAudit.audit.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=architectureAudit.audit.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/architectureAudit.audit.d.ts.map b/apps/client/dist-types/editor/architectureAudit.audit.d.ts.map
ddeleted file mode 100644
iindex 0a0b1b8..0000000
---- a/apps/client/dist-types/editor/architectureAudit.audit.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"architectureAudit.audit.d.ts","sourceRoot":"","sources":["../../src/editor/architectureAudit.audit.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/assets/assetController.d.ts b/apps/client/dist-types/editor/assets/assetController.d.ts
ddeleted file mode 100644
iindex 483e622..0000000
---- a/apps/client/dist-types/editor/assets/assetController.d.ts
++++ /dev/null
@@@ -1,62 +0,0 @@
--import type { EditorDocument } from '../document/editorDocument.js';
--import type { CommandHistory } from '../history/commandHistory.js';
--import type { ModelCache } from '../../render/modelCache.js';
--export interface UploadedAsset {
-- url: string;
-- hash: string;
-- bytes: number;
-- mime: string;
--}
--export interface AssetControllerOptions {
-- doc: EditorDocument;
-- history: CommandHistory<EditorDocument>;
-- modelCache: ModelCache;
-- editorKey: () => string;
-- /** Re-register custom textures with the renderer after a change. */
-- onAssetsChanged: () => void;
-- setMessage: (m: string) => void;
--}
--/** Where a texture reference can appear in the document. */
--export interface TextureUsage {
-- count: number;
-- ids: string[];
--}
--/**
-- * Every place a texture reference lives.
-- *
-- * The previous scan looked at terrain surfaces and `static.tex` only, so a
-- * texture used exclusively by a painted box face or a per-face override
-- * counted as unused — and "delete is refused while referenced" is worthless
-- * if the reference count is wrong.
-- */
--export declare function collectTextureUsage(doc: EditorDocument): Map<string, TextureUsage>;
--export declare function collectModelUsage(doc: EditorDocument): Map<string, TextureUsage>;
--/** A filesystem-ish, collision-free texture name. */
--export declare function sanitizeTextureName(raw: string, taken: (name: string) => boolean): string;
--export declare class AssetController {
-- private readonly opts;
-- constructor(opts: AssetControllerOptions);
-- /** POST raw bytes to the content-addressed store. */
-- private upload;
-- textureUsage(): Map<string, TextureUsage>;
-- modelUsage(): Map<string, TextureUsage>;
-- importTexture(file: File): Promise<string | null>;
-- /**
-- * Import a GLB. Bounds are DERIVED by instantiating it once through the
-- * cache, which also warms it — placing the model afterwards needs no second
-- * parse — rather than asking the user for numbers they cannot know.
-- */
-- importModel(file: File): Promise<string | null>;
-- deleteTexture(name: string): boolean;
-- deleteModel(id: string): boolean;
-- /**
-- * Rename a texture and EVERY reference to it, atomically.
-- *
-- * References are `custom:<name>` strings scattered through terrain and
-- * static surfaces, so renaming the entry alone would leave every user of it
-- * pointing at a texture that no longer exists. One history entry, and undo
-- * restores all of it.
-- */
-- renameTexture(from: string, to: string): boolean;
--}
--//# sourceMappingURL=assetController.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/assets/assetController.d.ts.map b/apps/client/dist-types/editor/assets/assetController.d.ts.map
ddeleted file mode 100644
iindex 250a970..0000000
---- a/apps/client/dist-types/editor/assets/assetController.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"assetController.d.ts","sourceRoot":"","sources":["../../../src/editor/assets/assetController.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAElE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAE5D,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,cAAc,CAAA;IACnB,OAAO,EAAE,cAAc,CAAC,cAAc,CAAC,CAAA;IACvC,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,MAAM,MAAM,CAAA;IACvB,oEAAoE;IACpE,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAED,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,EAAE,CAAA;CACd;AAID;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAkClF;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAWhF;AAED,qDAAqD;AACrD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,MAAM,CAWzF;AAED,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,sBAAsB;IAEzD,qDAAqD;YACvC,MAAM;IAepB,YAAY,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC;IAIzC,UAAU,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC;IAMjC,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAwBvD;;;;OAIG;IACG,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAyCrD,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAsBpC,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAuBhC;;;;;;;OAOG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO;CAoCjD"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/assets/assetController.test.d.ts b/apps/client/dist-types/editor/assets/assetController.test.d.ts
ddeleted file mode 100644
iindex 407310c..0000000
---- a/apps/client/dist-types/editor/assets/assetController.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=assetController.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/assets/assetController.test.d.ts.map b/apps/client/dist-types/editor/assets/assetController.test.d.ts.map
ddeleted file mode 100644
iindex dfbf3b6..0000000
---- a/apps/client/dist-types/editor/assets/assetController.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"assetController.test.d.ts","sourceRoot":"","sources":["../../../src/editor/assets/assetController.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/bindings.d.ts b/apps/client/dist-types/editor/bindings.d.ts
ddeleted file mode 100644
iindex 124d146..0000000
---- a/apps/client/dist-types/editor/bindings.d.ts
++++ /dev/null
@@@ -1,32 +0,0 @@
--/**
-- * Centralized editor keybindings: every keyboard action routes through one
-- * structured, remappable system (KeyboardEvent.code + modifiers), with
-- * conflict detection for the settings UI. Pure module — unit tested.
-- */
--export interface Binding {
-- code: string;
-- ctrl?: boolean;
-- shift?: boolean;
-- alt?: boolean;
-- meta?: boolean;
--}
--export interface ActionDef {
-- id: string;
-- label: string;
-- group: 'camera' | 'tools' | 'transform' | 'edit' | 'terrain' | 'ui';
-- default: Binding;
-- /** Held key (movement) rather than a triggered action. */
-- hold?: boolean;
-- /** Input context: same chord in DIFFERENT contexts is not a conflict. */
-- context?: 'global' | 'camera-fly' | 'camera-drag' | 'placement' | 'selection';
--}
--export declare const ACTIONS: ActionDef[];
--export declare function bindingMatches(b: Binding, e: KeyboardEvent): boolean;
--export declare function formatBinding(b: Binding): string;
--export declare function bindingFromEvent(e: KeyboardEvent): Binding;
--export declare function sameBinding(a: Binding, b: Binding): boolean;
--/** Action ids whose current binding collides with `b` (excluding `except`). */
--export declare function findConflicts(bindings: Record<string, Binding>, b: Binding, except?: string): string[];
--export declare function defaultBindings(): Record<string, Binding>;
--export declare function loadBindings(stored: string | null): Record<string, Binding>;
--//# sourceMappingURL=bindings.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/bindings.d.ts.map b/apps/client/dist-types/editor/bindings.d.ts.map
ddeleted file mode 100644
iindex 0f35419..0000000
---- a/apps/client/dist-types/editor/bindings.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../../src/editor/bindings.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,QAAQ,GAAG,OAAO,GAAG,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IACnE,OAAO,EAAE,OAAO,CAAA;IAChB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,yEAAyE;IACzE,OAAO,CAAC,EAAE,QAAQ,GAAG,YAAY,GAAG,aAAa,GAAG,WAAW,GAAG,WAAW,CAAA;CAC9E;AAED,eAAO,MAAM,OAAO,EAAE,SAAS,EAuJ9B,CAAA;AAUD,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,aAAa,GAAG,OAAO,CAepE;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAchD;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,OAAO,CAW1D;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAQ3D;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,EAAE,OAAO,EACV,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,EAAE,CAYV;AAED,wBAAgB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIzD;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAY3E"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/bindings.test.d.ts b/apps/client/dist-types/editor/bindings.test.d.ts
ddeleted file mode 100644
iindex f385973..0000000
---- a/apps/client/dist-types/editor/bindings.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=bindings.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/bindings.test.d.ts.map b/apps/client/dist-types/editor/bindings.test.d.ts.map
ddeleted file mode 100644
iindex 9fb2b67..0000000
---- a/apps/client/dist-types/editor/bindings.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"bindings.test.d.ts","sourceRoot":"","sources":["../../src/editor/bindings.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/catalog.d.ts b/apps/client/dist-types/editor/catalog.d.ts
ddeleted file mode 100644
iindex 2cdebe7..0000000
---- a/apps/client/dist-types/editor/catalog.d.ts
++++ /dev/null
@@@ -1,33 +0,0 @@
--/**
-- * The editor's authoring palettes and shared ids.
-- *
-- * Pure data: what the Mesh and Entity tools can place, which stock textures
-- * exist, and how resource nodes are represented in the viewport. Kept out of
-- * main.ts so adding a placeable is a data edit, not surgery on boot().
-- */
--import type { StaticBody } from '@openvibe/content';
--export type Tool = 'terrain' | 'paint' | 'entity' | 'mesh' | 'select' | 'face' | 'light' | 'zone';
--export declare const newId: (prefix: string) => string;
--export interface Placeable {
-- name: string;
-- kind: 'static' | 'node' | 'spawn' | 'model' | 'prop' | 'patch';
-- shape?: StaticBody['shape'];
-- color?: string;
-- tex?: string;
-- decor?: string;
-- node?: string;
-- modelId?: string;
--}
--/** Mesh tool: primitive shapes the user builds everything from (plus
-- * imported glb models, appended at runtime). No prefab world props —
-- * buildings, ramps and furniture are authored, not picked. */
--export declare const PLACEABLES: Placeable[];
--/** Entity tool: gameplay spawns — not geometry. */
--export declare const ENTITY_DEFS: Placeable[];
--export declare const TEXTURES: string[];
--/** Visual stand-ins for resource nodes (matched loosely to the game's). */
--export declare const NODE_LOOKS: Record<string, {
-- color: string;
-- shape: StaticBody['shape'];
--}>;
--//# sourceMappingURL=catalog.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/catalog.d.ts.map b/apps/client/dist-types/editor/catalog.d.ts.map
ddeleted file mode 100644
iindex 020a02b..0000000
---- a/apps/client/dist-types/editor/catalog.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/editor/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD,MAAM,MAAM,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AAIjG,eAAO,MAAM,KAAK,GAAI,QAAQ,MAAM,KAAG,MAC+B,CAAA;AAEtE,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAA;IAC9D,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;+DAE+D;AAC/D,eAAO,MAAM,UAAU,EAAE,SAAS,EAgBjC,CAAA;AAED,mDAAmD;AACnD,eAAO,MAAM,WAAW,EAAE,SAAS,EAWlC,CAAA;AAED,eAAO,MAAM,QAAQ,UAYpB,CAAA;AAED,2EAA2E;AAC3E,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,CAAA;CAAE,CAOpF,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/collaboration/editorConnection.d.ts b/apps/client/dist-types/editor/collaboration/editorConnection.d.ts
ddeleted file mode 100644
iindex bfa18a5..0000000
---- a/apps/client/dist-types/editor/collaboration/editorConnection.d.ts
++++ /dev/null
@@@ -1,68 +0,0 @@
--/**
-- * The editor's live channel: presence, remote selection, locks, and the push
-- * that says the map was saved.
-- *
-- * Speaks `@openvibe/protocol`'s editor messages, decoded rather than cast. The
-- * previous version of this file and the server had drifted apart — no
-- * `hello`, a tag the server never sent, no camera stream — so collaboration
-- * silently did nothing at all.
-- *
-- * The server assigns the peer id and colour and owns the lock table. A client
-- * that could choose its own identity could impersonate another editor's
-- * selection, and one that granted its own locks would not have a lock.
-- */
--import { Color3 } from '@babylonjs/core/Maths/math.color.js';
--import type { FreeCamera } from '@babylonjs/core/Cameras/freeCamera.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--export interface EditorConnectionOptions {
-- scene: Scene;
-- peersEl: HTMLElement;
-- camera: FreeCamera;
-- /** The editor credential, read at connect time. */
-- keyOf: () => string;
-- /** Display name for other editors. */
-- nameOf: () => string;
-- onRemoteSaved: () => void;
-- onChange: () => void;
-- onLockLost: () => void;
-- onLockDenied: (owner: string) => void;
--}
--export interface RemotePeer {
-- peerId: number;
-- name: string;
-- color: string;
-- selection: string[];
--}
--/**
-- * The lock vocabulary the editor actually needs.
-- *
-- * `canInspect` and `owns` are deliberately different questions. Treating
-- * "nobody else has it" as permission to mutate is how a server-authoritative
-- * lock system ends up never being acquired: both editors think they may
-- * proceed, and the arbitration never runs.
-- */
--export interface EditorConnection {
-- connect: () => void;
-- disconnect: () => void;
-- sendSelection: (ids: readonly string[]) => void;
-- /** Reading is always allowed, even for an object someone else holds. */
-- canInspect: (id: string) => boolean;
-- isLockedByOther: (id: string) => boolean;
-- owns: (id: string) => boolean;
-- ownsAll: (ids: readonly string[]) => boolean;
-- /** Request `ids`; `then` runs once they are actually granted. */
-- acquire: (ids: readonly string[], then?: () => void) => boolean;
-- release: (ids: readonly string[]) => void;
-- releaseAll: () => void;
-- lockOwner: (id: string) => string | null;
-- lockOwners: () => Map<string, string>;
-- lockColors: () => Map<string, string>;
-- remoteSelectionColors: () => Map<string, string>;
-- peers: () => RemotePeer[];
-- peerCount: () => number;
-- myPeerId: () => number;
-- connected: () => boolean;
--}
--export declare function createEditorConnection(opts: EditorConnectionOptions): EditorConnection;
--export { Color3 };
--//# sourceMappingURL=editorConnection.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/collaboration/editorConnection.d.ts.map b/apps/client/dist-types/editor/collaboration/editorConnection.d.ts.map
ddeleted file mode 100644
iindex c8bc454..0000000
---- a/apps/client/dist-types/editor/collaboration/editorConnection.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorConnection.d.ts","sourceRoot":"","sources":["../../../src/editor/collaboration/editorConnection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAA;AAS5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAA;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAIrD,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,KAAK,CAAA;IACZ,OAAO,EAAE,WAAW,CAAA;IACpB,MAAM,EAAE,UAAU,CAAA;IAClB,mDAAmD;IACnD,KAAK,EAAE,MAAM,MAAM,CAAA;IACnB,sCAAsC;IACtC,MAAM,EAAE,MAAM,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACtC;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,aAAa,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAA;IAE/C,wEAAwE;IACxE,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;IACnC,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;IACxC,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;IAC7B,OAAO,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAA;IAC5C,iEAAiE;IACjE,OAAO,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,IAAI,KAAK,OAAO,CAAA;IAC/D,OAAO,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAA;IACzC,UAAU,EAAE,MAAM,IAAI,CAAA;IAEtB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;IACxC,UAAU,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,UAAU,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,qBAAqB,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChD,KAAK,EAAE,MAAM,UAAU,EAAE,CAAA;IACzB,SAAS,EAAE,MAAM,MAAM,CAAA;IACvB,QAAQ,EAAE,MAAM,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,OAAO,CAAA;CACzB;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,uBAAuB,GAAG,gBAAgB,CAyLtF;AAED,OAAO,EAAE,MAAM,EAAE,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/collaboration/lockController.d.ts b/apps/client/dist-types/editor/collaboration/lockController.d.ts
ddeleted file mode 100644
iindex 05c828b..0000000
---- a/apps/client/dist-types/editor/collaboration/lockController.d.ts
++++ /dev/null
@@@ -1,89 +0,0 @@
--/**
-- * Client half of the editing locks. The SERVER arbitrates — this only asks,
-- * tracks what it was granted, and makes the answer visible.
-- *
-- * The rules that matter:
-- * - Hovering never acquires anything. Locks follow intent to EDIT, not
-- * attention, or moving the mouse across a busy map would lock it solid.
-- * - A group edit is ALL OR NOTHING. A transform that could only move half
-- * its members is worse than one that does not start.
-- * - Read-only selection of a locked object is fine; mutation is refused.
-- * - Losing a lease mid-gesture cancels the gesture back to its exact start,
-- * rather than leaving half-applied local state the server disagrees with.
-- */
--export type LockState = 'unlocked' | 'pending' | 'owned' | 'denied' | 'lost';
--export interface LockOwner {
-- peerId: number;
-- name: string;
-- color: string;
--}
--export interface LockTransport {
-- /** Ask the server for these ids, atomically. */
-- request: (ids: readonly string[]) => void;
-- release: (ids: readonly string[]) => void;
--}
--export interface LockControllerEvents {
-- /** Granted: the pending gesture may proceed. */
-- onGranted?: (ids: readonly string[]) => void;
-- onDenied?: (owner: string) => void;
-- /** The lease went away mid-gesture; roll back. */
-- onLost?: () => void;
-- onChange?: () => void;
--}
--export declare class LockController {
-- private readonly transport;
-- private readonly events;
-- private state;
-- private held;
-- private pending;
-- /** object id → who holds it, from the server's authoritative broadcast. */
-- private owners;
-- private myPeerId;
-- constructor(transport: LockTransport, events?: LockControllerEvents);
-- setPeerId(id: number): void;
-- get current(): LockState;
-- get heldIds(): readonly string[];
-- /** Who holds `id`, if anyone else does. */
-- ownerOf(id: string): LockOwner | null;
-- /** Every object someone else is editing, for the Outliner and visuals. */
-- remoteLocks(): Map<string, LockOwner>;
-- /**
-- * Do WE hold this lock?
-- *
-- * Deliberately not the same question as "is it free". Treating "nobody
-- * else has it" as permission to mutate is how a server-authoritative lock
-- * system ends up never being acquired at all: both editors decide they may
-- * proceed and the arbitration never runs.
-- */
-- owns(id: string): boolean;
-- /** All of them, or none: a partial group transform is not offered. */
-- ownsAll(ids: readonly string[]): boolean;
-- /** Free as far as the server's last broadcast says. NOT authorization. */
-- isFree(id: string): boolean;
-- /**
-- * Ask to edit `ids`. Returns true when the locks are already held, in
-- * which case the caller may proceed immediately; otherwise `then` runs on
-- * the grant.
-- */
-- acquire(ids: readonly string[], then?: () => void): boolean;
-- /** Server said yes. */
-- granted(ids: readonly string[]): void;
-- /** Server said no. Nothing local has been mutated at this point. */
-- denied(owner: string): void;
-- /**
-- * The server's authoritative lock table. This is also where a LOST lease
-- * is detected: we believe we hold ids the server no longer attributes to
-- * us (expiry, a reconnect race), so whatever is mid-gesture must roll back.
-- */
-- setOwners(owners: ReadonlyMap<string, LockOwner>): void;
-- /**
-- * Give back just these. Used when a selection shrinks: keeping a lock on
-- * something you deselected blocks a collaborator for no reason.
-- */
-- release(ids: readonly string[]): void;
-- /** Deselect everything, cancel, reload: give them all back. */
-- releaseAll(): void;
-- /** Connection dropped: the server will expire the leases; forget locally. */
-- disconnected(): void;
--}
--//# sourceMappingURL=lockController.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/collaboration/lockController.d.ts.map b/apps/client/dist-types/editor/collaboration/lockController.d.ts.map
ddeleted file mode 100644
iindex 56e5cc8..0000000
---- a/apps/client/dist-types/editor/collaboration/lockController.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"lockController.d.ts","sourceRoot":"","sources":["../../../src/editor/collaboration/lockController.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE5E,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,OAAO,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAA;CAC1C;AAED,MAAM,WAAW,oBAAoB;IACnC,gDAAgD;IAChD,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,KAAK,IAAI,CAAA;IAC5C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,qBAAa,cAAc;IASvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,OAAO,CAAoD;IACnE,2EAA2E;IAC3E,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,QAAQ,CAAK;gBAGF,SAAS,EAAE,aAAa,EACxB,MAAM,GAAE,oBAAyB;IAGpD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAI3B,IAAI,OAAO,IAAI,SAAS,CAEvB;IAED,IAAI,OAAO,IAAI,SAAS,MAAM,EAAE,CAE/B;IAED,2CAA2C;IAC3C,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAKrC,0EAA0E;IAC1E,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC;IAMrC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIzB,sEAAsE;IACtE,OAAO,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO;IAIxC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAI3B;;;;OAIG;IACH,OAAO,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO;IAiB3D,uBAAuB;IACvB,OAAO,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;IAUrC,oEAAoE;IACpE,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAO3B;;;;OAIG;IACH,SAAS,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,IAAI;IAavD;;;OAGG;IACH,OAAO,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;IASrC,+DAA+D;IAC/D,UAAU,IAAI,IAAI;IAQlB,6EAA6E;IAC7E,YAAY,IAAI,IAAI;CAOrB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/collaboration/lockController.test.d.ts b/apps/client/dist-types/editor/collaboration/lockController.test.d.ts
ddeleted file mode 100644
iindex f6e16d3..0000000
---- a/apps/client/dist-types/editor/collaboration/lockController.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=lockController.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/collaboration/lockController.test.d.ts.map b/apps/client/dist-types/editor/collaboration/lockController.test.d.ts.map
ddeleted file mode 100644
iindex 36510c3..0000000
---- a/apps/client/dist-types/editor/collaboration/lockController.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"lockController.test.d.ts","sourceRoot":"","sources":["../../../src/editor/collaboration/lockController.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/collaboration/peerAvatars.d.ts b/apps/client/dist-types/editor/collaboration/peerAvatars.d.ts
ddeleted file mode 100644
iindex b953165..0000000
---- a/apps/client/dist-types/editor/collaboration/peerAvatars.d.ts
++++ /dev/null
@@@ -1,14 +0,0 @@
--import type { Scene } from '@babylonjs/core/scene.js';
--export declare class PeerAvatars {
-- private readonly scene;
-- private readonly labelEl;
-- private readonly avatars;
-- constructor(scene: Scene, labelEl: HTMLElement);
-- get count(): number;
-- /** Create or move a peer's avatar. */
-- update(id: number, name: string, pos: number[], yaw: number, pitch: number): void;
-- remove(id: number): void;
-- private refreshLabel;
-- private create;
--}
--//# sourceMappingURL=peerAvatars.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/collaboration/peerAvatars.d.ts.map b/apps/client/dist-types/editor/collaboration/peerAvatars.d.ts.map
ddeleted file mode 100644
iindex b24fb4f..0000000
---- a/apps/client/dist-types/editor/collaboration/peerAvatars.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"peerAvatars.d.ts","sourceRoot":"","sources":["../../../src/editor/collaboration/peerAvatars.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAiCrD,qBAAa,WAAW;IAIpB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJ1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;gBAGrC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,WAAW;IAGvC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,sCAAsC;IACtC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAWjF,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAQxB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,MAAM;CA4Bf"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/devProbe.d.ts b/apps/client/dist-types/editor/devProbe.d.ts
ddeleted file mode 100644
iindex b9d4787..0000000
---- a/apps/client/dist-types/editor/devProbe.d.ts
++++ /dev/null
@@@ -1,47 +0,0 @@
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { FreeCamera } from '@babylonjs/core/Cameras/freeCamera.js';
--import type { AssetController } from './assets/assetController.js';
--import type { EditorConnection } from './collaboration/editorConnection.js';
--import type { EditorDocument } from './document/editorDocument.js';
--import type { CommandHistory } from './history/commandHistory.js';
--import type { InteractionController } from './interaction/interactionController.js';
--import type { ViewportInteraction } from './interaction/viewportInteraction.js';
--import type { SaveController } from './net/saveController.js';
--import type { FaceOverlayManager, FaceSelection } from './selection/faceSelection.js';
--import type { SelectionManager } from './selection/selectionManager.js';
--import type { ToolManager } from './tools/toolManager.js';
--import type { EditorCameraController } from './viewport/editorCameraController.js';
--import type { EditorViewRegistry } from './viewport/editorViewRegistry.js';
--import type { GizmoController } from './viewport/gizmoController.js';
--import type { TransformSession } from './viewport/transformSession.js';
--import type { EditorUi } from './ui/editorUi.js';
--/** Everything the probe reads. Live references, resolved at call time. */
--export interface EditorProbeDeps {
-- doc: EditorDocument;
-- views: EditorViewRegistry;
-- selection: SelectionManager;
-- history: CommandHistory;
-- tools: ToolManager;
-- gizmo: GizmoController;
-- xform: TransformSession;
-- interaction: InteractionController;
-- viewport: ViewportInteraction;
-- cameraController: EditorCameraController;
-- camera: FreeCamera;
-- canvas: HTMLCanvasElement;
-- scene: Scene;
-- faceSelection: FaceSelection;
-- faceOverlays: FaceOverlayManager;
-- saveController: SaveController;
-- connection: EditorConnection;
-- assets: AssetController;
-- ui: EditorUi;
-- /** The terrain a stroke is currently targeting — mutable, hence a getter. */
-- strokeTargetId: () => string | null;
-- placeModel: (modelId: string) => void;
-- withLock: (ids: readonly string[], then: () => void) => boolean;
-- refreshPivot: () => void;
--}
--/** Attach the harness surface to `window`. Called once, at the end of boot. */
--export declare function installEditorProbe(d: EditorProbeDeps): void;
--//# sourceMappingURL=devProbe.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/devProbe.d.ts.map b/apps/client/dist-types/editor/devProbe.d.ts.map
ddeleted file mode 100644
iindex 802ec2e..0000000
---- a/apps/client/dist-types/editor/devProbe.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"devProbe.d.ts","sourceRoot":"","sources":["../../src/editor/devProbe.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAA;AACvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAA;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAElE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAEjE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAA;AACnF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAA;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAE7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAA;AACrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAA;AAClF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AACpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AAEtE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAEhD,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,cAAc,CAAA;IACnB,KAAK,EAAE,kBAAkB,CAAA;IACzB,SAAS,EAAE,gBAAgB,CAAA;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,KAAK,EAAE,WAAW,CAAA;IAClB,KAAK,EAAE,eAAe,CAAA;IACtB,KAAK,EAAE,gBAAgB,CAAA;IACvB,WAAW,EAAE,qBAAqB,CAAA;IAClC,QAAQ,EAAE,mBAAmB,CAAA;IAC7B,gBAAgB,EAAE,sBAAsB,CAAA;IACxC,MAAM,EAAE,UAAU,CAAA;IAClB,MAAM,EAAE,iBAAiB,CAAA;IACzB,KAAK,EAAE,KAAK,CAAA;IACZ,aAAa,EAAE,aAAa,CAAA;IAC5B,YAAY,EAAE,kBAAkB,CAAA;IAChC,cAAc,EAAE,cAAc,CAAA;IAC9B,UAAU,EAAE,gBAAgB,CAAA;IAC5B,MAAM,EAAE,eAAe,CAAA;IACvB,EAAE,EAAE,QAAQ,CAAA;IACZ,6EAA6E;IAC7E,cAAc,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACnC,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,OAAO,CAAA;IAC/D,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,eAAe,GAAG,IAAI,CAiO3D"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/document/editorDocument.d.ts b/apps/client/dist-types/editor/document/editorDocument.d.ts
ddeleted file mode 100644
iindex 5a73afa..0000000
---- a/apps/client/dist-types/editor/document/editorDocument.d.ts
++++ /dev/null
@@@ -1,171 +0,0 @@
--/**
-- * EditorDocument — the ONE authority for map data in the editor.
-- *
-- * Before this, the editor held the map in six mutable arrays (`placedStatics`,
-- * `placedNodes`, `placedProps`, `patches`, `mapLightsArr`, `mapZones`) plus a
-- * copy of each object's transform on its Babylon mesh, plus another copy in
-- * every undo entry. Nothing said which was true. A remote merge updated some;
-- * a gizmo drag updated others; an undo restored a third set. Bugs of the form
-- * "the inspector says 4 but the saved file says 2" are not fixable while that
-- * is the shape of the code.
-- *
-- * So: the document owns map data, addressed by stable id. Babylon meshes are a
-- * PROJECTION of it (see `viewport/editorViewRegistry.ts`), history stores
-- * before/after document values (never mesh references or array indexes), and
-- * selection stores ids. Destroying and rebuilding a mesh cannot invalidate
-- * identity, history, locks, or selection, because none of them point at it.
-- *
-- * The document deliberately does NOT know about Babylon, the DOM, or the
-- * server. It is a validated `MapFileV2` with an index and a change feed.
-- */
--import { SPAWN_OBJECT_ID, type MapFileV2, type MapModelV2, type MapTextureEntry, type MapLightV2, type MapNodeV2, type MapPropV2, type MapZoneV2, type StaticObjectV2, type TerrainObjectV2 } from '@openvibe/content';
--/**
-- * The spawn point is stored top-level on the wire (`spawn` / `spawnYaw`) and
-- * changing that would break every existing v2 artifact for no gain. The editor
-- * still needs it to BE an object — selectable, undoable, lockable, listed in
-- * the Outliner — so the document exposes it as a synthetic one under the
-- * reserved id `spawn`, and folds it back into the wire on serialize.
-- */
--export interface SpawnObjectV2 {
-- id: typeof SPAWN_OBJECT_ID;
-- pos: [number, number, number];
-- yaw: number;
--}
--export interface EditorObjectByKind {
-- terrain: TerrainObjectV2;
-- static: StaticObjectV2;
-- node: MapNodeV2;
-- prop: MapPropV2;
-- light: MapLightV2;
-- zone: MapZoneV2;
-- spawn: SpawnObjectV2;
--}
--export type EditorObjectKind = keyof EditorObjectByKind;
--export type EditorObject = EditorObjectByKind[EditorObjectKind];
--export declare const EDITOR_OBJECT_KINDS: readonly EditorObjectKind[];
--/**
-- * What changed, by stable id, with enough detail for a view to update
-- * incrementally instead of rebuilding the world.
-- *
-- * `updated` and `replaced` both carry the top-level keys that differ, so a
-- * view can skip an expensive rebuild for a change it does not care about —
-- * repainting a tint must not rebuild collision. `replaced` used to carry
-- * none, so every Inspector edit (which replaces the whole object, to make one
-- * clean undo step) looked like "everything changed" and rebuilt the mesh —
-- * re-instantiating an imported model to alter its tint.
-- */
--export type DocumentChange = {
-- type: 'added';
-- id: string;
-- kind: EditorObjectKind;
--} | {
-- type: 'removed';
-- id: string;
-- kind: EditorObjectKind;
--} | {
-- type: 'updated';
-- id: string;
-- kind: EditorObjectKind;
-- keys: readonly string[];
--} | {
-- type: 'replaced';
-- id: string;
-- kind: EditorObjectKind;
-- keys: readonly string[];
--}
--/** Model/texture metadata changed (imported, renamed, removed). */
-- | {
-- type: 'assetsChanged';
--} | {
-- type: 'documentReplaced';
--};
--export type DocumentListener = (changes: readonly DocumentChange[], revision: number) => void;
--export declare class EditorDocument {
-- /** Bumps on every committed mutation. Views compare it to skip work. */
-- private rev;
-- private map;
-- /** id → kind. The one place identity is resolved. */
-- private index;
-- /**
-- * id → the live object. `get` used to scan the wire array, and `get` is the
-- * hottest read in the editor — hover picking, every view update, the
-- * Outliner and the Inspector all go through it — so on a large map every
-- * mouse move walked hundreds of entries. Writes keep this in step; `update`
-- * mutates in place, so only add/replace/remove/reindex touch it.
-- */
-- private byId;
-- private spawn;
-- private readonly listeners;
-- /** Non-null while a transaction is open; changes batch into it. */
-- private pending;
-- constructor(map?: MapFileV2);
-- get revision(): number;
-- has(id: string): boolean;
-- typeOf(id: string): EditorObjectKind | null;
-- /**
-- * The live object for `id`, or null. Callers MUST NOT mutate it: go through
-- * `update`/`replace` so the change is announced and the revision moves. The
-- * return is live rather than a copy because reads (hover, rendering, the
-- * Outliner) vastly outnumber writes; `snapshot` is the copying read.
-- */
-- get(id: string): EditorObject | null;
-- get<K extends EditorObjectKind>(id: string, kind: K): EditorObjectByKind[K] | null;
-- /** Every object, in Outliner order (terrain, static, node, prop, light, zone, spawn). */
-- list(): EditorObject[];
-- listByKind<K extends EditorObjectKind>(kind: K): EditorObjectByKind[K][];
-- ids(): string[];
-- /** A detached copy — what history stores as its before/after value. */
-- snapshot(id: string): EditorObject | null;
-- snapshotMany(ids: readonly string[]): Map<string, EditorObject>;
-- /**
-- * Insert an object. The kind decides which wire array it joins; ids are
-- * global, so a duplicate is a programming error rather than a silent
-- * overwrite of whatever happened to share the name.
-- */
-- add<K extends EditorObjectKind>(kind: K, object: EditorObjectByKind[K]): void;
-- /** Shallow-merge a patch. Absent keys are untouched; `undefined` deletes. */
-- update<K extends EditorObjectKind>(id: string, patch: Partial<EditorObjectByKind[K]>): void;
-- /** Swap the whole value — what undo of a property edit restores. */
-- replace<K extends EditorObjectKind>(id: string, value: EditorObjectByKind[K]): void;
-- remove(id: string): void;
-- /**
-- * Run `body` as one change batch: listeners see a single notification with
-- * every change in it. One user gesture is one notification, so a multi-
-- * object drag does not make the Outliner rebuild once per object.
-- *
-- * Nesting joins the outer batch rather than opening a second one.
-- */
-- transact<T>(body: () => T): T;
-- models(): readonly MapModelV2[];
-- textures(): readonly MapTextureEntry[];
-- modelById(id: string): MapModelV2 | null;
-- textureByName(name: string): MapTextureEntry | null;
-- addModelAsset(model: MapModelV2): void;
-- removeModelAsset(id: string): void;
-- addTextureAsset(texture: MapTextureEntry): void;
-- removeTextureAsset(name: string): void;
-- /**
-- * Rename a texture. Callers are responsible for the REFERENCES —
-- * `custom:<name>` appears in terrain and static surfaces — which is why
-- * the Asset Browser does this inside one history transaction rather than
-- * calling it directly.
-- */
-- renameTextureAsset(from: string, to: string): void;
-- /** Replace the whole asset metadata set (undo of a rename, import). */
-- replaceAssets(models: readonly MapModelV2[], textures: readonly MapTextureEntry[]): void;
-- /** The canonical wire form, spawn folded back to top level. */
-- serialize(): MapFileV2;
-- /**
-- * Adopt a document fetched from the server. One `documentReplaced` change:
-- * views rebuild, but ids that still exist keep their identity, so selection
-- * and locks survive a remote save that did not touch what you had selected.
-- */
-- replaceFromRemote(next: MapFileV2): void;
-- subscribe(listener: DocumentListener): () => void;
-- private arrayFor;
-- private mutableFor;
-- private reindex;
-- private emit;
-- private notify;
--}
--//# sourceMappingURL=editorDocument.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/document/editorDocument.d.ts.map b/apps/client/dist-types/editor/document/editorDocument.d.ts.map
ddeleted file mode 100644
iindex 73c7291..0000000
---- a/apps/client/dist-types/editor/document/editorDocument.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorDocument.d.ts","sourceRoot":"","sources":["../../../src/editor/document/editorDocument.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/document/editorDocument.test.d.ts b/apps/client/dist-types/editor/document/editorDocument.test.d.ts
ddeleted file mode 100644
iindex 0145e52..0000000
---- a/apps/client/dist-types/editor/document/editorDocument.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=editorDocument.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/document/editorDocument.test.d.ts.map b/apps/client/dist-types/editor/document/editorDocument.test.d.ts.map
ddeleted file mode 100644
iindex 001a9e2..0000000
---- a/apps/client/dist-types/editor/document/editorDocument.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorDocument.test.d.ts","sourceRoot":"","sources":["../../../src/editor/document/editorDocument.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/document/editorObject.d.ts b/apps/client/dist-types/editor/document/editorObject.d.ts
ddeleted file mode 100644
iindex a0051c9..0000000
---- a/apps/client/dist-types/editor/document/editorObject.d.ts
++++ /dev/null
@@@ -1,42 +0,0 @@
--import { SPAWN_OBJECT_ID } from '@openvibe/content';
--import type { EditorDocument, EditorObject, EditorObjectKind } from './editorDocument.js';
--import { type EditorTransform } from '../viewport/transformMath.js';
--export interface KindInfo {
-- label: string;
-- /** Outliner group heading. */
-- group: string;
-- icon: string;
-- /** Whether the gizmo may move it. */
-- transformable: boolean;
-- /** Whether rotation is meaningful (a resource node has no facing). */
-- rotatable: boolean;
-- /** Whether non-uniform scale is meaningful. */
-- scalable: boolean;
--}
--export declare const KIND_INFO: Record<EditorObjectKind, KindInfo>;
--/** Author-facing name for the Outliner and the inspector title. */
--export declare function displayName(kind: EditorObjectKind, object: EditorObject): string;
--/** Rotation as a quaternion for a light's `dir` vector, and back. */
--export declare function quatFromDir(dir: readonly number[] | undefined): [number, number, number, number];
--export declare function dirFromQuat(q: readonly [number, number, number, number]): [number, number, number];
--/**
-- * The canonical transform of any document object.
-- *
-- * Every kind stores its pose differently on the wire — statics have
-- * pos/yaw/rot/scale, terrains pos/rot/scale, nodes just a ground position,
-- * lights a position and a direction vector, zones a min/max box — and the
-- * gizmo should not know or care. This is the one translation.
-- */
--export declare function transformOf(doc: EditorDocument, id: string): EditorTransform | null;
--/** Ground sampler, so nodes and props stay on the terrain when moved. */
--export type GroundSampler = (x: number, z: number) => number;
--/**
-- * Write a transform back into the document. Produces exactly the wire shape
-- * each kind expects, dropping identity rotation/scale so a canonical save
-- * does not gain noise on every drag.
-- */
--export declare function setTransform(doc: EditorDocument, id: string, t: EditorTransform): void;
--/** Ids that a gizmo may currently move. */
--export declare function transformableIds(doc: EditorDocument, ids: readonly string[]): string[];
--export { SPAWN_OBJECT_ID };
--//# sourceMappingURL=editorObject.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/document/editorObject.d.ts.map b/apps/client/dist-types/editor/document/editorObject.d.ts.map
ddeleted file mode 100644
iindex 720bb1a..0000000
---- a/apps/client/dist-types/editor/document/editorObject.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorObject.d.ts","sourceRoot":"","sources":["../../../src/editor/document/editorObject.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AACzF,OAAO,EAA+B,KAAK,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAEhG,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,qCAAqC;IACrC,aAAa,EAAE,OAAO,CAAA;IACtB,sEAAsE;IACtE,SAAS,EAAE,OAAO,CAAA;IAClB,+CAA+C;IAC/C,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAyDxD,CAAA;AAED,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,CAsBhF;AAED,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAmBhG;AAED,wBAAgB,WAAW,CACzB,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAC3C,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAc1B;AAID;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CA2DnF;AAED,yEAAyE;AACzE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;AAE5D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,eAAe,GAAG,IAAI,CA2DtF;AAED,2CAA2C;AAC3C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAKtF;AAED,OAAO,EAAE,eAAe,EAAE,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/document/editorObject.test.d.ts b/apps/client/dist-types/editor/document/editorObject.test.d.ts
ddeleted file mode 100644
iindex 552dd26..0000000
---- a/apps/client/dist-types/editor/document/editorObject.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=editorObject.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/document/editorObject.test.d.ts.map b/apps/client/dist-types/editor/document/editorObject.test.d.ts.map
ddeleted file mode 100644
iindex 4d54c3a..0000000
---- a/apps/client/dist-types/editor/document/editorObject.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorObject.test.d.ts","sourceRoot":"","sources":["../../../src/editor/document/editorObject.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/editorApp.d.ts b/apps/client/dist-types/editor/editorApp.d.ts
ddeleted file mode 100644
iindex f3a99c6..0000000
---- a/apps/client/dist-types/editor/editorApp.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export declare function bootEditor(): Promise<void>;
--//# sourceMappingURL=editorApp.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/editorApp.d.ts.map b/apps/client/dist-types/editor/editorApp.d.ts.map
ddeleted file mode 100644
iindex e6e67ff..0000000
---- a/apps/client/dist-types/editor/editorApp.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorApp.d.ts","sourceRoot":"","sources":["../../src/editor/editorApp.ts"],"names":[],"mappings":"AAmGA,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAy6BhD"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/history/commandHistory.d.ts b/apps/client/dist-types/editor/history/commandHistory.d.ts
ddeleted file mode 100644
iindex c55339c..0000000
---- a/apps/client/dist-types/editor/history/commandHistory.d.ts
++++ /dev/null
@@@ -1,107 +0,0 @@
--/**
-- * Undo/redo as a stack of COMMANDS, replacing the old `UndoOp` union.
-- *
-- * The union had one variant per mutation shape (23 of them) and one giant
-- * `applyOp` switch that reached directly into editor closures. Every new
-- * feature added a variant and a branch, and every branch had to remember to
-- * rebuild meshes, re-ground props and fix up selection.
-- *
-- * A command instead owns its own execute/undo pair, so the history has no
-- * knowledge of what is being changed. Two rules make it dependable:
-- *
-- * - ONE user interaction produces ONE entry. A transaction groups the
-- * intermediate states of a drag or a numeric scrub into a single command.
-- * - Undo must be an exact inverse. `history.test.ts` asserts
-- * before → execute → undo deep-equals before for every command type.
-- */
--export interface EditorCommand<TDoc = unknown> {
-- /** Shown in the history panel; also used to coalesce scrubs. */
-- label: string;
-- execute(document: TDoc): void;
-- undo(document: TDoc): void;
-- /** Rough retained size, for the memory budget. Defaults to 1 KB. */
-- estimatedBytes?: number;
-- /**
-- * Optional: fold a newer command of the same kind into this one, so a
-- * continuous scrub stays a single entry. Return true if absorbed.
-- */
-- coalesceWith?(next: EditorCommand<TDoc>): boolean;
--}
--/** A command built from two plain snapshots — the common case. */
--export declare function snapshotCommand<TDoc, TState>(label: string, before: TState, after: TState, apply: (doc: TDoc, state: TState) => void, estimatedBytes?: number): EditorCommand<TDoc>;
--/** Several commands committed and undone as one logical step. */
--export declare function compositeCommand<TDoc>(label: string, parts: EditorCommand<TDoc>[]): EditorCommand<TDoc>;
--export interface HistoryState {
-- depth: number;
-- redo: number;
-- dirty: boolean;
-- label: string | null;
-- bytes: number;
--}
--export declare class CommandHistory<TDoc = unknown> {
-- private readonly doc;
-- private readonly maxBytes;
-- private readonly maxEntries;
-- private _done;
-- private _undone;
-- /** Open transaction; commands accumulate here until commit. */
-- private _txn;
-- /**
-- * The command that was on top when we last saved. Comparing identity — not
-- * stack depth — is what makes "undo back to the saved state clears dirty"
-- * work while "undo, then do something else" stays dirty.
-- */
-- private _savedTop;
-- private _listeners;
-- constructor(doc: TDoc, maxBytes?: number, maxEntries?: number);
-- get inTransaction(): boolean;
-- get depth(): number;
-- get redoDepth(): number;
-- state(): HistoryState;
-- isDirty(): boolean;
-- /** Mark the current state as saved; undoing back to it clears dirty. */
-- markSaved(): void;
-- /**
-- * Run a command and record it. Inside a transaction the command is executed
-- * immediately (so the viewport previews live) but only lands on the stack
-- * when the transaction commits.
-- */
-- execute(cmd: EditorCommand<TDoc>): void;
-- /** Record a command WITHOUT running it (the caller already applied it). */
-- /**
-- * Run a command and record it. This is how document commands are issued:
-- * one call, so a caller cannot apply a mutation and forget to make it
-- * undoable, or record one it never applied.
-- *
-- * `record` is the older half of the pair, for mutations that were applied
-- * by hand first; it exists only while the legacy paths are being retired.
-- */
-- apply(cmd: EditorCommand<TDoc>): void;
-- record(cmd: EditorCommand<TDoc>): void;
-- beginTransaction(label: string): void;
-- /** Close the transaction into ONE entry (or nothing, if it did nothing). */
-- commitTransaction(label?: string): void;
-- /** Roll the transaction back and record nothing (Escape mid-drag). */
-- cancelTransaction(): void;
-- undo(): EditorCommand<TDoc> | null;
-- redo(): EditorCommand<TDoc> | null;
-- /** Labels newest-first, for the history panel. */
-- labels(): {
-- label: string;
-- undone: boolean;
-- }[];
-- clear(): void;
-- /**
-- * The document was replaced wholesale (remote revision loaded). Past
-- * commands reference objects that may no longer exist, so the stack is
-- * dropped rather than left to corrupt the new document.
-- */
-- rebase(): void;
-- onChange(fn: (s: HistoryState) => void): () => void;
-- private _push;
-- private _bytes;
-- /** Drop the oldest entries once the budget is exceeded. */
-- private _trim;
-- private _emit;
--}
--//# sourceMappingURL=commandHistory.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/history/commandHistory.d.ts.map b/apps/client/dist-types/editor/history/commandHistory.d.ts.map
ddeleted file mode 100644
iindex 57fefcb..0000000
---- a/apps/client/dist-types/editor/history/commandHistory.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"commandHistory.d.ts","sourceRoot":"","sources":["../../../src/editor/history/commandHistory.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/history/commandHistory.test.d.ts b/apps/client/dist-types/editor/history/commandHistory.test.d.ts
ddeleted file mode 100644
iindex eb25a9e..0000000
---- a/apps/client/dist-types/editor/history/commandHistory.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=commandHistory.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/history/commandHistory.test.d.ts.map b/apps/client/dist-types/editor/history/commandHistory.test.d.ts.map
ddeleted file mode 100644
iindex d2bf733..0000000
---- a/apps/client/dist-types/editor/history/commandHistory.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"commandHistory.test.d.ts","sourceRoot":"","sources":["../../../src/editor/history/commandHistory.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/history/commands.d.ts b/apps/client/dist-types/editor/history/commands.d.ts
ddeleted file mode 100644
iindex 08baef0..0000000
---- a/apps/client/dist-types/editor/history/commands.d.ts
++++ /dev/null
@@@ -1,91 +0,0 @@
--/**
-- * The editor's commands, expressed against `EditorDocument`.
-- *
-- * These replace the `UndoOp` union and its 400-line `applyOp` switch. That
-- * design stored Babylon `Mesh` references and live object references in undo
-- * entries, so undoing a delete restored an object the meshes no longer knew
-- * about, and every new feature meant a new variant plus a new branch that had
-- * to remember to rebuild meshes, re-ground props and fix selection.
-- *
-- * A command here captures STABLE IDS and PLAIN VALUES — never a mesh, never a
-- * live reference, never an array index. Applying one mutates the document;
-- * views follow through the change feed on their own. Undo is therefore an
-- * exact inverse by construction rather than by remembering to write the
-- * mirror-image branch.
-- */
--import { compositeCommand, type EditorCommand } from './commandHistory.js';
--import type { EditorDocument, EditorObject, EditorObjectKind } from '../document/editorDocument.js';
--import type { EditorTransform } from '../viewport/transformMath.js';
--import type { MaskPatch } from '../materials/paintMask.js';
--export type DocCommand = EditorCommand<EditorDocument>;
--/** Create an object. Undo removes it; redo restores the same id and value. */
--export declare function addObject(kind: EditorObjectKind, object: EditorObject, label?: string): DocCommand;
--/**
-- * Delete an object. The full value is captured so undo restores it under the
-- * SAME id — which is what keeps a delete/undo cycle from orphaning history
-- * entries, locks or selection that referred to it.
-- */
--export declare function removeObject(doc: EditorDocument, id: string, label?: string): DocCommand | null;
--export declare function removeObjects(doc: EditorDocument, ids: readonly string[], label?: string): DocCommand | null;
--export declare function addObjects(entries: readonly {
-- kind: EditorObjectKind;
-- object: EditorObject;
--}[], label?: string): DocCommand | null;
--/**
-- * Set properties on one object. Both sides are captured as whole values
-- * because a partial patch cannot express "this key was absent before" —
-- * which is exactly what undoing "give it a scale" has to restore.
-- */
--export declare function setProperties(doc: EditorDocument, id: string, patch: Record<string, unknown>, label?: string): DocCommand | null;
--/** Swap an object's whole value. */
--export declare function replaceObject(doc: EditorDocument, id: string, after: EditorObject, label?: string): DocCommand | null;
--export declare function setPropertiesMany(doc: EditorDocument, ids: readonly string[], patch: Record<string, unknown>, label?: string): DocCommand | null;
--/**
-- * Move/rotate/scale a set of objects. ONE entry per gesture: a gizmo drag
-- * produces hundreds of intermediate poses and exactly one command.
-- *
-- * Coalescing folds a later transform of the SAME ids into this one, so a
-- * numeric scrub is a single entry too — keeping the original `before`, which
-- * is what makes one undo return to where the scrub started.
-- */
--export declare function transformObjects(ids: readonly string[], before: readonly EditorTransform[], after: readonly EditorTransform[], label?: string): DocCommand;
--/**
-- * A terrain sculpt stroke, stored as the CHANGED RECTANGLE of the
-- * heightfield rather than the whole field.
-- *
-- * A 512×512 terrain is a megabyte of Float32 per snapshot; storing two of
-- * those per brush dab exhausts any sane history budget in seconds. A dab
-- * touches a handful of samples, so that is what is kept.
-- */
--export interface HeightPatch {
-- /** Column/row of the rectangle's top-left sample. */
-- x: number;
-- y: number;
-- w: number;
-- h: number;
-- before: Float32Array;
-- after: Float32Array;
--}
--/** Compute the minimal changed rectangle between two height fields. */
--export declare function heightDelta(before: Float32Array, after: Float32Array, stride: number): HeightPatch | null;
--export declare function applyHeightPatch(target: Float32Array, stride: number, patch: HeightPatch, which: 'before' | 'after'): void;
--/**
-- * One terrain stroke. The heights live in the view's working buffer (decoded
-- * Float32) as well as the document (base64), so the command is given both a
-- * writer for the live buffer and the document id to re-encode.
-- */
--export declare function terrainSculpt(id: string, patch: HeightPatch, stride: number, write: (id: string, apply: (heights: Float32Array) => void) => void, label?: string): DocCommand;
--/** One paint stroke, as the mask's changed rectangle. */
--export declare function paintStroke(id: string, surfaceId: string, before: MaskPatch, after: MaskPatch, write: (id: string, surfaceId: string, patch: MaskPatch) => void, label?: string): DocCommand;
--export { compositeCommand };
--/**
-- * Turn an inspector edit into a command. Vector components arrive as
-- * `pos[1]`, and nested rule flags as `rules.pvp`, so both are written back
-- * into a whole-object replacement — which is also what makes undo restore an
-- * absent key rather than a zeroed one.
-- */
--export declare function buildPropertyCommand(doc: EditorDocument, ids: readonly string[], key: string, value: unknown): {
-- execute: (d: EditorDocument) => void;
-- undo: (d: EditorDocument) => void;
--};
--//# sourceMappingURL=commands.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/history/commands.d.ts.map b/apps/client/dist-types/editor/history/commands.d.ts.map
ddeleted file mode 100644
iindex 0f7a300..0000000
---- a/apps/client/dist-types/editor/history/commands.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../src/editor/history/commands.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/history/commands.test.d.ts b/apps/client/dist-types/editor/history/commands.test.d.ts
ddeleted file mode 100644
iindex ee837d6..0000000
---- a/apps/client/dist-types/editor/history/commands.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=commands.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/history/commands.test.d.ts.map b/apps/client/dist-types/editor/history/commands.test.d.ts.map
ddeleted file mode 100644
iindex 36211dc..0000000
---- a/apps/client/dist-types/editor/history/commands.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"commands.test.d.ts","sourceRoot":"","sources":["../../../src/editor/history/commands.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/input/actionRouter.d.ts b/apps/client/dist-types/editor/input/actionRouter.d.ts
ddeleted file mode 100644
iindex b338f41..0000000
---- a/apps/client/dist-types/editor/input/actionRouter.d.ts
++++ /dev/null
@@@ -1,54 +0,0 @@
--/**
-- * Keyboard actions, in one place.
-- *
-- * Every keyboard behaviour in the editor goes through here and therefore
-- * through the centralised bindings, which is what makes them all remappable
-- * and all visible in the settings panel. A shortcut wired directly to a
-- * `keydown` somewhere else is invisible to conflict detection and cannot be
-- * changed, so there are none.
-- *
-- * Two rules the previous handling got wrong:
-- * - Typing is not shortcuts. While focus is in a text field, a number field
-- * or a keybind capture, no action runs at all — otherwise typing "wasd"
-- * into a name flies the camera across the map.
-- * - The most specific binding wins. Ctrl+Shift+Z must beat Ctrl+Z rather
-- * than depending on the order the actions happen to be declared in.
-- */
--import { type Binding } from '../bindings.js';
--export interface ActionDescriptor {
-- id: string;
-- hold?: boolean;
--}
--export type ActionHandler = (id: string) => boolean;
--/** Right Shift is treated as Left: nobody binds them separately. */
--export declare const normalizeCode: (code: string) => string;
--/**
-- * True when the event came from somewhere text is being entered. A keybind
-- * capture marks itself with `data-capturing`, so pressing "W" to rebind
-- * forward does not also fly forward.
-- */
--export declare function isTypingTarget(target: EventTarget | null): boolean;
--/** Specificity: more modifiers = more specific, so redo beats undo. */
--export declare function specificity(b: Binding): number;
--export declare class ActionRouter {
-- private readonly actions;
-- private readonly bindingOf;
-- private readonly handler;
-- private readonly held;
-- constructor(actions: readonly ActionDescriptor[], bindingOf: (id: string) => Binding, handler: ActionHandler);
-- /** True while the key bound to `action` is down. */
-- holding(action: string): boolean;
-- /** Returns true when an action ran and the event should be consumed. */
-- keyDown(e: KeyboardEvent): boolean;
-- keyUp(e: KeyboardEvent): void;
-- /** The window losing focus must not leave a key stuck down forever. */
-- clear(): void;
-- /** Movement axes from the held bindings, for the flight integrator. */
-- movement(): {
-- x: number;
-- y: number;
-- z: number;
-- fast: boolean;
-- };
--}
--//# sourceMappingURL=actionRouter.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/input/actionRouter.d.ts.map b/apps/client/dist-types/editor/input/actionRouter.d.ts.map
ddeleted file mode 100644
iindex 94970b6..0000000
---- a/apps/client/dist-types/editor/input/actionRouter.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"actionRouter.d.ts","sourceRoot":"","sources":["../../../src/editor/input/actionRouter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAkB,KAAK,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;AAEnD,oEAAoE;AACpE,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAAsD,CAAA;AAEnG;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAMlE;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAE9C;AAED,qBAAa,YAAY;IAIrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;gBAGtB,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,EAClC,OAAO,EAAE,aAAa;IAGzC,oDAAoD;IACpD,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIhC,wEAAwE;IACxE,OAAO,CAAC,CAAC,EAAE,aAAa,GAAG,OAAO;IAelC,KAAK,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI;IAI7B,uEAAuE;IACvE,KAAK,IAAI,IAAI;IAIb,uEAAuE;IACvE,QAAQ,IAAI;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE;CAQ/D"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/input/actionRouter.test.d.ts b/apps/client/dist-types/editor/input/actionRouter.test.d.ts
ddeleted file mode 100644
iindex 730e64d..0000000
---- a/apps/client/dist-types/editor/input/actionRouter.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=actionRouter.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/input/actionRouter.test.d.ts.map b/apps/client/dist-types/editor/input/actionRouter.test.d.ts.map
ddeleted file mode 100644
iindex c38d3fa..0000000
---- a/apps/client/dist-types/editor/input/actionRouter.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"actionRouter.test.d.ts","sourceRoot":"","sources":["../../../src/editor/input/actionRouter.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/interaction/editorPicker.d.ts b/apps/client/dist-types/editor/interaction/editorPicker.d.ts
ddeleted file mode 100644
iindex 07a16bd..0000000
---- a/apps/client/dist-types/editor/interaction/editorPicker.d.ts
++++ /dev/null
@@@ -1,79 +0,0 @@
--/**
-- * The editor's ONE picking entry point.
-- *
-- * A selection gesture asks this once and gets a single front-most eligible
-- * hit. It replaces the old sequence of "pick ordinary meshes, then separately
-- * multiPick terrain, then prefer the smallest patch within 1.5m", which could
-- * return several objects for one click and made overlapping terrain
-- * unpredictable.
-- *
-- * Two Babylon traps are handled here so no caller can forget them:
-- *
-- * 1. `scene.pick(x, y, predicate)` SKIPS the built-in isPickable / isVisible
-- * / isEnabled checks when a predicate is supplied. Every predicate we
-- * build therefore re-tests them itself — a deleted (disabled) island used
-- * to stay invisibly selectable because of exactly this.
-- * 2. Gizmo geometry lives in a UtilityLayerRenderer scene, so it is not in
-- * the main scene's pick set at all; ownership of a gizmo gesture is
-- * decided by the InteractionController, never by ray distance.
-- */
--import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { EditorObjectKind } from '../selection/selectionManager.js';
--export interface EditorPick {
-- objectId: string;
-- kind: EditorObjectKind;
-- mesh: AbstractMesh;
-- point: Vector3;
-- normal: Vector3 | null;
-- faceId: number;
-- distance: number;
--}
--/** What a mesh in the viewport belongs to, or null for editor-only helpers. */
--export interface MeshOwner {
-- objectId: string;
-- kind: EditorObjectKind;
--}
--/** Resolves a Babylon mesh to the document object that owns it. */
--export type OwnerLookup = (mesh: AbstractMesh) => MeshOwner | null;
--export interface PickOptions {
-- /** Restrict to these kinds (e.g. the Terrain tool only wants terrain). */
-- kinds?: readonly EditorObjectKind[];
-- /** Include water/other normally-excluded surfaces when explicitly wanted. */
-- includeIds?: readonly string[];
--}
--/**
-- * Walk a mesh up its parent chain until something claims ownership, so an
-- * imported GLB's child meshes resolve to their owning editor object.
-- * Returns null for sky, clouds, brush cursor, ghosts, wires, overlays and
-- * peer avatars — anything no document object claims.
-- */
--export declare function resolveOwner(mesh: AbstractMesh, lookup: OwnerLookup): MeshOwner | null;
--/**
-- * Pick the winner from a set of ray hits. Pure, so the tie-break rules are
-- * testable without a scene.
-- *
-- * Nearest wins. For surfaces that are coincident within `epsilon` the object
-- * id breaks the tie, which makes the result deterministic instead of
-- * dependent on scene traversal order — the reason two stacked terrains used
-- * to select unpredictably.
-- */
--export declare function resolveFrontMost<T extends {
-- objectId: string;
-- distance: number;
--}>(hits: readonly T[], epsilon?: number): T | null;
--export declare class EditorPicker {
-- private readonly scene;
-- private readonly lookup;
-- constructor(scene: Scene, lookup: OwnerLookup);
-- /**
-- * One click → at most one object. `scene.pick` already returns the nearest
-- * hit, so this is a single traversal; `resolveFrontMost` only matters when
-- * a caller supplies pre-collected hits.
-- */
-- pick(x: number, y: number, opts?: PickOptions): EditorPick | null;
-- /** Convenience for the current cursor position. */
-- pickAtPointer(opts?: PickOptions): EditorPick | null;
--}
--//# sourceMappingURL=editorPicker.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/interaction/editorPicker.d.ts.map b/apps/client/dist-types/editor/interaction/editorPicker.d.ts.map
ddeleted file mode 100644
iindex c799f8f..0000000
---- a/apps/client/dist-types/editor/interaction/editorPicker.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorPicker.d.ts","sourceRoot":"","sources":["../../../src/editor/interaction/editorPicker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAA;AAC1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAA;AAExE,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,gBAAgB,CAAA;IACtB,IAAI,EAAE,YAAY,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,+EAA+E;AAC/E,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,gBAAgB,CAAA;CACvB;AAED,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,SAAS,GAAG,IAAI,CAAA;AAElE,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACnC,6EAA6E;IAC7E,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAQtF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAC/E,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,OAAO,SAAO,GACb,CAAC,GAAG,IAAI,CAYV;AAED,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,WAAW;IAGtC;;;;OAIG;IACH,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,UAAU,GAAG,IAAI;IA0BrE,mDAAmD;IACnD,aAAa,CAAC,IAAI,GAAE,WAAgB,GAAG,UAAU,GAAG,IAAI;CAGzD"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/interaction/editorPicker.test.d.ts b/apps/client/dist-types/editor/interaction/editorPicker.test.d.ts
ddeleted file mode 100644
iindex a6395ff..0000000
---- a/apps/client/dist-types/editor/interaction/editorPicker.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=editorPicker.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/interaction/editorPicker.test.d.ts.map b/apps/client/dist-types/editor/interaction/editorPicker.test.d.ts.map
ddeleted file mode 100644
iindex 2f8cdc9..0000000
---- a/apps/client/dist-types/editor/interaction/editorPicker.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorPicker.test.d.ts","sourceRoot":"","sources":["../../../src/editor/interaction/editorPicker.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/interaction/interactionController.d.ts b/apps/client/dist-types/editor/interaction/interactionController.d.ts
ddeleted file mode 100644
iindex fee02ff..0000000
---- a/apps/client/dist-types/editor/interaction/interactionController.d.ts
++++ /dev/null
@@@ -1,65 +0,0 @@
--/**
-- * One authoritative viewport interaction state machine.
-- *
-- * The rule this exists to enforce: ONE pointer gesture performs ONE editor
-- * action. A gizmo drag is not also a selection; a camera orbit is not also a
-- * selection; a sculpt stroke never becomes a click. Previously that was
-- * approximated with a `gizmoDragging` boolean, `gizmo.isHovered` and a 50ms
-- * timeout — three racing signals with no ownership guarantee.
-- *
-- * Here, a gesture is *claimed* on pointer-down and stays claimed until
-- * pointer-up. Everything else asks `canStartGesture()` / `is()` instead of
-- * guessing from stale hover flags.
-- */
--export type InteractionState = 'idle'
--/** Pointer is down over nothing special; may still become a click. */
-- | 'selection-click-candidate' | 'camera-orbit' | 'camera-pan' | 'camera-freelook' | 'gizmo-drag' | 'placement' | 'terrain-sculpt' | 'surface-paint' | 'face-edit';
--export interface GestureOrigin {
-- x: number;
-- y: number;
-- pointerId: number;
--}
--export declare class InteractionController {
-- private _state;
-- private _origin;
-- /** Largest pointer travel since the gesture began, in CSS pixels. */
-- private _travel;
-- private _listeners;
-- /** A click is only a click if the pointer barely moved. */
-- readonly clickSlop: number;
-- constructor(clickSlop?: number);
-- get state(): InteractionState;
-- get travel(): number;
-- get origin(): GestureOrigin | null;
-- is(...states: InteractionState[]): boolean;
-- /** True when the camera controller may consume this frame's input. */
-- cameraMayMove(): boolean;
-- /**
-- * True when free camera flight (WASD, wheel zoom) is permitted. Flight is
-- * blocked during any object-editing gesture so a transform can never be
-- * polluted by camera motion.
-- */
-- flightAllowed(): boolean;
-- /** True when scene picking / selection logic may run for this event. */
-- pickingAllowed(): boolean;
-- /** A new gesture may only start from idle (or from a bare click candidate). */
-- canStartGesture(): boolean;
-- /**
-- * Claim the pointer for `state`. Returns false when another gesture already
-- * owns it — the caller must then do nothing at all.
-- */
-- begin(state: InteractionState, origin: GestureOrigin): boolean;
-- /** Feed pointer motion so click-vs-drag can be decided on release. */
-- move(x: number, y: number): void;
-- /**
-- * End the gesture. Returns true when it should be treated as a CLICK —
-- * i.e. it was a click candidate and the pointer never meaningfully moved.
-- * A drag never produces a selection.
-- */
-- end(): boolean;
-- /** Abandon the gesture without it counting as a click (Escape, lost lock). */
-- cancel(): void;
-- onChange(fn: (s: InteractionState) => void): () => void;
-- private _set;
--}
--//# sourceMappingURL=interactionController.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/interaction/interactionController.d.ts.map b/apps/client/dist-types/editor/interaction/interactionController.d.ts.map
ddeleted file mode 100644
iindex b3d33b0..0000000
---- a/apps/client/dist-types/editor/interaction/interactionController.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"interactionController.d.ts","sourceRoot":"","sources":["../../../src/editor/interaction/interactionController.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,gBAAgB,GACxB,MAAM;AACR,sEAAsE;GACpE,2BAA2B,GAC3B,cAAc,GACd,YAAY,GACZ,iBAAiB,GACjB,YAAY,GACZ,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,WAAW,CAAA;AAcf,MAAM,WAAW,aAAa;IAC5B,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,OAAO,CAA6B;IAC5C,qEAAqE;IACrE,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,UAAU,CAA2C;IAC7D,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;gBAEd,SAAS,SAAI;IAIzB,IAAI,KAAK,IAAI,gBAAgB,CAE5B;IACD,IAAI,MAAM,IAAI,MAAM,CAEnB;IACD,IAAI,MAAM,IAAI,aAAa,GAAG,IAAI,CAEjC;IAED,EAAE,CAAC,GAAG,MAAM,EAAE,gBAAgB,EAAE,GAAG,OAAO;IAI1C,sEAAsE;IACtE,aAAa,IAAI,OAAO;IAIxB;;;;OAIG;IACH,aAAa,IAAI,OAAO;IAQxB,wEAAwE;IACxE,cAAc,IAAI,OAAO;IAIzB,+EAA+E;IAC/E,eAAe,IAAI,OAAO;IAI1B;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO;IAQ9D,sEAAsE;IACtE,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI;IAMhC;;;;OAIG;IACH,GAAG,IAAI,OAAO;IAQd,8EAA8E;IAC9E,MAAM,IAAI,IAAI;IAMd,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKvD,OAAO,CAAC,IAAI;CAKb"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/interaction/interactionController.test.d.ts b/apps/client/dist-types/editor/interaction/interactionController.test.d.ts
ddeleted file mode 100644
iindex 0e03c9f..0000000
---- a/apps/client/dist-types/editor/interaction/interactionController.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=interactionController.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/interaction/interactionController.test.d.ts.map b/apps/client/dist-types/editor/interaction/interactionController.test.d.ts.map
ddeleted file mode 100644
iindex eb400a6..0000000
---- a/apps/client/dist-types/editor/interaction/interactionController.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"interactionController.test.d.ts","sourceRoot":"","sources":["../../../src/editor/interaction/interactionController.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/interaction/viewportInteraction.d.ts b/apps/client/dist-types/editor/interaction/viewportInteraction.d.ts
ddeleted file mode 100644
iindex 582bdfc..0000000
---- a/apps/client/dist-types/editor/interaction/viewportInteraction.d.ts
++++ /dev/null
@@@ -1,69 +0,0 @@
--/**
-- * What a press on the canvas means, given the active tool.
-- *
-- * One place decides: select, place, sculpt, paint or face-pick. Every branch
-- * asks `InteractionController` first, so a gesture the gizmo or the camera
-- * already owns can never also do something here — which is the whole
-- * click-through story, expressed once rather than as a hover flag per
-- * handler.
-- */
--import { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { Tool } from '../catalog.js';
--import type { InteractionController } from './interactionController.js';
--export type SelectMode = 'replace' | 'add' | 'subtract';
--export declare const selectModeOf: (e: {
-- ctrlKey: boolean;
-- metaKey: boolean;
-- altKey: boolean;
--}) => SelectMode;
--export interface ViewportHandlers {
-- /** Something was clicked (or empty space, when `id` is null). */
-- onSelect: (id: string | null, mode: SelectMode) => void;
-- onPlace: () => void;
-- /** Terrain stroke; sign is +1 for raise, -1 for lower. */
-- onSculptStart: (sign: number) => void;
-- onSculptMove: () => void;
-- onStrokeEnd: () => void;
-- onPaintStart: () => void;
-- onPaintMove: () => void;
-- onFacePick: (mode: SelectMode) => void;
-- /** Light and Zone both create one object at the clicked point. */
-- onPlaceLight: () => void;
-- onPlaceZone: () => void;
-- /** Frame update: hover highlight, brush cursor, placement preview. */
-- onFrame: () => void;
--}
--export interface ViewportInteractionOptions {
-- canvas: HTMLCanvasElement;
-- scene: Scene;
-- interaction: InteractionController;
-- tool: () => Tool | null;
-- handlers: ViewportHandlers;
-- /** Resolve a picked mesh to a document id. */
-- ownerOf: (mesh: AbstractMesh | null) => string | null;
-- /** Meshes the picker may hit (the ghost and helpers are excluded). */
-- pickable: (mesh: AbstractMesh) => boolean;
--}
--export declare class ViewportInteraction {
-- private readonly opts;
-- /** 0 none, 1 primary (raise/paint), -1 secondary (lower). */
-- private stroke;
-- private readonly disposers;
-- constructor(opts: ViewportInteractionOptions);
-- private on;
-- dispose(): void;
-- get painting(): number;
-- /** What the editor's own picker resolves at a screen point. */
-- pickIdAt(x: number, y: number): string | null;
-- pickPoint(): {
-- id: string | null;
-- point: Vector3;
-- normal: Vector3;
-- } | null;
-- private onDown;
-- private onMove;
-- private onUp;
--}
--//# sourceMappingURL=viewportInteraction.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/interaction/viewportInteraction.d.ts.map b/apps/client/dist-types/editor/interaction/viewportInteraction.d.ts.map
ddeleted file mode 100644
iindex 07a08cc..0000000
---- a/apps/client/dist-types/editor/interaction/viewportInteraction.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"viewportInteraction.d.ts","sourceRoot":"","sources":["../../../src/editor/interaction/viewportInteraction.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAA;AAC1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AAEvE,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,KAAK,GAAG,UAAU,CAAA;AAEvD,eAAO,MAAM,YAAY,GAAI,GAAG;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;CAChB,KAAG,UAAkF,CAAA;AAEtF,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,UAAU,KAAK,IAAI,CAAA;IACvD,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,0DAA0D;IAC1D,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,UAAU,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAA;IACtC,kEAAkE;IAClE,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,sEAAsE;IACtE,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,iBAAiB,CAAA;IACzB,KAAK,EAAE,KAAK,CAAA;IACZ,WAAW,EAAE,qBAAqB,CAAA;IAClC,IAAI,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,8CAA8C;IAC9C,OAAO,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,KAAK,MAAM,GAAG,IAAI,CAAA;IACrD,sEAAsE;IACtE,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAA;CAC1C;AAED,qBAAa,mBAAmB;IAKlB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,6DAA6D;IAC7D,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;gBAElB,IAAI,EAAE,0BAA0B;IAU7D,OAAO,CAAC,EAAE;IAKV,OAAO,IAAI,IAAI;IAKf,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,+DAA+D;IAC/D,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAK7C,SAAS,IAAI;QAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IAW1E,OAAO,CAAC,MAAM;IA2Cd,OAAO,CAAC,MAAM;IAOd,OAAO,CAAC,IAAI;CAKb"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/main.d.ts b/apps/client/dist-types/editor/main.d.ts
ddeleted file mode 100644
iindex d42b5b9..0000000
---- a/apps/client/dist-types/editor/main.d.ts
++++ /dev/null
@@@ -1,9 +0,0 @@
--/**
-- * Editor entry point.
-- *
-- * Boot only. Everything the editor does lives in the modules `editorApp.ts`
-- * composes — this file exists to load the stylesheet, start the app, and put
-- * a readable message on screen if it cannot.
-- */
--import './editor.css';
--//# sourceMappingURL=main.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/main.d.ts.map b/apps/client/dist-types/editor/main.d.ts.map
ddeleted file mode 100644
iindex 58eba1d..0000000
---- a/apps/client/dist-types/editor/main.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/editor/main.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,cAAc,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/materials/paintController.d.ts b/apps/client/dist-types/editor/materials/paintController.d.ts
ddeleted file mode 100644
iindex d7c140a..0000000
---- a/apps/client/dist-types/editor/materials/paintController.d.ts
++++ /dev/null
@@@ -1,63 +0,0 @@
--import type { Scene } from '@babylonjs/core/scene.js';
--import { type SurfaceMaterialData } from '@openvibe/content';
--import type { EditorDocument } from '../document/editorDocument.js';
--import type { EditorViewRegistry } from '../viewport/editorViewRegistry.js';
--import { type PaintUV, type ProjectionMode } from './paintableSurface.js';
--import type { PaintSurfaceRegistry, PaintSurfaceState } from './paintSurfaceRegistry.js';
--export interface PaintHit {
-- ownerId: string;
-- surfaceId: string;
-- projection: ProjectionMode;
-- uv: PaintUV;
-- /** Live mask state for this exact surface. */
-- state: PaintSurfaceState;
-- /** The authored surface data as the document currently holds it. */
-- data: SurfaceMaterialData;
--}
--export interface PaintControllerOptions {
-- scene: Scene;
-- doc: EditorDocument;
-- views: EditorViewRegistry;
-- registry: PaintSurfaceRegistry;
-- newId: (prefix: string) => string;
--}
--/**
-- * The surface data for (owner, surface), defaulted so that painting a
-- * never-painted object PRESERVES how it already looks.
-- *
-- * A legacy static carries `color` / `tex` / `uv` / `faces`; a legacy terrain
-- * carries `tex` / `color`. Those become the BASE of the v2 surface on first
-- * paint, so the first brush stroke adds coverage on top of the existing
-- * appearance instead of resetting the object to grey.
-- */
--export declare function surfaceDataFor(doc: EditorDocument, ownerId: string, surfaceId: string): SurfaceMaterialData;
--/** Write surface data back to the right place for the owner's kind. */
--export declare function writeSurfaceData(doc: EditorDocument, ownerId: string, surfaceId: string, data: SurfaceMaterialData): void;
--export declare class PaintController {
-- private readonly opts;
-- constructor(opts: PaintControllerOptions);
-- /**
-- * Resolve whatever is under the cursor into a paint target, or null.
-- *
-- * The owning object comes from the view registry (stable document id), NOT
-- * from a Babylon mesh name — instance names vary between loads, so identity
-- * inferred from one would not survive a reload.
-- */
-- hitTest(brushRadius: number): PaintHit | null;
-- private terrainHit;
-- private staticHit;
-- /**
-- * Allocate (or reuse) the layer the brush paints into, writing the result
-- * to the document so a new layer is itself part of the map.
-- *
-- * Returns null at the layer budget rather than swapping a texture out —
-- * silently replacing someone's paint is the behaviour the layered surface
-- * model was built to end.
-- */
-- ensureLayer(hit: PaintHit, tex: string, tint: string | undefined): {
-- channel: string;
-- data: SurfaceMaterialData;
-- } | null;
-- get layerBudget(): number;
--}
--//# sourceMappingURL=paintController.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/materials/paintController.d.ts.map b/apps/client/dist-types/editor/materials/paintController.d.ts.map
ddeleted file mode 100644
iindex 4a2a5f4..0000000
---- a/apps/client/dist-types/editor/materials/paintController.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"paintController.d.ts","sourceRoot":"","sources":["../../../src/editor/materials/paintController.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAGL,KAAK,mBAAmB,EAGzB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA;AAE3E,OAAO,EAYL,KAAK,OAAO,EACZ,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAExF,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,cAAc,CAAA;IAC1B,EAAE,EAAE,OAAO,CAAA;IACX,8CAA8C;IAC9C,KAAK,EAAE,iBAAiB,CAAA;IACxB,oEAAoE;IACpE,IAAI,EAAE,mBAAmB,CAAA;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,KAAK,CAAA;IACZ,GAAG,EAAE,cAAc,CAAA;IACnB,KAAK,EAAE,kBAAkB,CAAA;IACzB,QAAQ,EAAE,oBAAoB,CAAA;IAC9B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,mBAAmB,CAmCrB;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,mBAAmB,GACxB,IAAI,CAeN;AAED,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,sBAAsB;IAEzD;;;;;;OAMG;IACH,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAwC7C,OAAO,CAAC,UAAU;IAmBlB,OAAO,CAAC,SAAS;IAoFjB;;;;;;;OAOG;IACH,WAAW,CACT,GAAG,EAAE,QAAQ,EACb,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,mBAAmB,CAAA;KAAE,GAAG,IAAI;IASxD,IAAI,WAAW,IAAI,MAAM,CAExB;CACF"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/materials/paintMask.d.ts b/apps/client/dist-types/editor/materials/paintMask.d.ts
ddeleted file mode 100644
iindex e0cf592..0000000
---- a/apps/client/dist-types/editor/materials/paintMask.d.ts
++++ /dev/null
@@@ -1,67 +0,0 @@
--/**
-- * The paintable RGBA coverage mask behind a layered surface.
-- *
-- * Each of the four channels is one paint layer's coverage, so a brush stroke
-- * writes to a single channel and cannot disturb the others — or the base
-- * texture, which lives outside the mask entirely.
-- *
-- * Undo stores the changed RECTANGLE, not a whole-canvas snapshot: a small
-- * stroke on a 512² mask costs a few KB instead of a megabyte, which is what
-- * keeps the history memory budget realistic for painting.
-- */
--import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import { type PaintChannel } from '@openvibe/content';
--export declare const MASK_SIZE = 512;
--export interface MaskPatch {
-- x: number;
-- y: number;
-- w: number;
-- h: number;
-- data: Uint8ClampedArray;
--}
--export interface BrushStamp {
-- /** Mask-space centre and radius, in pixels. */
-- u: number;
-- v: number;
-- radius: number;
-- /** 0..1 coverage added (or removed when erasing) at the brush core. */
-- strength: number;
-- /** Falloff exponent; higher = harder edge. */
-- feather: number;
-- erase: boolean;
--}
--export declare class PaintMask {
-- readonly size: number;
-- readonly texture: DynamicTexture;
-- private readonly ctx;
-- /** Pending stroke bounds, accumulated so one stroke = one undo entry. */
-- private strokeBox;
-- private strokeBefore;
-- constructor(scene: Scene, name: string, size?: number);
-- /** Replace the mask contents from a loaded image. */
-- drawImage(img: CanvasImageSource): void;
-- toDataURL(): string;
-- toBlob(): Promise<Blob | null>;
-- beginStroke(): void;
-- /**
-- * Stamp the brush into one channel. Channels are edited independently, so
-- * painting layer B never erases layer A and the base is never touched.
-- */
-- stamp(channel: PaintChannel, s: BrushStamp): void;
-- /**
-- * Close the stroke and return the before/after patches for one history
-- * entry, or null when the stroke changed nothing.
-- */
-- endStroke(): {
-- before: MaskPatch;
-- after: MaskPatch;
-- } | null;
-- /** Apply a stored patch (undo/redo). */
-- applyPatch(patch: MaskPatch): void;
-- /** Zero one channel — "clear layer mask" in the layer manager. */
-- clearChannel(channel: PaintChannel): void;
-- dispose(): void;
--}
--export declare const patchBytes: (p: MaskPatch) => number;
--//# sourceMappingURL=paintMask.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/materials/paintMask.d.ts.map b/apps/client/dist-types/editor/materials/paintMask.d.ts.map
ddeleted file mode 100644
iindex 9a999e7..0000000
---- a/apps/client/dist-types/editor/materials/paintMask.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"paintMask.d.ts","sourceRoot":"","sources":["../../../src/editor/materials/paintMask.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,sDAAsD,CAAA;AACrF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErE,eAAO,MAAM,SAAS,MAAM,CAAA;AAE5B,MAAM,WAAW,SAAS;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,IAAI,EAAE,iBAAiB,CAAA;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,+CAA+C;IAC/C,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,MAAM,EAAE,MAAM,CAAA;IACd,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAA;IAChB,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,OAAO,CAAA;CACf;AAED,qBAAa,SAAS;IAUlB,QAAQ,CAAC,IAAI;IATf,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA0B;IAC9C,yEAAyE;IACzE,OAAO,CAAC,SAAS,CAAkE;IACnF,OAAO,CAAC,YAAY,CAAyB;gBAG3C,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,MAAM,EACH,IAAI,SAAY;IAS3B,qDAAqD;IACrD,SAAS,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI;IAMvC,SAAS,IAAI,MAAM;IAIb,MAAM,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAKpC,WAAW,IAAI,IAAI;IAKnB;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,YAAY,EAAE,CAAC,EAAE,UAAU,GAAG,IAAI;IA6CjD;;;OAGG;IACH,SAAS,IAAI;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,GAAG,IAAI;IAoC3D,wCAAwC;IACxC,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI;IAOlC,kEAAkE;IAClE,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IASzC,OAAO,IAAI,IAAI;CAGhB;AAED,eAAO,MAAM,UAAU,GAAI,GAAG,SAAS,KAAG,MAAuB,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/materials/paintSurfaceRegistry.d.ts b/apps/client/dist-types/editor/materials/paintSurfaceRegistry.d.ts
ddeleted file mode 100644
iindex bb3f0ec..0000000
---- a/apps/client/dist-types/editor/materials/paintSurfaceRegistry.d.ts
++++ /dev/null
@@@ -1,61 +0,0 @@
--/**
-- * Live paint state, keyed by (object id, surface id).
-- *
-- * Terrain used to keep its one mask on `TerrainView`, and the whole paint
-- * path was written against that: pick a terrain, reach into the view, stamp
-- * its mask. Nothing else could be painted because nothing else had one.
-- *
-- * A box has six independently paintable faces, a cylinder and a sphere have
-- * their own wraps, and an imported model has a slot per material — so a mask
-- * belongs to a SURFACE, not to a view, and the pair (owner, surface) is the
-- * key. That pair is also what history and the save path address, so a stroke
-- * is undoable and persistable without anything having to know which kind of
-- * object it landed on.
-- *
-- * The registry holds runtime state only. The authored data — layers, tints,
-- * the mask's URL — lives in the document, as always.
-- */
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { SurfaceMaterialData } from '@openvibe/content';
--import { PaintMask } from './paintMask.js';
--/** A surface's live mask plus whether it needs uploading. */
--export interface PaintSurfaceState {
-- ownerId: string;
-- surfaceId: string;
-- mask: PaintMask;
-- /**
-- * True once a stroke has changed the mask since the persisted reference
-- * was written. Save uploads only these, so an unchanged mask costs no
-- * HTTP request at all — server-side dedupe would still cost the round
-- * trip and the PNG encode.
-- */
-- dirty: boolean;
-- /** The mask URL the document currently holds, if any. */
-- persistedRef: string | null;
--}
--export declare const surfaceKey: (ownerId: string, surfaceId: string) => string;
--export declare class PaintSurfaceRegistry {
-- private readonly scene;
-- private readonly surfaces;
-- constructor(scene: Scene);
-- get size(): number;
-- /** Every live surface, for the save path. */
-- all(): PaintSurfaceState[];
-- peek(ownerId: string, surfaceId: string): PaintSurfaceState | null;
-- /**
-- * The mask for a surface, created on first use and seeded from whatever
-- * the document already persisted. Lazy because a map with a thousand
-- * statics must not allocate a thousand 512² canvases to open.
-- */
-- ensure(ownerId: string, surfaceId: string, data: SurfaceMaterialData): PaintSurfaceState;
-- /** A stroke finished: this surface now differs from what was persisted. */
-- markDirty(ownerId: string, surfaceId: string): void;
-- /** The save path wrote this URL; the mask matches it again. */
-- markPersisted(ownerId: string, surfaceId: string, ref: string): void;
-- /** Surfaces whose mask has changed since it was last written. */
-- dirtySurfaces(): PaintSurfaceState[];
-- /** Drop everything belonging to an object whose view went away. */
-- disposeOwner(ownerId: string): void;
-- disposeAll(): void;
--}
--//# sourceMappingURL=paintSurfaceRegistry.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/materials/paintSurfaceRegistry.d.ts.map b/apps/client/dist-types/editor/materials/paintSurfaceRegistry.d.ts.map
ddeleted file mode 100644
iindex b3a7543..0000000
---- a/apps/client/dist-types/editor/materials/paintSurfaceRegistry.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"paintSurfaceRegistry.d.ts","sourceRoot":"","sources":["../../../src/editor/materials/paintSurfaceRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,SAAS,CAAA;IACf;;;;;OAKG;IACH,KAAK,EAAE,OAAO,CAAA;IACd,yDAAyD;IACzD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,WAAW,MAAM,KAAG,MAAmC,CAAA;AAEnG,qBAAa,oBAAoB;IAGnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAFlC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;gBAEnC,KAAK,EAAE,KAAK;IAEzC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,6CAA6C;IAC7C,GAAG,IAAI,iBAAiB,EAAE;IAI1B,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAIlE;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,iBAAiB;IA0BxF,2EAA2E;IAC3E,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKnD,+DAA+D;IAC/D,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAOpE,iEAAiE;IACjE,aAAa,IAAI,iBAAiB,EAAE;IAIpC,mEAAmE;IACnE,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQnC,UAAU,IAAI,IAAI;CAInB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/materials/paintableSurface.d.ts b/apps/client/dist-types/editor/materials/paintableSurface.d.ts
ddeleted file mode 100644
iindex 5e2374b..0000000
---- a/apps/client/dist-types/editor/materials/paintableSurface.d.ts
++++ /dev/null
@@@ -1,74 +0,0 @@
--/**
-- * Painting, generalised past terrain.
-- *
-- * Terrain paint worked because a heightfield has one obvious UV
-- * parameterisation: local metres map linearly to the mask. A box has six
-- * faces that should be paintable independently, a cylinder wraps, a sphere
-- * has poles, and an imported GLB may have no usable UVs at all. Rather than
-- * a branch per case in the brush, each paintable thing answers the same two
-- * questions:
-- *
-- * - which surface am I? (a stable id, so the paint can be saved)
-- * - where in my mask is this world-space point?
-- *
-- * The DOCUMENT holds the surface data. A `PaintableSurface` is a view onto
-- * it, exactly like the meshes are.
-- */
--import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js';
--import type { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { SurfaceMaterialData } from '@openvibe/content';
--import type { PaintMask } from './paintMask.js';
--/** Where a brush stamp lands, in mask pixels. */
--export interface PaintUV {
-- u: number;
-- v: number;
-- /** Brush radius converted from world metres into mask pixels. */
-- radiusPixels: number;
--}
--/**
-- * How a surface's UVs were derived. Shown in the Inspector and used by the
-- * Issues panel, because "the paint went somewhere unexpected" is otherwise
-- * indistinguishable from "the paint did nothing".
-- */
--export type ProjectionMode = 'planar' | 'face' | 'cylindrical' | 'spherical' | 'uv0' | 'box';
--export interface PaintableSurface {
-- /** Document object that owns this surface. */
-- readonly ownerId: string;
-- /**
-- * Stable surface id within the owner: `surface` for a whole object,
-- * `face:0`..`face:5` for box faces, `mesh:<node path>/material:<slot>`
-- * for an imported model's slots.
-- */
-- readonly surfaceId: string;
-- readonly mesh: AbstractMesh;
-- readonly projection: ProjectionMode;
-- /** The authoritative data — read from the document, never cached here. */
-- getMaterialData(): SurfaceMaterialData;
-- /** The live mask being painted into. */
-- getMask(): PaintMask;
-- /**
-- * World-space pick → mask pixels, or null when this point is not on this
-- * surface (a pick on face 3 while face 1 is the active surface).
-- */
-- mapPickToPaintUV(worldPoint: Vector3, worldNormal: Vector3, brushRadius: number): PaintUV | null;
--}
--export declare const WHOLE_SURFACE = "surface";
--export declare const faceSurfaceId: (index: number) => string;
--export declare const modelSurfaceId: (nodePath: string, slot: number) => string;
--export declare function parseSurfaceId(id: string): {
-- kind: 'whole';
--} | {
-- kind: 'face';
-- index: number;
--} | {
-- kind: 'model';
-- path: string;
-- slot: number;
--} | null;
--export { boxProjectionUV, cylindricalUV, faceIndexFromNormal, faceUV, hasUsableUV0, planarUV, sphericalUV, staticExtent, } from '../../render/surfaceProjection.js';
--/** Convert a UV in 0..1 plus a world radius into mask pixels. */
--export declare function toPaintUV(uv: {
-- u: number;
-- v: number;
--}, maskSize: number, worldRadius: number, worldExtent: number): PaintUV;
--//# sourceMappingURL=paintableSurface.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/materials/paintableSurface.d.ts.map b/apps/client/dist-types/editor/materials/paintableSurface.d.ts.map
ddeleted file mode 100644
iindex ebd17f1..0000000
---- a/apps/client/dist-types/editor/materials/paintableSurface.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"paintableSurface.d.ts","sourceRoot":"","sources":["../../../src/editor/materials/paintableSurface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAA;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AACnE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C,iDAAiD;AACjD,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,aAAa,GAAG,WAAW,GAAG,KAAK,GAAG,KAAK,CAAA;AAE5F,MAAM,WAAW,gBAAgB;IAC/B,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAA;IACnC,0EAA0E;IAC1E,eAAe,IAAI,mBAAmB,CAAA;IACtC,wCAAwC;IACxC,OAAO,IAAI,SAAS,CAAA;IACpB;;;OAGG;IACH,gBAAgB,CAAC,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;CACjG;AAID,eAAO,MAAM,aAAa,YAAY,CAAA;AACtC,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAAyB,CAAA;AACvE,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,KAAG,MAC3B,CAAA;AAIrC,wBAAgB,cAAc,CAC5B,EAAE,EAAE,MAAM,GAER;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC7C,IAAI,CAOP;AAOD,OAAO,EACL,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,YAAY,GACb,MAAM,mCAAmC,CAAA;AAE1C,iEAAiE;AACjE,wBAAgB,SAAS,CACvB,EAAE,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,OAAO,CAMT"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/materials/paintableSurface.test.d.ts b/apps/client/dist-types/editor/materials/paintableSurface.test.d.ts
ddeleted file mode 100644
iindex 6680ebd..0000000
---- a/apps/client/dist-types/editor/materials/paintableSurface.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=paintableSurface.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/materials/paintableSurface.test.d.ts.map b/apps/client/dist-types/editor/materials/paintableSurface.test.d.ts.map
ddeleted file mode 100644
iindex 7fc1361..0000000
---- a/apps/client/dist-types/editor/materials/paintableSurface.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"paintableSurface.test.d.ts","sourceRoot":"","sources":["../../../src/editor/materials/paintableSurface.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/net/editorToken.d.ts b/apps/client/dist-types/editor/net/editorToken.d.ts
ddeleted file mode 100644
iindex a7f26b3..0000000
---- a/apps/client/dist-types/editor/net/editorToken.d.ts
++++ /dev/null
@@@ -1,10 +0,0 @@
--/**
-- * Editor credential resolution. An explicitly typed admin token always wins
-- * (the EDITOR_KEY shared-secret path still works without SSO); otherwise the
-- * OpenVibe SSO session from the game (`ovg_sso`, same origin as /play) is
-- * used, so admins and owners never have to paste anything — the server
-- * validates the token against OpenVibe and checks admin/owner rank.
-- */
--export declare function ssoToken(): string;
--export declare function editorToken(): string;
--//# sourceMappingURL=editorToken.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/net/editorToken.d.ts.map b/apps/client/dist-types/editor/net/editorToken.d.ts.map
ddeleted file mode 100644
iindex d2f4a2a..0000000
---- a/apps/client/dist-types/editor/net/editorToken.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorToken.d.ts","sourceRoot":"","sources":["../../../src/editor/net/editorToken.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,wBAAgB,QAAQ,IAAI,MAAM,CAKjC;AAED,wBAAgB,WAAW,IAAI,MAAM,CAIpC"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/net/saveController.d.ts b/apps/client/dist-types/editor/net/saveController.d.ts
ddeleted file mode 100644
iindex 41b6c11..0000000
---- a/apps/client/dist-types/editor/net/saveController.d.ts
++++ /dev/null
@@@ -1,43 +0,0 @@
--/**
-- * Everything between the document and the server: save, revision conflicts,
-- * remote adoption, drafts and asset uploads.
-- *
-- * Kept out of the composition root because it is the one part with real
-- * sequencing — upload masks, then serialise, then If-Match, then interpret
-- * the status — and because "did this overwrite someone else's work?" is
-- * worth being able to read in one place.
-- */
--import { canonicalizeMapFile, type MapFileV2 } from '@openvibe/content';
--import type { EditorDocument } from '../document/editorDocument.js';
--import type { CommandHistory } from '../history/commandHistory.js';
--import type { PaintSurfaceRegistry } from '../materials/paintSurfaceRegistry.js';
--export interface SaveControllerOptions {
-- doc: EditorDocument;
-- history: CommandHistory<EditorDocument>;
-- bootRevision: string;
-- /** Adopt a whole remote/imported/restored document. */
-- onAdopt: (map: MapFileV2) => void;
-- /** Every live painted surface, keyed by (object, surface). */
-- paintSurfaces: PaintSurfaceRegistry;
-- setMessage: (m: string) => void;
--}
--export interface SaveController {
-- save: () => Promise<void>;
-- maskUploadCount: () => number;
-- pollRemote: () => Promise<void>;
-- isDirty: () => boolean;
-- revision: () => string;
-- serialize: () => MapFileV2;
-- exportMap: () => void;
-- importMap: (file: File) => Promise<void>;
-- conflict: () => {
-- open: boolean;
-- summary: string[];
-- };
-- resolveConflict: (choice: 'theirs' | 'mine' | 'export') => void;
-- restoreDraftIfAny: () => Promise<void>;
-- onChange: (fn: () => void) => void;
--}
--export declare function createSaveController(opts: SaveControllerOptions): SaveController;
--export { canonicalizeMapFile };
--//# sourceMappingURL=saveController.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/net/saveController.d.ts.map b/apps/client/dist-types/editor/net/saveController.d.ts.map
ddeleted file mode 100644
iindex d646a2a..0000000
---- a/apps/client/dist-types/editor/net/saveController.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"saveController.d.ts","sourceRoot":"","sources":["../../../src/editor/net/saveController.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,mBAAmB,EAKnB,KAAK,SAAS,EACf,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAClE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAA;AAKhF,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,cAAc,CAAA;IACnB,OAAO,EAAE,cAAc,CAAC,cAAc,CAAC,CAAA;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,uDAAuD;IACvD,OAAO,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,IAAI,CAAA;IACjC,8DAA8D;IAC9D,aAAa,EAAE,oBAAoB,CAAA;IACnC,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,eAAe,EAAE,MAAM,MAAM,CAAA;IAC7B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,OAAO,EAAE,MAAM,OAAO,CAAA;IACtB,QAAQ,EAAE,MAAM,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,SAAS,CAAA;IAC1B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,QAAQ,EAAE,MAAM;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IACpD,eAAe,EAAE,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAA;IAC/D,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;CACnC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,GAAG,cAAc,CA0RhF;AAED,OAAO,EAAE,mBAAmB,EAAE,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/perf/editorProfiler.d.ts b/apps/client/dist-types/editor/perf/editorProfiler.d.ts
ddeleted file mode 100644
iindex 00e29bf..0000000
---- a/apps/client/dist-types/editor/perf/editorProfiler.d.ts
++++ /dev/null
@@@ -1,38 +0,0 @@
--/**
-- * Counters and timers for the editor's hot paths.
-- *
-- * The point is that performance claims can be checked instead of asserted.
-- * Every optimisation in the editor is justified by a number this produces on
-- * a large map, and the same numbers are asserted in the E2E profile check so
-- * a regression fails a gate rather than being noticed a year later.
-- *
-- * Counting must be cheaper than what it measures: a `Map` lookup and an add.
-- * `time()` uses `performance.now()`, so it is only used around work that is
-- * already at least microseconds (a serialize, a full rebuild) — never per
-- * mesh.
-- */
--export interface PerfSample {
-- count: number;
-- /** Total milliseconds, for timed sections only. */
-- ms: number;
--}
--export declare class EditorProfiler {
-- private readonly samples;
-- /** Off by default: an unobserved counter should cost nothing. */
-- private on;
-- enabled(): boolean;
-- setEnabled(on: boolean): void;
-- count(name: string, n?: number): void;
-- /** Time a section AND count it. Returns whatever the body returns. */
-- time<T>(name: string, body: () => T): T;
-- snapshot(): Record<string, PerfSample>;
-- reset(): void;
-- private entry;
--}
--/**
-- * One profiler per page. A module-level singleton because the things worth
-- * measuring (view churn, document lookups, picking) are spread across layers
-- * that must not grow a constructor argument each to carry it.
-- */
--export declare const editorPerf: EditorProfiler;
--//# sourceMappingURL=editorProfiler.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/perf/editorProfiler.d.ts.map b/apps/client/dist-types/editor/perf/editorProfiler.d.ts.map
ddeleted file mode 100644
iindex 6b6d6f8..0000000
---- a/apps/client/dist-types/editor/perf/editorProfiler.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorProfiler.d.ts","sourceRoot":"","sources":["../../../src/editor/perf/editorProfiler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAA;CACX;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,iEAAiE;IACjE,OAAO,CAAC,EAAE,CAAQ;IAElB,OAAO,IAAI,OAAO;IAIlB,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;IAI7B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,SAAI,GAAG,IAAI;IAKhC,sEAAsE;IACtE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC;IAYvC,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC;IAMtC,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,KAAK;CAQd;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,gBAAuB,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/perf/editorProfiler.test.d.ts b/apps/client/dist-types/editor/perf/editorProfiler.test.d.ts
ddeleted file mode 100644
iindex c8265dc..0000000
---- a/apps/client/dist-types/editor/perf/editorProfiler.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=editorProfiler.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/perf/editorProfiler.test.d.ts.map b/apps/client/dist-types/editor/perf/editorProfiler.test.d.ts.map
ddeleted file mode 100644
iindex c5034c7..0000000
---- a/apps/client/dist-types/editor/perf/editorProfiler.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorProfiler.test.d.ts","sourceRoot":"","sources":["../../../src/editor/perf/editorProfiler.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/recovery/draftStore.d.ts b/apps/client/dist-types/editor/recovery/draftStore.d.ts
ddeleted file mode 100644
iindex 35c97c0..0000000
---- a/apps/client/dist-types/editor/recovery/draftStore.d.ts
++++ /dev/null
@@@ -1,95 +0,0 @@
--/**
-- * Crash/reload recovery for unsaved editor work.
-- *
-- * Everything the editor holds lives in one tab's memory until Save. Close it,
-- * reload it, or crash the GPU process, and an afternoon of terrain sculpting
-- * is gone — the `beforeunload` prompt is a warning, not a safety net.
-- *
-- * A draft is the map DOCUMENT (not Babylon objects, which are a projection of
-- * it) written to browser-local durable storage a moment after you stop typing.
-- * It is NEVER published to the server: publishing is what Save means, and a
-- * crash recovering itself into everyone else's world would be worse than
-- * losing the work. On startup, if a draft exists for THIS map at a revision
-- * the editor recognises, the user is offered it.
-- *
-- * Storage is injected so the logic is testable without IndexedDB; the browser
-- * binding is `indexedDbDraftStorage()` below.
-- */
--import type { MapFileV2 } from '@openvibe/content';
--export interface DraftRecord {
-- /** Which map this belongs to — never restore one map's work into another. */
-- mapKey: string;
-- /** The revision the editor had loaded when the draft was taken. */
-- baseRevision: string;
-- /** Epoch millis, for "saved 3 minutes ago" and for discarding stale drafts. */
-- savedAt: number;
-- map: MapFileV2;
--}
--/** The minimum a backing store must do. IndexedDB, memory, or a test fake. */
--export interface DraftStorage {
-- read(key: string): Promise<DraftRecord | null>;
-- write(key: string, record: DraftRecord): Promise<void>;
-- remove(key: string): Promise<void>;
--}
--/** Drafts older than this are not offered; the map has moved on. */
--export declare const DRAFT_MAX_AGE_MS: number;
--export interface DraftStoreOptions {
-- storage: DraftStorage;
-- /** Identity of the map being edited (server origin + path). */
-- mapKey: string;
-- /** Quiet period after the last change before writing. */
-- debounceMs?: number;
-- now?: () => number;
-- /** Injected so tests do not wait on real time. */
-- schedule?: (fn: () => void, ms: number) => unknown;
-- cancel?: (handle: unknown) => void;
--}
--export declare class DraftStore {
-- private readonly storage;
-- private readonly mapKey;
-- private readonly debounceMs;
-- private readonly now;
-- private readonly schedule;
-- private readonly cancel;
-- private timer;
-- private queued;
-- /** In-flight write, so `flush()` can be awaited deterministically. */
-- private writing;
-- constructor(opts: DraftStoreOptions);
-- /**
-- * Record a change. Debounced: a terrain stroke is hundreds of document
-- * mutations and must not be hundreds of IndexedDB writes.
-- */
-- noteChange(map: MapFileV2, baseRevision: string): void;
-- /** Write any pending draft immediately (tab hidden, explicit flush). */
-- flush(): Promise<void>;
-- /**
-- * The draft worth offering, or null.
-- *
-- * Refuses a draft from a different map (restoring one map's work into
-- * another is worse than losing it), one that is merely stale, and — the
-- * subtle one — a draft whose base revision no longer matches: the map has
-- * been saved since, so the draft's parent no longer exists and applying it
-- * would silently revert whatever happened in between.
-- */
-- pendingDraft(currentRevision: string): Promise<DraftRecord | null>;
-- /** A draft that exists but does not apply cleanly — offer export only. */
-- staleDraft(currentRevision: string): Promise<DraftRecord | null>;
-- /** After a successful server save the draft is obsolete. */
-- markSaved(): Promise<void>;
-- discard(): Promise<void>;
-- private flushCancel;
-- private writeNow;
--}
--/** In-memory storage — tests, and a fallback where IndexedDB is blocked. */
--export declare function memoryDraftStorage(): DraftStorage;
--/**
-- * IndexedDB storage. Chosen over localStorage because a sculpted map is
-- * megabytes: localStorage is a synchronous ~5 MB budget shared with
-- * everything else on the origin, and exceeding it throws mid-edit.
-- *
-- * Every failure resolves to "no draft" rather than throwing: private-mode
-- * browsers and blocked storage must not stop the editor from opening.
-- */
--export declare function indexedDbDraftStorage(): DraftStorage;
--//# sourceMappingURL=draftStore.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/recovery/draftStore.d.ts.map b/apps/client/dist-types/editor/recovery/draftStore.d.ts.map
ddeleted file mode 100644
iindex 4533ece..0000000
---- a/apps/client/dist-types/editor/recovery/draftStore.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"draftStore.d.ts","sourceRoot":"","sources":["../../../src/editor/recovery/draftStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAElD,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,SAAS,CAAA;CACf;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC9C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAED,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAA0B,CAAA;AAEvD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,YAAY,CAAA;IACrB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAA;IACd,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;IAClD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;IAClE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAElD,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,MAAM,CAAwD;IACtE,sEAAsE;IACtE,OAAO,CAAC,OAAO,CAAmC;gBAEtC,IAAI,EAAE,iBAAiB;IASnC;;;OAGG;IACH,UAAU,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAStD,wEAAwE;IAClE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B;;;;;;;;OAQG;IACG,YAAY,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAYxE,0EAA0E;IACpE,UAAU,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAMtE,4DAA4D;IACtD,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAK1B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAKhB,WAAW;YASX,QAAQ;CAWvB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,IAAI,YAAY,CAajD;AAKD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,YAAY,CAyCpD"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/recovery/draftStore.test.d.ts b/apps/client/dist-types/editor/recovery/draftStore.test.d.ts
ddeleted file mode 100644
iindex 4f26816..0000000
---- a/apps/client/dist-types/editor/recovery/draftStore.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=draftStore.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/recovery/draftStore.test.d.ts.map b/apps/client/dist-types/editor/recovery/draftStore.test.d.ts.map
ddeleted file mode 100644
iindex 35cb86a..0000000
---- a/apps/client/dist-types/editor/recovery/draftStore.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"draftStore.test.d.ts","sourceRoot":"","sources":["../../../src/editor/recovery/draftStore.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/selection/faceSelection.d.ts b/apps/client/dist-types/editor/selection/faceSelection.d.ts
ddeleted file mode 100644
iindex e706838..0000000
---- a/apps/client/dist-types/editor/selection/faceSelection.d.ts
++++ /dev/null
@@@ -1,60 +0,0 @@
--import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--/**
-- * Which surface of an object is selected. `face` is a box face index 0..5;
-- * `null` means the whole surface (cylinders, spheres, terrain), where finer
-- * sub-surface semantics do not exist yet.
-- */
--export interface FaceRef {
-- objectId: string;
-- face: number | null;
--}
--export declare const faceKey: (r: FaceRef) => string;
--export declare const parseFaceKey: (key: string) => FaceRef;
--/** Babylon's box face order, matching `Math.floor(pickInfo.faceId / 2)`. */
--export declare const FACE_NAMES: readonly ["Front", "Back", "Right", "Left", "Top", "Bottom"];
--export declare const faceLabel: (r: FaceRef) => string;
--/**
-- * Ordered set of selected faces. Insertion order fixes which one is primary,
-- * so the Face panel's "lift" always reads from a predictable surface.
-- */
--export declare class FaceSelection {
-- private _keys;
-- get size(): number;
-- keys(): string[];
-- refs(): FaceRef[];
-- primary(): FaceRef | null;
-- has(r: FaceRef): boolean;
-- replace(r: FaceRef): void;
-- /** Ctrl-click: add, or toggle off when the face is already selected. */
-- toggle(r: FaceRef): void;
-- clear(): void;
-- /** Drop faces whose object no longer exists (after undo / remote merge). */
-- retain(exists: (objectId: string) => boolean): void;
-- describe(): string;
--}
--/**
-- * Draws one translucent overlay per selected face, built from that face's own
-- * triangles. Overlays are editor-only: not pickable, and rendered in a later
-- * rendering group with a negative z-offset so they sit on the surface without
-- * z-fighting.
-- */
--export declare class FaceOverlayManager {
-- private readonly scene;
-- private overlays;
-- private readonly matPrimary;
-- private readonly matSecondary;
-- private readonly matHover;
-- constructor(scene: Scene);
-- get count(): number;
-- keys(): string[];
-- /**
-- * Rebuild overlays to exactly match `refs`. `meshOf` resolves an object id
-- * to its CURRENT mesh, so a rebuilt mesh simply produces a fresh overlay.
-- */
-- sync(refs: readonly FaceRef[], meshOf: (objectId: string) => AbstractMesh | null, hover?: FaceRef | null): void;
-- clear(): void;
-- dispose(): void;
-- private build;
--}
--//# sourceMappingURL=faceSelection.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/selection/faceSelection.d.ts.map b/apps/client/dist-types/editor/selection/faceSelection.d.ts.map
ddeleted file mode 100644
iindex 6c544c2..0000000
---- a/apps/client/dist-types/editor/selection/faceSelection.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"faceSelection.d.ts","sourceRoot":"","sources":["../../../src/editor/selection/faceSelection.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAA;AAC1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAErD;;;;GAIG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,eAAO,MAAM,OAAO,GAAI,GAAG,OAAO,KAAG,MAA4C,CAAA;AAEjF,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,KAAG,OAI1C,CAAA;AAED,4EAA4E;AAC5E,eAAO,MAAM,UAAU,8DAA+D,CAAA;AAEtF,eAAO,MAAM,SAAS,GAAI,GAAG,OAAO,KAAG,MACuC,CAAA;AAE9E;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAAe;IAE5B,IAAI,IAAI,IAAI,MAAM,CAEjB;IACD,IAAI,IAAI,MAAM,EAAE;IAGhB,IAAI,IAAI,OAAO,EAAE;IAGjB,OAAO,IAAI,OAAO,GAAG,IAAI;IAIzB,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO;IAIxB,OAAO,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAGzB,wEAAwE;IACxE,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAMxB,KAAK,IAAI,IAAI;IAGb,4EAA4E;IAC5E,MAAM,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI;IAGnD,QAAQ,IAAI,MAAM;CAMnB;AAED;;;;;GAKG;AACH,qBAAa,kBAAkB;IAMjB,OAAO,CAAC,QAAQ,CAAC,KAAK;IALlC,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAC7C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkB;IAC/C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;gBAEd,KAAK,EAAE,KAAK;IAkBzC,IAAI,KAAK,IAAI,MAAM,CAElB;IACD,IAAI,IAAI,MAAM,EAAE;IAIhB;;;OAGG;IACH,IAAI,CACF,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,GAAG,IAAI,EACjD,KAAK,GAAE,OAAO,GAAG,IAAW,GAC3B,IAAI;IAqBP,KAAK,IAAI,IAAI;IAKb,OAAO,IAAI,IAAI;IAOf,OAAO,CAAC,KAAK;CAmCd"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/selection/faceSelection.test.d.ts b/apps/client/dist-types/editor/selection/faceSelection.test.d.ts
ddeleted file mode 100644
iindex bf2556e..0000000
---- a/apps/client/dist-types/editor/selection/faceSelection.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=faceSelection.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/selection/faceSelection.test.d.ts.map b/apps/client/dist-types/editor/selection/faceSelection.test.d.ts.map
ddeleted file mode 100644
iindex 6a28e00..0000000
---- a/apps/client/dist-types/editor/selection/faceSelection.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"faceSelection.test.d.ts","sourceRoot":"","sources":["../../../src/editor/selection/faceSelection.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/selection/selectionManager.d.ts b/apps/client/dist-types/editor/selection/selectionManager.d.ts
ddeleted file mode 100644
iindex f99d381..0000000
---- a/apps/client/dist-types/editor/selection/selectionManager.d.ts
++++ /dev/null
@@@ -1,76 +0,0 @@
--/**
-- * The editor's single selection authority.
-- *
-- * Selection is a set of **document ids**, never Babylon meshes: meshes are
-- * transient projections that get rebuilt by undo, remote merges and material
-- * changes, so anything holding a mesh reference goes stale. Ids survive all
-- * of that, which is what lets undo/redo keep a selection alive.
-- *
-- * `primaryId` is the selection's anchor — the object whose values the
-- * inspector shows, and the one drawn in the strong highlight colour. It is
-- * always a member of `selectedIds` (or null when the set is empty).
-- */
--/** Everything the editor can select. Face selection is separate. */
--export type EditorObjectKind = 'static' | 'model' | 'terrain' | 'node' | 'prop' | 'spawn' | 'light';
--export interface SelectionSnapshot {
-- primaryId: string | null;
-- ids: string[];
--}
--/** How a click combines with the existing selection. */
--export type SelectMode = 'replace' | 'add' | 'remove';
--export declare class SelectionManager {
-- private _ids;
-- private _primary;
-- /** Insertion order, so the inspector and gizmo pivot are stable. */
-- private _order;
-- private _listeners;
-- /** While frozen (mid transform) selection mutations are refused. */
-- private _frozen;
-- get primaryId(): string | null;
-- get size(): number;
-- get frozen(): boolean;
-- /** Ids in insertion order — primary first. */
-- ids(): string[];
-- contains(id: string): boolean;
-- snapshot(): SelectionSnapshot;
-- onChange(fn: (s: SelectionSnapshot) => void): () => void;
-- /**
-- * Freeze during a transform session: a drag must not be able to change
-- * what it is dragging half way through (lost pointer, stray click,
-- * remote update). Unfreeze restores normal behaviour.
-- */
-- freeze(): void;
-- unfreeze(): void;
-- replace(id: string | null): boolean;
-- replaceMany(ids: string[]): boolean;
-- /** Ctrl+click: add, and make it primary. Already-selected stays selected. */
-- add(id: string): boolean;
-- /** Alt+click on a SELECTED object: drop just that one. */
-- remove(id: string): boolean;
-- clear(): boolean;
-- /**
-- * Apply the editor's click semantics in one place.
-- *
-- * plain click on object → replace
-- * ctrl click on object → add (idempotent)
-- * alt click on object → remove IF selected, otherwise nothing
-- * plain click on empty → clear
-- * ctrl/alt click empty → keep the selection
-- */
-- applyClick(hitId: string | null, mode: SelectMode): boolean;
-- /**
-- * Keep the selection meaningful when the document loses objects (undo of a
-- * create, a remote delete): drop only the ids that vanished instead of
-- * wiping the whole selection.
-- */
-- retain(existing: (id: string) => boolean): boolean;
-- private _sameAs;
-- private _emit;
--}
--/** Modifier keys → selection mode. Shift is deliberately NOT a multi-select. */
--export declare function selectModeFromEvent(e: {
-- ctrlKey?: boolean;
-- metaKey?: boolean;
-- altKey?: boolean;
--}): SelectMode;
--//# sourceMappingURL=selectionManager.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/selection/selectionManager.d.ts.map b/apps/client/dist-types/editor/selection/selectionManager.d.ts.map
ddeleted file mode 100644
iindex 99384a1..0000000
---- a/apps/client/dist-types/editor/selection/selectionManager.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"selectionManager.d.ts","sourceRoot":"","sources":["../../../src/editor/selection/selectionManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAA;AAEnG,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,GAAG,EAAE,MAAM,EAAE,CAAA;CACd;AAED,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,CAAA;AAErD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,QAAQ,CAAsB;IACtC,oEAAoE;IACpE,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,UAAU,CAA4C;IAC9D,oEAAoE;IACpE,OAAO,CAAC,OAAO,CAAQ;IAEvB,IAAI,SAAS,IAAI,MAAM,GAAG,IAAI,CAE7B;IACD,IAAI,IAAI,IAAI,MAAM,CAEjB;IACD,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,8CAA8C;IAC9C,GAAG,IAAI,MAAM,EAAE;IAKf,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAI7B,QAAQ,IAAI,iBAAiB;IAI7B,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,iBAAiB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD;;;;OAIG;IACH,MAAM,IAAI,IAAI;IAGd,QAAQ,IAAI,IAAI;IAIhB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO;IAWnC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO;IAWnC,6EAA6E;IAC7E,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAexB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAU3B,KAAK,IAAI,OAAO;IAUhB;;;;;;;;OAQG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO;IAO3D;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO;IAUlD,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,KAAK;CAId;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,CAAC,EAAE;IACrC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,GAAG,UAAU,CAIb"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/selection/selectionManager.test.d.ts b/apps/client/dist-types/editor/selection/selectionManager.test.d.ts
ddeleted file mode 100644
iindex 588e186..0000000
---- a/apps/client/dist-types/editor/selection/selectionManager.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=selectionManager.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/selection/selectionManager.test.d.ts.map b/apps/client/dist-types/editor/selection/selectionManager.test.d.ts.map
ddeleted file mode 100644
iindex bed4feb..0000000
---- a/apps/client/dist-types/editor/selection/selectionManager.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"selectionManager.test.d.ts","sourceRoot":"","sources":["../../../src/editor/selection/selectionManager.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/selection/selectionVisuals.d.ts b/apps/client/dist-types/editor/selection/selectionVisuals.d.ts
ddeleted file mode 100644
iindex 8a73040..0000000
---- a/apps/client/dist-types/editor/selection/selectionVisuals.d.ts
++++ /dev/null
@@@ -1,71 +0,0 @@
--/**
-- * Everything that shows what is selected, in one place.
-- *
-- * The old code decided highlights from mesh identity, scattered across
-- * `deselect()`, `select()`, five `selected*` variables and four `multi*`
-- * arrays — so an undo, a remote merge or a material change (all of which
-- * rebuild meshes) silently dropped the highlight, and a terrain's wireframe
-- * flickered off whenever anything rebuilt.
-- *
-- * Here the visuals are derived from the selection SET and the view registry
-- * every time they are recomputed. A mesh being rebuilt cannot lose a
-- * highlight, because the highlight was never attached to the mesh.
-- *
-- * The five states are visually distinct on purpose:
-- * hover pale blue outline
-- * local secondary warm amber
-- * local primary strong orange (what the inspector is editing)
-- * remote selected the collaborator's colour
-- * remote locked the collaborator's colour, plus a lock badge in the UI
-- */
--import { Color3 } from '@babylonjs/core/Maths/math.color.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { EditorViewRegistry } from '../viewport/editorViewRegistry.js';
--export declare const C_PRIMARY: Color3;
--export declare const C_SECONDARY: Color3;
--export declare const C_HOVER: Color3;
--export interface RemoteSelection {
-- color: Color3;
-- ids: string[];
--}
--export interface SelectionVisualState {
-- /** Local selection, primary first. */
-- ids: readonly string[];
-- primary: string | null;
-- hover: string | null;
-- /** peer id → what that collaborator has selected. */
-- remote: ReadonlyMap<number, RemoteSelection>;
-- /** object id → lock owner colour. */
-- locks: ReadonlyMap<string, string>;
-- /** True while the terrain tool wants a hovered terrain's grid shown. */
-- terrainHoverWire: string | null;
-- /** Terrain being sculpted/painted right now — its wire is pinned on. */
-- strokeWire: string | null;
--}
--export declare class SelectionVisuals {
-- private readonly views;
-- private readonly layer;
-- constructor(scene: Scene, views: EditorViewRegistry);
-- /**
-- * Recompute every highlight from the current state. Cheap enough to call
-- * on any selection or document change; deliberately NOT called per frame.
-- */
-- refresh(state: SelectionVisualState): void;
-- /**
-- * A selected terrain keeps its grid visible until deselected — it is the
-- * only cue that a flat terrain is selected at all, and it must survive
-- * hover, transforms, sculpting, undo and UI interaction.
-- */
-- private refreshTerrainWires;
-- private add;
-- /**
-- * `HighlightLayer` hooks a mesh's own bind observables, which an
-- * `InstancedMesh` does not have — imported models instantiate as instances,
-- * so selecting one used to throw and abandon the whole refresh, leaving the
-- * previous selection's highlight on screen. The model's proxy root carries
-- * the geometry that shows selection instead.
-- */
-- private canHighlight;
-- dispose(): void;
--}
--//# sourceMappingURL=selectionVisuals.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/selection/selectionVisuals.d.ts.map b/apps/client/dist-types/editor/selection/selectionVisuals.d.ts.map
ddeleted file mode 100644
iindex 189964d..0000000
---- a/apps/client/dist-types/editor/selection/selectionVisuals.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"selectionVisuals.d.ts","sourceRoot":"","sources":["../../../src/editor/selection/selectionVisuals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAA;AAI5D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAA;AAG3E,eAAO,MAAM,SAAS,QAAkC,CAAA;AACxD,eAAO,MAAM,WAAW,QAAkC,CAAA;AAC1D,eAAO,MAAM,OAAO,QAAkC,CAAA;AAEtD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,EAAE,CAAA;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qDAAqD;IACrD,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC5C,qCAAqC;IACrC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,wEAAwE;IACxE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,wEAAwE;IACxE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAJxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;gBAGpC,KAAK,EAAE,KAAK,EACK,KAAK,EAAE,kBAAkB;IAQ5C;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI;IAc1C;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,GAAG;IAWX;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;IAOpB,OAAO,IAAI,IAAI;CAGhB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/lightTool.d.ts b/apps/client/dist-types/editor/tools/lightTool.d.ts
ddeleted file mode 100644
iindex 5110fb7..0000000
---- a/apps/client/dist-types/editor/tools/lightTool.d.ts
++++ /dev/null
@@@ -1,26 +0,0 @@
--/**
-- * Placing lights.
-- *
-- * `light` was in the Tool union, on the toolbar and in the keybindings, but
-- * `ViewportInteraction` had no case for it — so choosing the Light tool fell
-- * through to selection behaviour and clicking did nothing. The tool existed
-- * in every list except the one that mattered.
-- */
--import type { MapLightV2 } from '@openvibe/content';
--export type LightType = MapLightV2['type'];
--export interface LightPlacement {
-- type: LightType;
-- color: string;
-- intensity: number;
-- /** Where the click landed, and the surface normal there. */
-- point: [number, number, number];
-- normal: [number, number, number];
--}
--/**
-- * A light for the given click, with defaults that make it visible
-- * immediately. A light placed with a range of 0 or pointing into the surface
-- * it was dropped on looks broken, and the user has no way to tell whether
-- * placement worked at all.
-- */
--export declare function lightForPlacement(p: LightPlacement): MapLightV2;
--//# sourceMappingURL=lightTool.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/lightTool.d.ts.map b/apps/client/dist-types/editor/tools/lightTool.d.ts.map
ddeleted file mode 100644
iindex 3c45c6c..0000000
---- a/apps/client/dist-types/editor/tools/lightTool.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"lightTool.d.ts","sourceRoot":"","sources":["../../../src/editor/tools/lightTool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAGnD,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;AAE1C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAKD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,cAAc,GAAG,UAAU,CAyC/D"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/lightZoneTool.test.d.ts b/apps/client/dist-types/editor/tools/lightZoneTool.test.d.ts
ddeleted file mode 100644
iindex c1ee898..0000000
---- a/apps/client/dist-types/editor/tools/lightZoneTool.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=lightZoneTool.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/lightZoneTool.test.d.ts.map b/apps/client/dist-types/editor/tools/lightZoneTool.test.d.ts.map
ddeleted file mode 100644
iindex e3c7129..0000000
---- a/apps/client/dist-types/editor/tools/lightZoneTool.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"lightZoneTool.test.d.ts","sourceRoot":"","sources":["../../../src/editor/tools/lightZoneTool.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/placementTool.d.ts b/apps/client/dist-types/editor/tools/placementTool.d.ts
ddeleted file mode 100644
iindex aa95d78..0000000
---- a/apps/client/dist-types/editor/tools/placementTool.d.ts
++++ /dev/null
@@@ -1,68 +0,0 @@
--/**
-- * Placing things: the ghost preview, where a click lands, and what object
-- * that becomes.
-- *
-- * Two behaviours worth naming:
-- *
-- * The BLANK-MAP BOOTSTRAP. A brand-new map has no geometry, so a placement
-- * ray hits nothing and the first object could never be placed at all. When
-- * the authored count is zero the first object commits at exactly the origin
-- * wherever the user clicks — rather than the old answer, which was to keep a
-- * hidden ground plane around purely so the ray had something to hit.
-- *
-- * SURFACE ALIGNMENT. A placed object's local +Y aligns to the surface normal,
-- * so a box dropped on a slope sits on it rather than through it, and the
-- * wheel yaw then turns it about that normal. Nodes, props and the spawn flag
-- * stay upright regardless: a tree growing sideways out of a hill is never
-- * what was meant.
-- */
--import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { Mesh } from '@babylonjs/core/Meshes/mesh.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import { type MapModelV2, type StaticObjectV2 } from '@openvibe/content';
--import { type Placeable } from '../catalog.js';
--import type { EditorObjectKind } from '../document/editorDocument.js';
--export interface PlacePose {
-- pos: Vector3;
-- rot: Quaternion;
--}
--export interface PlacementContext {
-- scene: Scene;
-- /** How many authored objects exist — drives the origin bootstrap. */
-- authoredCount: () => number;
-- models: () => readonly MapModelV2[];
-- /** Snap step in metres; 0 or a held bypass disables it. */
-- snapStep: () => number;
-- snapBypassed: () => boolean;
--}
--export declare const shapeHeight: (shape: StaticObjectV2["shape"]) => number;
--export declare function placeableShape(def: Placeable, models: readonly MapModelV2[]): StaticObjectV2['shape'];
--export declare class PlacementTool {
-- private readonly ctx;
-- private ghost;
-- private ghostKey;
-- private yaw;
-- constructor(ctx: PlacementContext);
-- get placeYaw(): number;
-- /** The wheel rotates the preview while a placement tool is active. */
-- rotatePreview(delta: number): void;
-- resetYaw(): void;
-- private snap;
-- /** Build/refresh the translucent preview for `def`. */
-- ensureGhost(def: Placeable | null, key: string): Mesh | null;
-- clearGhost(): void;
-- get preview(): Mesh | null;
-- /** Where the object would go, or null when the ray hits nothing. */
-- computePose(def: Placeable | null): PlacePose | null;
-- updatePreview(def: Placeable | null, key: string): void;
--}
--/**
-- * The document object a placement produces. Returning the value rather than
-- * inserting it keeps this pure — the caller wraps it in a command, which is
-- * what makes placement undoable without this knowing history exists.
-- */
--export declare function objectForPlacement(def: Placeable, pose: PlacePose, models: readonly MapModelV2[]): {
-- kind: EditorObjectKind;
-- object: Record<string, unknown>;
--} | null;
--//# sourceMappingURL=placementTool.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/placementTool.d.ts.map b/apps/client/dist-types/editor/tools/placementTool.d.ts.map
ddeleted file mode 100644
iindex 8f7d8ee..0000000
---- a/apps/client/dist-types/editor/tools/placementTool.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"placementTool.d.ts","sourceRoot":"","sources":["../../../src/editor/tools/placementTool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAC1E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAA;AAC1D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAErD,OAAO,EAAgB,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACtF,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,eAAe,CAAA;AAEjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAErE,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,OAAO,CAAA;IACZ,GAAG,EAAE,UAAU,CAAA;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,KAAK,CAAA;IACZ,qEAAqE;IACrE,aAAa,EAAE,MAAM,MAAM,CAAA;IAC3B,MAAM,EAAE,MAAM,SAAS,UAAU,EAAE,CAAA;IACnC,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,OAAO,CAAA;CAC5B;AAED,eAAO,MAAM,WAAW,GAAI,OAAO,cAAc,CAAC,OAAO,CAAC,KAAG,MACuC,CAAA;AAEpG,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,EACd,MAAM,EAAE,SAAS,UAAU,EAAE,GAC5B,cAAc,CAAC,OAAO,CAAC,CAQzB;AAKD,qBAAa,aAAa;IAKZ,OAAO,CAAC,QAAQ,CAAC,GAAG;IAJhC,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,GAAG,CAAI;gBAEc,GAAG,EAAE,gBAAgB;IAElD,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,sEAAsE;IACtE,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIlC,QAAQ,IAAI,IAAI;IAIhB,OAAO,CAAC,IAAI;IAMZ,uDAAuD;IACvD,WAAW,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAqB5D,UAAU,IAAI,IAAI;IAMlB,IAAI,OAAO,IAAI,IAAI,GAAG,IAAI,CAEzB;IAED,oEAAoE;IACpE,WAAW,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI;IAmCpD,aAAa,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;CASxD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,SAAS,UAAU,EAAE,GAC5B;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,IAAI,CAkEpE"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/terrainTool.d.ts b/apps/client/dist-types/editor/tools/terrainTool.d.ts
ddeleted file mode 100644
iindex df4d66b..0000000
---- a/apps/client/dist-types/editor/tools/terrainTool.d.ts
++++ /dev/null
@@@ -1,43 +0,0 @@
--/**
-- * Terrain sculpting: the brush maths, and the rule that one stroke is one
-- * history entry holding only what changed.
-- *
-- * The heightfield the brush edits is the TerrainView's decoded working
-- * buffer. Writing it back to the document as base64 on every dab would
-- * re-encode a quarter of a million floats per frame, so the stroke edits the
-- * buffer live and commits once on release — the same shape as a gizmo drag.
-- */
--export type SculptMode = 'sculpt' | 'smooth' | 'flatten';
--export interface BrushSettings {
-- radius: number;
-- strength: number;
-- feather: number;
--}
--export interface SculptTarget {
-- heights: Float32Array;
-- /** Grid subdivisions; the field is (sub+1)². */
-- sub: number;
-- /** Half-width in metres. */
-- halfExtent: number;
--}
--/**
-- * Apply one brush dab at terrain-local (px, pz). `sign` is +1 to raise and
-- * -1 to lower. Mutates `target.heights` in place; the caller refreshes the
-- * mesh and, on release, computes the delta for history.
-- */
--export declare function sculptDab(target: SculptTarget, px: number, pz: number, sign: number, mode: SculptMode, brush: BrushSettings): void;
--/**
-- * World point → terrain-local, accounting for the terrain's own transform.
-- * Sculpting a rotated or scaled terrain has to land where the cursor is, not
-- * where it would be if the terrain were at the origin.
-- */
--export declare function toTerrainLocal(world: {
-- x: number;
-- y: number;
-- z: number;
--}, pos: readonly [number, number, number], rot: readonly [number, number, number] | undefined, scale: readonly [number, number, number] | undefined): {
-- x: number;
-- y: number;
-- z: number;
--};
--//# sourceMappingURL=terrainTool.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/terrainTool.d.ts.map b/apps/client/dist-types/editor/tools/terrainTool.d.ts.map
ddeleted file mode 100644
iindex f835eb4..0000000
---- a/apps/client/dist-types/editor/tools/terrainTool.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"terrainTool.d.ts","sourceRoot":"","sources":["../../../src/editor/tools/terrainTool.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAA;AAExD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,YAAY,CAAA;IACrB,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAA;IACX,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,YAAY,EACpB,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,aAAa,GACnB,IAAI,CA6CN;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC1C,GAAG,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACtC,GAAG,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAClD,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GACnD;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAoBrC"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/terrainTool.test.d.ts b/apps/client/dist-types/editor/tools/terrainTool.test.d.ts
ddeleted file mode 100644
iindex 56c408c..0000000
---- a/apps/client/dist-types/editor/tools/terrainTool.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=terrainTool.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/terrainTool.test.d.ts.map b/apps/client/dist-types/editor/tools/terrainTool.test.d.ts.map
ddeleted file mode 100644
iindex 18ac189..0000000
---- a/apps/client/dist-types/editor/tools/terrainTool.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"terrainTool.test.d.ts","sourceRoot":"","sources":["../../../src/editor/tools/terrainTool.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/toolManager.d.ts b/apps/client/dist-types/editor/tools/toolManager.d.ts
ddeleted file mode 100644
iindex 696f6d8..0000000
---- a/apps/client/dist-types/editor/tools/toolManager.d.ts
++++ /dev/null
@@@ -1,43 +0,0 @@
--/**
-- * Which tool is active — including none.
-- *
-- * "No tool" used to be inexpressible: `tool` was always one of the seven, so
-- * the editor was always in some mode that reacted to clicks. Pressing the
-- * active tool's hotkey again did nothing, and there was no way to get back to
-- * "just look at the map" short of picking a tool that happened to be
-- * harmless. `Tool | null` is the fix, and it is the ONLY place tool state
-- * lives: the toolbar's active class and every hotkey read from here.
-- *
-- * With no tool active: selection still works, navigation still works, and
-- * nothing places, sculpts, paints or edits.
-- */
--import type { Tool } from '../catalog.js';
--export type ToolChangeListener = (tool: Tool | null, previous: Tool | null) => void;
--export interface ToolManagerOptions {
-- /**
-- * Called before leaving a tool, so it can drop transient state (a
-- * placement ghost, a half-finished face selection). Returning false vetoes
-- * the change — used only when a gesture is genuinely mid-flight.
-- */
-- onLeave?: (tool: Tool) => boolean | void;
-- onEnter?: (tool: Tool) => void;
--}
--export declare class ToolManager {
-- private readonly opts;
-- private current;
-- private readonly listeners;
-- constructor(opts?: ToolManagerOptions);
-- get active(): Tool | null;
-- is(tool: Tool): boolean;
-- /** Explicitly set (or clear) the tool. */
-- set(next: Tool | null): void;
-- /**
-- * What a toolbar button or a tool hotkey does: activate it, or — if it is
-- * already active — turn it off. One control, both directions.
-- */
-- toggle(tool: Tool): void;
-- /** Escape: leave whatever mode we are in. */
-- clear(): void;
-- subscribe(listener: ToolChangeListener): () => void;
--}
--//# sourceMappingURL=toolManager.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/toolManager.d.ts.map b/apps/client/dist-types/editor/tools/toolManager.d.ts.map
ddeleted file mode 100644
iindex abd59fb..0000000
---- a/apps/client/dist-types/editor/tools/toolManager.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"toolManager.d.ts","sourceRoot":"","sources":["../../../src/editor/tools/toolManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AAEzC,MAAM,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAA;AAEnF,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,GAAG,IAAI,CAAA;IACxC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;CAC/B;AAED,qBAAa,WAAW;IAIV,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;gBAE7B,IAAI,GAAE,kBAAuB;IAE1D,IAAI,MAAM,IAAI,IAAI,GAAG,IAAI,CAExB;IAED,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO;IAIvB,0CAA0C;IAC1C,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI;IAS5B;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAIxB,6CAA6C;IAC7C,KAAK,IAAI,IAAI;IAIb,SAAS,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI;CAIpD"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/toolManager.test.d.ts b/apps/client/dist-types/editor/tools/toolManager.test.d.ts
ddeleted file mode 100644
iindex 414fc0e..0000000
---- a/apps/client/dist-types/editor/tools/toolManager.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=toolManager.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/toolManager.test.d.ts.map b/apps/client/dist-types/editor/tools/toolManager.test.d.ts.map
ddeleted file mode 100644
iindex 73a42c7..0000000
---- a/apps/client/dist-types/editor/tools/toolManager.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"toolManager.test.d.ts","sourceRoot":"","sources":["../../../src/editor/tools/toolManager.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/zoneTool.d.ts b/apps/client/dist-types/editor/tools/zoneTool.d.ts
ddeleted file mode 100644
iindex c0d9966..0000000
---- a/apps/client/dist-types/editor/tools/zoneTool.d.ts
++++ /dev/null
@@@ -1,30 +0,0 @@
--/**
-- * Creating zones.
-- *
-- * Zones already had a schema, runtime rule application, a view (translucent
-- * volume + wireframe), an Outliner entry and Inspector fields — everything
-- * except a way to make one. So there was no path from an empty map to a zone
-- * at all.
-- *
-- * A click makes a sensible axis-aligned volume centred on where you clicked,
-- * selects it, and lets the ordinary move/scale gizmo do the rest: a zone's
-- * scale IS its half-extent (see `editorObject.ts`), so resizing it with the
-- * gizmo resizes the volume the rules apply in, not a mesh that then lies
-- * about where they are.
-- */
--import type { MapZoneV2 } from '@openvibe/content';
--export interface ZonePlacement {
-- point: [number, number, number];
-- /** Footprint width/depth in metres. */
-- size: number;
-- height: number;
-- /** Existing zone ids, so the new one is unique and readable. */
-- taken: (id: string) => boolean;
--}
--/**
-- * Zone ids are lower-case alphanumeric with `_`/`-` (see `MapZoneSchemaV2`),
-- * so they cannot use the generic `newId` timestamp format directly.
-- */
--export declare function nextZoneId(taken: (id: string) => boolean): string;
--export declare function zoneForPlacement(p: ZonePlacement): MapZoneV2;
--//# sourceMappingURL=zoneTool.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/tools/zoneTool.d.ts.map b/apps/client/dist-types/editor/tools/zoneTool.d.ts.map
ddeleted file mode 100644
iindex 027c7b8..0000000
---- a/apps/client/dist-types/editor/tools/zoneTool.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"zoneTool.d.ts","sourceRoot":"","sources":["../../../src/editor/tools/zoneTool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAElD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;CAC/B;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,MAAM,CAMjE;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,SAAS,CAe5D"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui.d.ts b/apps/client/dist-types/editor/ui.d.ts
ddeleted file mode 100644
iindex e794902..0000000
---- a/apps/client/dist-types/editor/ui.d.ts
++++ /dev/null
@@@ -1,41 +0,0 @@
--/**
-- * Editor UI widgets: drag-scrubbable number inputs (Blender-style) and a
-- * thumbnail texture picker that replaces bare <select> dropdowns. Pure DOM
-- * — no Babylon dependencies.
-- */
--/**
-- * Blender/Unity-style numeric input: click-drag left/right scrubs the
-- * value (Shift = fine), plain click focuses for typing, and ←/→/↑/↓ step
-- * while focused. Dispatches 'input' during scrub and 'change' on release.
-- */
--export declare function makeScrubbable(input: HTMLInputElement): void;
--/** Make every number input under `root` scrubbable (idempotent). */
--export declare function scrubAllNumbers(root: ParentNode): void;
--export interface TexOption {
-- value: string;
-- label: string;
-- /** Thumbnail URL; null renders a flat color swatch. */
-- thumb: string | null;
--}
--export interface TexPicker {
-- /** Re-read options (after uploads/renames) and sync the button. */
-- refresh(): void;
-- /** Update the button after external `select.value = …` writes. */
-- sync(): void;
--}
--/**
-- * Thumbnail dropdown bound to a hidden <select>: selecting a tile writes
-- * select.value and fires 'change', so existing handlers keep working.
-- */
--export declare function createTexPicker(sel: HTMLSelectElement, getOptions: () => TexOption[]): TexPicker;
--/**
-- * Downscale an uploaded image to a web-friendly texture (≤2048px, JPEG).
-- * Keeps 4k source files usable without shipping 10MB+ to every player.
-- */
--export declare function downscaleImage(file: File, maxDim?: number): Promise<{
-- blob: Blob;
-- ext: 'jpg';
-- width: number;
-- height: number;
--}>;
--//# sourceMappingURL=ui.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui.d.ts.map b/apps/client/dist-types/editor/ui.d.ts.map
ddeleted file mode 100644
iindex 4170814..0000000
---- a/apps/client/dist-types/editor/ui.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/editor/ui.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAgE5D;AAED,oEAAoE;AACpE,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAEtD;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,uDAAuD;IACvD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,mEAAmE;IACnE,OAAO,IAAI,IAAI,CAAA;IACf,kEAAkE;IAClE,IAAI,IAAI,IAAI,CAAA;CACb;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,SAAS,EAAE,GAAG,SAAS,CAwEhG;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,IAAI,EACV,MAAM,SAAO,GACZ,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAgBpE"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/editorShell.d.ts b/apps/client/dist-types/editor/ui/editorShell.d.ts
ddeleted file mode 100644
iindex f7f8118..0000000
---- a/apps/client/dist-types/editor/ui/editorShell.d.ts
++++ /dev/null
@@@ -1,33 +0,0 @@
--import { type Tool } from '../catalog.js';
--export interface ShellElements {
-- root: HTMLElement;
-- toolbar: HTMLElement;
-- tools: HTMLElement;
-- outliner: HTMLElement;
-- outlinerFilter: HTMLInputElement;
-- inspector: HTMLElement;
-- viewport: HTMLElement;
-- dock: HTMLElement;
-- dockTabs: HTMLElement;
-- dockPanels: Record<string, HTMLElement>;
-- status: HTMLElement;
-- message: HTMLElement;
-- /** Tool-specific control strips shown above the dock. */
-- brush: HTMLElement;
-- placement: HTMLElement;
-- face: HTMLElement;
-- light: HTMLElement;
-- zone: HTMLElement;
-- resizers: {
-- outliner: HTMLElement;
-- inspector: HTMLElement;
-- dock: HTMLElement;
-- };
-- collapse: Record<'outliner' | 'inspector' | 'dock', HTMLButtonElement>;
--}
--export declare const TOOLS: [Tool, string, string][];
--/** Build the workspace into `mount` and return every element the app drives. */
--export declare function buildShell(mount: HTMLElement, canvas: HTMLCanvasElement): ShellElements;
--/** Every keyboard action, for the settings panel's completeness check. */
--export declare const ALL_ACTIONS: import("../bindings.js").ActionDef[];
--//# sourceMappingURL=editorShell.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/editorShell.d.ts.map b/apps/client/dist-types/editor/ui/editorShell.d.ts.map
ddeleted file mode 100644
iindex c7603aa..0000000
---- a/apps/client/dist-types/editor/ui/editorShell.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorShell.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/editorShell.ts"],"names":[],"mappings":"AAcA,OAAO,EAAqC,KAAK,IAAI,EAAE,MAAM,eAAe,CAAA;AAE5E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAA;IACjB,OAAO,EAAE,WAAW,CAAA;IACpB,KAAK,EAAE,WAAW,CAAA;IAClB,QAAQ,EAAE,WAAW,CAAA;IACrB,cAAc,EAAE,gBAAgB,CAAA;IAChC,SAAS,EAAE,WAAW,CAAA;IACtB,QAAQ,EAAE,WAAW,CAAA;IACrB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,WAAW,CAAA;IACrB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACvC,MAAM,EAAE,WAAW,CAAA;IACnB,OAAO,EAAE,WAAW,CAAA;IACpB,yDAAyD;IACzD,KAAK,EAAE,WAAW,CAAA;IAClB,SAAS,EAAE,WAAW,CAAA;IACtB,IAAI,EAAE,WAAW,CAAA;IACjB,KAAK,EAAE,WAAW,CAAA;IAClB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE;QAAE,QAAQ,EAAE,WAAW,CAAC;QAAC,SAAS,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,WAAW,CAAA;KAAE,CAAA;IAC9E,QAAQ,EAAE,MAAM,CAAC,UAAU,GAAG,WAAW,GAAG,MAAM,EAAE,iBAAiB,CAAC,CAAA;CACvE;AAED,eAAO,MAAM,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EASzC,CAAA;AAqDD,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,GAAG,aAAa,CAwPvF;AAeD,0EAA0E;AAC1E,eAAO,MAAM,WAAW,sCAAU,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/editorUi.d.ts b/apps/client/dist-types/editor/ui/editorUi.d.ts
ddeleted file mode 100644
iindex a757389..0000000
---- a/apps/client/dist-types/editor/ui/editorUi.d.ts
++++ /dev/null
@@@ -1,69 +0,0 @@
--/**
-- * The panels, wired to the editor's state.
-- *
-- * Each panel is a dumb renderer (see `outliner.ts`, `inspector.ts`,
-- * `panels.ts`); this is where they learn what to show and where their
-- * callbacks go. Splitting it this way is what lets the panels be tested
-- * without an engine, a document or a server.
-- */
--import type { Environment } from '../../render/environment.js';
--import type { EditorDocument } from '../document/editorDocument.js';
--import type { CommandHistory } from '../history/commandHistory.js';
--import type { SelectionManager } from '../selection/selectionManager.js';
--import type { ToolManager } from '../tools/toolManager.js';
--import type { EditorViewRegistry } from '../viewport/editorViewRegistry.js';
--import type { GizmoController } from '../viewport/gizmoController.js';
--import type { EditorPreferences } from '../viewport/editorPreferences.js';
--import type { Binding } from '../bindings.js';
--import type { AssetController } from '../assets/assetController.js';
--import { type ShellElements } from './editorShell.js';
--import type { SaveController } from '../net/saveController.js';
--export interface EditorConnectionView {
-- peerCount: () => number;
-- remoteSelectionColors: () => ReadonlyMap<string, string>;
-- lockOwners: () => ReadonlyMap<string, string>;
-- lockOwner: (id: string) => string | null;
-- owns: (id: string) => boolean;
-- connected: () => boolean;
--}
--export interface EditorUiOptions {
-- shell: ShellElements;
-- doc: EditorDocument;
-- views: EditorViewRegistry;
-- selection: SelectionManager;
-- history: CommandHistory<EditorDocument>;
-- tools: ToolManager;
-- prefs: {
-- current: EditorPreferences;
-- };
-- gizmo: GizmoController;
-- env: Environment;
-- connection: EditorConnectionView;
-- saveController: SaveController;
-- bindings: Record<string, Binding>;
-- bindingOf: (action: string) => Binding;
-- assets: AssetController;
-- /** Arm the placement tool with an imported model. */
-- placeModel: (id: string) => void;
-- onPreferences: () => void;
-- focusObject: (id: string) => void;
-- deleteSelection: () => void;
-- duplicateSelection: () => void;
-- setProperty: (ids: readonly string[], key: string, value: unknown) => void;
--}
--export interface EditorUi {
-- refreshAll: () => void;
-- refreshSelection: () => void;
-- refreshInspectorValues: () => void;
-- refreshStatus: () => void;
-- refreshTools: () => void;
-- setMessage: (m: string) => void;
-- togglePanel: (panel: 'outliner' | 'inspector') => void;
-- showDockTab: (tab: string) => void;
-- openSettings: () => void;
-- duplicate: () => void;
-- remove: () => void;
-- setProperty: (ids: readonly string[], key: string, value: unknown) => void;
--}
--export declare function createEditorUi(opts: EditorUiOptions): EditorUi;
--//# sourceMappingURL=editorUi.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/editorUi.d.ts.map b/apps/client/dist-types/editor/ui/editorUi.d.ts.map
ddeleted file mode 100644
iindex ecca4f5..0000000
---- a/apps/client/dist-types/editor/ui/editorUi.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorUi.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/editorUi.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/inspector.d.ts b/apps/client/dist-types/editor/ui/inspector.d.ts
ddeleted file mode 100644
iindex 7d75fa5..0000000
---- a/apps/client/dist-types/editor/ui/inspector.d.ts
++++ /dev/null
@@@ -1,92 +0,0 @@
--/**
-- * The Inspector: a registry of per-kind property editors.
-- *
-- * The old inspector was one floating popover with every control for every
-- * kind in it, shown and hidden by hand — so a terrain showed cylinder radius
-- * fields, a light showed box dimensions, and adding a kind meant another
-- * round of `style.display = 'none'`. Here each kind declares the fields it
-- * has, and the panel renders exactly those.
-- *
-- * Multi-selection shows the fields the selection has in COMMON. Where the
-- * values disagree the field shows an em dash rather than a zero: an editor
-- * that displays 0 for "these differ" invites you to apply 0 to all of them.
-- *
-- * Every edit goes through the callbacks, which route to CommandHistory.
-- * Nothing here writes to the document.
-- */
--import type { EditorDocument, EditorObject, EditorObjectKind } from '../document/editorDocument.js';
--export type FieldType = 'number' | 'text' | 'color' | 'checkbox' | 'select' | 'readonly';
--export interface FieldSpec {
-- key: string;
-- label: string;
-- type: FieldType;
-- /** Radians in the model, degrees in the box. */
-- degrees?: boolean;
-- step?: number;
-- min?: number;
-- max?: number;
-- options?: {
-- value: string;
-- label: string;
-- }[];
-- /** Vector components are edited together under one label. */
-- vector?: 3;
-- /** Not editable while another user holds the lock; still readable. */
-- section: string;
--}
--export interface InspectorCallbacks {
-- /** One edit → one history entry, applied to every selected id. */
-- setProperty: (ids: readonly string[], key: string, value: unknown) => void;
-- /** A scrub gesture: preview without recording. */
-- previewProperty?: (ids: readonly string[], key: string, value: unknown) => void;
-- beginGesture?: (label: string) => void;
-- endGesture?: () => void;
--}
--/**
-- * The fields for a kind. Dimensions and transform scale are DELIBERATELY
-- * separate: a 2 m box scaled ×3 and a 6 m box collide identically but mean
-- * different things to an author, and conflating them is how a resize ends up
-- * changing a shared prefab.
-- */
--export declare function fieldsFor(kind: EditorObjectKind, object: EditorObject | null): FieldSpec[];
--/** Fields shared by every selected object, in the first one's order. */
--export declare function commonFields(doc: EditorDocument, ids: readonly string[]): {
-- fields: FieldSpec[];
-- kinds: EditorObjectKind[];
--};
--export declare const readPath: (object: unknown, path: string) => unknown;
--/**
-- * What a field should show for a selection: the shared value, or null when
-- * they disagree. `null` renders as an em dash.
-- */
--export declare function displayValue(doc: EditorDocument, ids: readonly string[], field: FieldSpec, component?: number): string | number | boolean | null;
--export interface InspectorState {
-- ids: readonly string[];
-- /** Set while another user holds the lock: readable, not editable. */
-- lockedBy: string | null;
--}
--export declare class Inspector {
-- private readonly root;
-- private readonly doc;
-- private readonly callbacks;
-- /** Texture options for `select` fields; supplied by the asset registry. */
-- private readonly textureOptions;
-- private state;
-- constructor(root: HTMLElement, doc: EditorDocument, callbacks: InspectorCallbacks,
-- /** Texture options for `select` fields; supplied by the asset registry. */
-- textureOptions?: () => {
-- value: string;
-- label: string;
-- }[]);
-- setState(next: InspectorState): void;
-- /** Values only — what a gizmo drag or an undo needs. */
-- refreshValues(): void;
-- private specs;
-- private specFor;
-- private render;
-- private renderField;
-- private renderInput;
-- private commitInput;
-- private commit;
--}
--//# sourceMappingURL=inspector.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/inspector.d.ts.map b/apps/client/dist-types/editor/ui/inspector.d.ts.map
ddeleted file mode 100644
iindex 36b0001..0000000
---- a/apps/client/dist-types/editor/ui/inspector.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"inspector.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/inspector.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/inspector.test.d.ts b/apps/client/dist-types/editor/ui/inspector.test.d.ts
ddeleted file mode 100644
iindex c54f016..0000000
---- a/apps/client/dist-types/editor/ui/inspector.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=inspector.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/inspector.test.d.ts.map b/apps/client/dist-types/editor/ui/inspector.test.d.ts.map
ddeleted file mode 100644
iindex 4ce34f0..0000000
---- a/apps/client/dist-types/editor/ui/inspector.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"inspector.test.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/inspector.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/issuesPanel.d.ts b/apps/client/dist-types/editor/ui/issuesPanel.d.ts
ddeleted file mode 100644
iindex 4495eb9..0000000
---- a/apps/client/dist-types/editor/ui/issuesPanel.d.ts
++++ /dev/null
@@@ -1,27 +0,0 @@
--/**
-- * The Issues panel: everything wrong with the map, in one place.
-- *
-- * These checks already existed — `validateMapFile` and `validateSurface` run
-- * on every save — but their output only appeared as a rejection message after
-- * the fact. Surfacing them live means an author sees a missing texture or a
-- * duplicate id while they can still fix it, and clicking an issue selects the
-- * object it belongs to.
-- */
--export type IssueSeverity = 'error' | 'warning' | 'info';
--export interface EditorIssue {
-- severity: IssueSeverity;
-- message: string;
-- /** Object to select when the row is clicked, when there is one. */
-- objectId?: string;
--}
--export interface IssuesPanelDeps {
-- /** Recompute the current issue list. */
-- collect: () => EditorIssue[];
-- /** Select and frame an object the user clicked through to. */
-- focus: (objectId: string) => void;
--}
--export declare function createIssuesPanel(deps: IssuesPanelDeps): {
-- refresh: () => void;
-- count: () => number;
--};
--//# sourceMappingURL=issuesPanel.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/issuesPanel.d.ts.map b/apps/client/dist-types/editor/ui/issuesPanel.d.ts.map
ddeleted file mode 100644
iindex 17a8058..0000000
---- a/apps/client/dist-types/editor/ui/issuesPanel.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"issuesPanel.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/issuesPanel.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;AAExD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,aAAa,CAAA;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,wCAAwC;IACxC,OAAO,EAAE,MAAM,WAAW,EAAE,CAAA;IAC5B,8DAA8D;IAC9D,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC;AASD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG;IACxD,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,MAAM,CAAA;CACpB,CAqDA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/numberField.d.ts b/apps/client/dist-types/editor/ui/numberField.d.ts
ddeleted file mode 100644
iindex 663c8f2..0000000
---- a/apps/client/dist-types/editor/ui/numberField.d.ts
++++ /dev/null
@@@ -1,74 +0,0 @@
--/**
-- * The editor's number input.
-- *
-- * Typing a value into a box is the least of what this has to do. Level
-- * editors are driven by DRAGGING numbers, and the details are what make it
-- * usable: a drag must be one undo entry rather than one per pixel, Escape
-- * must return the value you started from, arrow keys must step, a modifier
-- * must make the step fine or coarse, and the box must keep showing the truth
-- * while a gizmo moves the same object underneath it.
-- *
-- * Committing is explicit (`onCommit`) and separate from previewing
-- * (`onPreview`), which is what lets the caller open one history transaction
-- * for the whole gesture.
-- */
--export interface NumberFieldOptions {
-- input: HTMLInputElement;
-- /** Live value while dragging/typing. Not a history entry. */
-- onPreview?: (value: number) => void;
-- /** End of gesture: make it one history entry. */
-- onCommit: (value: number) => void;
-- /** Called when the gesture is abandoned; restore the starting value. */
-- onCancel?: () => void;
-- /** Units per pixel of horizontal drag. */
-- sensitivity?: number;
-- step?: number;
-- min?: number;
-- max?: number;
-- /** Author-facing degrees over a radian model. */
-- degrees?: boolean;
--}
--export declare class NumberField {
-- private readonly input;
-- private readonly opts;
-- private dragging;
-- private startValue;
-- private startX;
-- private accumulated;
-- /** True while `set()` is writing, so the change handler stays quiet. */
-- private writing;
-- private readonly disposers;
-- constructor(options: NumberFieldOptions);
-- private on;
-- dispose(): void;
-- /**
-- * Write a value in without firing a commit — gizmo drags, undo, a remote
-- * document replacement. This also becomes the value Escape returns to and
-- * the one arrow keys step from: it is the field's truth, just not one the
-- * field caused.
-- */
-- set(value: number | null): void;
-- /**
-- * Write the text only. During a drag the displayed value changes every
-- * frame while `startValue` must stay at the pose the gesture started from
-- * — that is what Escape restores and what makes "did this drag change
-- * anything?" answerable at the end.
-- */
-- private show;
-- value(): number;
-- private display;
-- private model;
-- private read;
-- private clamp;
-- private onPointerDown;
-- private onPointerMove;
-- private onPointerUp;
-- private finishDrag;
-- private onKeyDown;
-- private commit;
--}
--/** Trim float noise without hiding real precision. */
--export declare function formatNumber(v: number): string;
--/** Common value across a selection, or null when they disagree. */
--export declare function commonValue(values: readonly number[]): number | null;
--//# sourceMappingURL=numberField.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/numberField.d.ts.map b/apps/client/dist-types/editor/ui/numberField.d.ts.map
ddeleted file mode 100644
iindex e9d175c..0000000
---- a/apps/client/dist-types/editor/ui/numberField.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"numberField.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/numberField.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,gBAAgB,CAAA;IACvB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,iDAAiD;IACjD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,iDAAiD;IACjD,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAKD,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,IAAI,CACD;IACpB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,WAAW,CAAI;IACvB,wEAAwE;IACxE,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;gBAEnC,OAAO,EAAE,kBAAkB;IAiBvC,OAAO,CAAC,EAAE;IAKV,OAAO,IAAI,IAAI;IAKf;;;;;OAKG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAQ/B;;;;;OAKG;IACH,OAAO,CAAC,IAAI;IAQZ,KAAK,IAAI,MAAM;IAIf,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,SAAS;IA2BjB,OAAO,CAAC,MAAM;CAKf;AAED,sDAAsD;AACtD,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI9C;AAED,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAIpE"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/numberField.test.d.ts b/apps/client/dist-types/editor/ui/numberField.test.d.ts
ddeleted file mode 100644
iindex 09faca6..0000000
---- a/apps/client/dist-types/editor/ui/numberField.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=numberField.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/numberField.test.d.ts.map b/apps/client/dist-types/editor/ui/numberField.test.d.ts.map
ddeleted file mode 100644
iindex 167637f..0000000
---- a/apps/client/dist-types/editor/ui/numberField.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"numberField.test.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/numberField.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/outliner.d.ts b/apps/client/dist-types/editor/ui/outliner.d.ts
ddeleted file mode 100644
iindex 2349369..0000000
---- a/apps/client/dist-types/editor/ui/outliner.d.ts
++++ /dev/null
@@@ -1,64 +0,0 @@
--/**
-- * The scene graph, built from the DOCUMENT.
-- *
-- * Deliberately not from `scene.getNodes()`: Babylon's tree contains
-- * wireframe overlays, light widgets, gizmo utility meshes, imported-model
-- * hierarchies and the brush cursor, none of which are objects a user has any
-- * business seeing, and it does not contain the one thing that matters — the
-- * stable id. Reading the document gives exactly the authored objects, in a
-- * stable order, with their identity.
-- */
--import type { EditorDocument, EditorObjectKind } from '../document/editorDocument.js';
--export interface OutlinerRow {
-- id: string;
-- kind: EditorObjectKind;
-- label: string;
-- icon: string;
-- group: string;
--}
--export interface OutlinerState {
-- selected: ReadonlySet<string>;
-- primary: string | null;
-- /** object id → collaborator colour, for remote selection. */
-- remote: ReadonlyMap<string, string>;
-- /** object id → lock owner name. */
-- locks: ReadonlyMap<string, string>;
-- /** Editor-only visibility (the eye toggle). */
-- hidden: ReadonlySet<string>;
-- filter: string;
--}
--export interface OutlinerCallbacks {
-- onSelect: (id: string, mode: 'replace' | 'add' | 'subtract') => void;
-- onFocus: (id: string) => void;
-- onToggleVisible: (id: string) => void;
-- onDelete: (id: string) => void;
-- onDuplicate: (id: string) => void;
--}
--export declare function buildRows(doc: EditorDocument, filter: string): OutlinerRow[];
--export declare function groupRows(rows: readonly OutlinerRow[]): {
-- group: string;
-- rows: OutlinerRow[];
--}[];
--/** Ctrl adds, Alt subtracts, a plain click replaces — same as the viewport. */
--export declare function selectModeFor(e: {
-- ctrlKey: boolean;
-- metaKey: boolean;
-- altKey: boolean;
--}): 'replace' | 'add' | 'subtract';
--export declare class Outliner {
-- private readonly root;
-- private readonly doc;
-- private readonly callbacks;
-- private state;
-- /** The rows currently in the DOM, so a refresh can patch rather than rebuild. */
-- private rendered;
-- constructor(root: HTMLElement, doc: EditorDocument, callbacks: OutlinerCallbacks);
-- setFilter(filter: string): void;
-- setState(next: Partial<OutlinerState>): void;
-- /** Full rebuild — objects added or removed. */
-- render(): void;
-- private renderRow;
-- /** Selection, remote colours and lock badges, without rebuilding rows. */
-- private restyle;
--}
--//# sourceMappingURL=outliner.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/outliner.d.ts.map b/apps/client/dist-types/editor/ui/outliner.d.ts.map
ddeleted file mode 100644
iindex 1455aa5..0000000
---- a/apps/client/dist-types/editor/ui/outliner.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"outliner.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/outliner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAGrF,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,gBAAgB,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,6DAA6D;IAC7D,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,mCAAmC;IACnC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,+CAA+C;IAC/C,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,KAAK,GAAG,UAAU,KAAK,IAAI,CAAA;IACpE,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7B,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC;AAKD,wBAAgB,SAAS,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE,CAqB5E;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,EAAE,CAAA;CAAE,EAAE,CAchG;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,CAAC,EAAE;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;CAChB,GAAG,SAAS,GAAG,KAAK,GAAG,UAAU,CAIjC;AAED,qBAAa,QAAQ;IAajB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAd5B,OAAO,CAAC,KAAK,CAOZ;IACD,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAiC;gBAG9B,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,cAAc,EACnB,SAAS,EAAE,iBAAiB;IAG/C,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK/B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI;IAM5C,+CAA+C;IAC/C,MAAM,IAAI,IAAI;IAwBd,OAAO,CAAC,SAAS;IAyCjB,0EAA0E;IAC1E,OAAO,CAAC,OAAO;CAkBhB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/outliner.test.d.ts b/apps/client/dist-types/editor/ui/outliner.test.d.ts
ddeleted file mode 100644
iindex 6a5c0cd..0000000
---- a/apps/client/dist-types/editor/ui/outliner.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=outliner.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/outliner.test.d.ts.map b/apps/client/dist-types/editor/ui/outliner.test.d.ts.map
ddeleted file mode 100644
iindex a18740e..0000000
---- a/apps/client/dist-types/editor/ui/outliner.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"outliner.test.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/outliner.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/panels.d.ts b/apps/client/dist-types/editor/ui/panels.d.ts
ddeleted file mode 100644
iindex 334b3aa..0000000
---- a/apps/client/dist-types/editor/ui/panels.d.ts
++++ /dev/null
@@@ -1,106 +0,0 @@
--/**
-- * The dock's smaller panels: Assets, History, Scene and the status bar.
-- *
-- * They share a shape — render from state, no internal truth — so they live
-- * together rather than in four files that each hold one class and an import
-- * list. (Issues has its own file already; it predates the workspace and has
-- * its own validation model.)
-- */
--import type { MapModelV2, MapTextureEntry } from '@openvibe/content';
--import type { HistoryState } from '../history/commandHistory.js';
--import type { EditorPreferences } from '../viewport/editorPreferences.js';
--export interface AssetUsage {
-- /** How many document objects reference this asset. */
-- count: number;
-- ids: string[];
--}
--export interface AssetBrowserCallbacks {
-- onUseTexture: (name: string) => void;
-- onPlaceModel: (id: string) => void;
-- onFindUsages: (ids: string[]) => void;
-- onDeleteTexture: (name: string) => void;
-- onDeleteModel: (id: string) => void;
-- onRenameTexture: (name: string, next: string) => void;
-- onImportTexture: (file: File) => void;
-- onImportModel: (file: File) => void;
--}
--export interface AssetBrowserState {
-- textures: readonly MapTextureEntry[];
-- models: readonly MapModelV2[];
-- textureUsage: ReadonlyMap<string, AssetUsage>;
-- modelUsage: ReadonlyMap<string, AssetUsage>;
-- filter: string;
-- tab: 'textures' | 'models';
--}
--/**
-- * Deleting a referenced asset is refused rather than done-and-repaired: the
-- * repair would be silently retexturing objects the user did not select.
-- * There is no automatic garbage collection for the same reason.
-- */
--export declare function canDeleteAsset(usage: AssetUsage | undefined): {
-- ok: boolean;
-- reason?: string;
--};
--export declare class AssetBrowser {
-- private readonly root;
-- private readonly callbacks;
-- private state;
-- constructor(root: HTMLElement, callbacks: AssetBrowserCallbacks);
-- setState(next: Partial<AssetBrowserState>): void;
-- private render;
-- private textureItem;
-- private modelItem;
-- private actions;
--}
--export interface HistoryEntryView {
-- label: string;
-- undone: boolean;
--}
--/**
-- * A view of the ONE CommandHistory. It has no stack of its own — a second
-- * undo system is exactly the bug this whole program is about.
-- */
--export declare class HistoryPanel {
-- private readonly root;
-- private readonly onJump;
-- constructor(root: HTMLElement, onJump: (steps: number) => void);
-- render(entries: readonly HistoryEntryView[], state: HistoryState): void;
--}
--export interface StatusState {
-- tool: string | null;
-- selectionCount: number;
-- primary: string | null;
-- transformMode: string;
-- space: string;
-- snap: string;
-- dirty: boolean;
-- revision: string;
-- peers: number;
-- lock: string | null;
-- message: string;
--}
--export declare function statusText(s: StatusState): string;
--export declare class StatusBar {
-- private readonly root;
-- private readonly messageEl;
-- constructor(root: HTMLElement, messageEl: HTMLElement);
-- render(state: StatusState): void;
--}
--export interface ScenePanelCallbacks {
-- onTimeOfDay: (t: number) => void;
-- onPreset: (name: 'morning' | 'noon' | 'sunset' | 'night') => void;
-- onTogglePlay: (on: boolean) => void;
-- onPreferences: (next: EditorPreferences) => void;
--}
--/**
-- * Editor-only preview controls over the SHARED Environment. Deliberately not
-- * a second sky system: what the editor shows must be what players see, so
-- * this drives the same object the game does and stores nothing in the map.
-- */
--export declare class ScenePanel {
-- private readonly root;
-- private readonly callbacks;
-- constructor(root: HTMLElement, callbacks: ScenePanelCallbacks);
-- render(prefs: EditorPreferences, timeOfDay: number, playing: boolean): void;
--}
--//# sourceMappingURL=panels.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/panels.d.ts.map b/apps/client/dist-types/editor/ui/panels.d.ts.map
ddeleted file mode 100644
iindex 1c6fc48..0000000
---- a/apps/client/dist-types/editor/ui/panels.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"panels.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/panels.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAA;AAIzE,MAAM,WAAW,UAAU;IACzB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,EAAE,CAAA;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IACrC,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACrD,eAAe,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IACrC,aAAa,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;CACpC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;IACpC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAA;IAC7B,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAC7C,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAC3C,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,UAAU,GAAG,QAAQ,CAAA;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAM9F;AAED,qBAAa,YAAY;IAWrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAX5B,OAAO,CAAC,KAAK,CAOZ;gBAGkB,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,qBAAqB;IAGnD,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI;IAKhD,OAAO,CAAC,MAAM;IAyDd,OAAO,CAAC,WAAW;IA8BnB,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,OAAO;CA8ChB;AAID,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,OAAO,CAAA;CAChB;AAED;;;GAGG;AACH,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI;IAGlD,MAAM,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI;CA2BxE;AAID,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAejD;AAED,qBAAa,SAAS;IAElB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,WAAW;IAGzC,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;CAIjC;AAID,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAA;IACjE,YAAY,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,IAAI,CAAA;IACnC,aAAa,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAA;CACjD;AAED;;;;GAIG;AACH,qBAAa,UAAU;IAEnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,mBAAmB;IAGjD,MAAM,CAAC,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;CAkE5E"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/panels.test.d.ts b/apps/client/dist-types/editor/ui/panels.test.d.ts
ddeleted file mode 100644
iindex ddae452..0000000
---- a/apps/client/dist-types/editor/ui/panels.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=panels.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/panels.test.d.ts.map b/apps/client/dist-types/editor/ui/panels.test.d.ts.map
ddeleted file mode 100644
iindex 1e2bea1..0000000
---- a/apps/client/dist-types/editor/ui/panels.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"panels.test.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/panels.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/settingsPanel.d.ts b/apps/client/dist-types/editor/ui/settingsPanel.d.ts
ddeleted file mode 100644
iindex 226392d..0000000
---- a/apps/client/dist-types/editor/ui/settingsPanel.d.ts
++++ /dev/null
@@@ -1,19 +0,0 @@
--/**
-- * The keybinding settings panel.
-- *
-- * Self-contained: it owns the bindings map, persists it, and tells the caller
-- * when something changed so tool chips and help text can re-render. Nothing
-- * about the viewport, the document or the tools leaks in here.
-- */
--import { type Binding } from '../bindings.js';
--export interface SettingsPanelDeps {
-- /** The live bindings map, mutated in place so holders stay in sync. */
-- bindings: Record<string, Binding>;
-- bindingOf: (action: string) => Binding;
-- /** Called after any change, so hotkey chips and help text can refresh. */
-- onChanged: () => void;
--}
--export declare function createSettingsPanel(deps: SettingsPanelDeps): {
-- render: () => void;
--};
--//# sourceMappingURL=settingsPanel.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/settingsPanel.d.ts.map b/apps/client/dist-types/editor/ui/settingsPanel.d.ts.map
ddeleted file mode 100644
iindex d4a2137..0000000
---- a/apps/client/dist-types/editor/ui/settingsPanel.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"settingsPanel.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/settingsPanel.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAML,KAAK,OAAO,EACb,MAAM,gBAAgB,CAAA;AAIvB,MAAM,WAAW,iBAAiB;IAChC,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;IACtC,0EAA0E;IAC1E,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG;IAAE,MAAM,EAAE,MAAM,IAAI,CAAA;CAAE,CA4EnF"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/workspace.d.ts b/apps/client/dist-types/editor/ui/workspace.d.ts
ddeleted file mode 100644
iindex 54fe537..0000000
---- a/apps/client/dist-types/editor/ui/workspace.d.ts
++++ /dev/null
@@@ -1,65 +0,0 @@
--/**
-- * The workspace shell: a toolbar, three columns and a dock, all resizable,
-- * collapsible and remembered.
-- *
-- * What it replaces: one fixed 270px sidebar overlaying the canvas, with a
-- * collapse button that hid itself along with the panel. Collapsing produced
-- * horizontal overflow, and the control that would have expanded it again was
-- * inside the thing that had just gone. That class of bug is structural, so
-- * the fix is structural: the collapse control lives in a RAIL that is always
-- * present, and the layout is a grid whose columns can go to zero without the
-- * body ever gaining a scrollbar.
-- *
-- * +---------------------------------------------------+
-- * | Toolbar |
-- * +----------+---------------------------+------------+
-- * | Outliner | Viewport | Inspector |
-- * +----------+---------------------------+------------+
-- * | Assets | Issues | History | Scene | Status |
-- * +---------------------------------------------------+
-- */
--export type PanelId = 'outliner' | 'inspector' | 'dock';
--export interface WorkspaceLayout {
-- outliner: number;
-- inspector: number;
-- dock: number;
-- collapsed: Record<PanelId, boolean>;
-- /** Which dock tab is showing. */
-- dockTab: string;
--}
--export declare const DEFAULT_LAYOUT: WorkspaceLayout;
--export declare function loadLayout(raw: string | null): WorkspaceLayout;
--export interface WorkspaceElements {
-- root: HTMLElement;
-- outliner: HTMLElement;
-- inspector: HTMLElement;
-- dock: HTMLElement;
-- viewport: HTMLElement;
--}
--/**
-- * Drives the CSS grid from the layout. The stylesheet owns appearance;
-- * this owns sizes, collapse state and persistence.
-- */
--export declare class Workspace {
-- private readonly els;
-- private readonly storage;
-- private layout;
-- private readonly listeners;
-- constructor(els: WorkspaceElements, storage?: Pick<Storage, 'getItem' | 'setItem'>);
-- get current(): WorkspaceLayout;
-- isCollapsed(panel: PanelId): boolean;
-- setSize(panel: PanelId, px: number): void;
-- setCollapsed(panel: PanelId, on: boolean): void;
-- toggle(panel: PanelId): void;
-- setDockTab(tab: string): void;
-- get dockTab(): string;
-- subscribe(fn: (l: WorkspaceLayout) => void): () => void;
-- private commit;
-- private apply;
-- /**
-- * Make an element drag a panel's size. `axis` is which way the panel
-- * grows; `invert` is for the inspector, whose left edge grows leftward.
-- */
-- attachResizer(handle: HTMLElement, panel: PanelId, axis: 'x' | 'y', invert?: boolean): () => void;
--}
--//# sourceMappingURL=workspace.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/ui/workspace.d.ts.map b/apps/client/dist-types/editor/ui/workspace.d.ts.map
ddeleted file mode 100644
iindex c3ec367..0000000
---- a/apps/client/dist-types/editor/ui/workspace.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/workspace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,WAAW,GAAG,MAAM,CAAA;AAEvD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IACnC,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAA;CAChB;AAID,eAAO,MAAM,cAAc,EAAE,eAM5B,CAAA;AAMD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,eAAe,CAuB9D;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,WAAW,CAAA;IACrB,SAAS,EAAE,WAAW,CAAA;IACtB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,WAAW,CAAA;CACtB;AAED;;;GAGG;AACH,qBAAa,SAAS;IAKlB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;gBAGjD,GAAG,EAAE,iBAAiB,EACtB,OAAO,GAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAgB;IAM/E,IAAI,OAAO,IAAI,eAAe,CAE7B;IAED,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAIpC,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAazC,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,IAAI;IAK/C,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAI5B,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAO7B,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI;IAKvD,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,KAAK;IAqBb;;;OAGG;IACH,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,GAAG,GAAG,EAAE,MAAM,UAAQ,GAAG,MAAM,IAAI;CAsChG"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/editorCameraController.d.ts b/apps/client/dist-types/editor/viewport/editorCameraController.d.ts
ddeleted file mode 100644
iindex 1f071f9..0000000
---- a/apps/client/dist-types/editor/viewport/editorCameraController.d.ts
++++ /dev/null
@@@ -1,77 +0,0 @@
--/**
-- * Editor navigation.
-- *
-- * Babylon's own camera inputs are cleared: they fight the editor's action
-- * system for the same keys and cannot be remapped, and their wheel handler
-- * dollies along the view axis regardless of what is under the cursor. All
-- * movement comes from here, driven by the centralised bindings.
-- *
-- * Three rules the previous handling broke:
-- * - MMB uses POINTER CAPTURE, so a drag keeps working past the canvas edge
-- * and the browser's middle-click autoscroll never appears.
-- * - Nothing moves while another gesture owns the pointer, so a gizmo drag
-- * cannot also orbit the camera.
-- * - The wheel zooms toward what is UNDER the cursor, not toward the centre
-- * of the screen, and scales its step by distance so approaching a small
-- * object does not overshoot it.
-- */
--import { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { FreeCamera } from '@babylonjs/core/Cameras/freeCamera.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { InteractionController } from '../interaction/interactionController.js';
--export interface CameraControllerOptions {
-- scene: Scene;
-- camera: FreeCamera;
-- canvas: HTMLCanvasElement;
-- interaction: InteractionController;
-- /** True while the named binding's key is held (centralised bindings). */
-- holding: (action: string) => boolean;
-- /**
-- * True when the wheel belongs to something else — a placement preview
-- * rotating, a brush radius. The context is the tool's, not the camera's.
-- */
-- wheelIsClaimed: () => boolean;
--}
--export declare class EditorCameraController {
-- private readonly opts;
-- private freeLook;
-- private mmb;
-- private lastX;
-- private lastY;
-- private readonly disposers;
-- constructor(opts: CameraControllerOptions);
-- private on;
-- dispose(): void;
-- get isFreeLook(): boolean;
-- get isDragging(): boolean;
-- toggleFreeLook(): void;
-- private onPointerDown;
-- private onPointerUp;
-- private onPointerMove;
-- private onMouseMove;
-- /**
-- * Cursor-centric dolly. Picks the point under the cursor and moves along
-- * the ray toward it, so the thing you are looking at stays put and grows —
-- * which is what makes approaching a doorway feel aimed rather than
-- * approximate. Step scales with distance and clamps at both ends.
-- */
-- private onWheel;
-- /** Frame a world-space bounding sphere (F, Outliner focus). */
-- frame(centre: Vector3, radius: number): void;
-- /** Fly step for the movement actions, in world units for this frame. */
-- fly(forward: number, right: number, up: number, speed: number): void;
-- /**
-- * Project a world point to PAGE pixels.
-- *
-- * The canvas fills the viewport grid cell, not the window, so its own
-- * coordinate space starts at the cell's top-left. Anything that wants to
-- * put a cursor or a DOM element at a world position needs page
-- * coordinates, so the offset is added here rather than at each call site
-- * (which is how it went wrong: a projection that is right in canvas space
-- * and used as a page position is silently off by the width of a panel).
-- */
-- worldToScreen(p: Vector3): [number, number];
-- /** Page pixels → canvas pixels, the inverse of `worldToScreen`. */
-- toCanvasSpace(pageX: number, pageY: number): [number, number];
--}
--//# sourceMappingURL=editorCameraController.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/editorCameraController.d.ts.map b/apps/client/dist-types/editor/viewport/editorCameraController.d.ts.map
ddeleted file mode 100644
iindex 0cf8b73..0000000
---- a/apps/client/dist-types/editor/viewport/editorCameraController.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorCameraController.d.ts","sourceRoot":"","sources":["../../../src/editor/viewport/editorCameraController.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAU,OAAO,EAAE,MAAM,sCAAsC,CAAA;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAA;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAErD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAA;AAEpF,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,EAAE,UAAU,CAAA;IAClB,MAAM,EAAE,iBAAiB,CAAA;IACzB,WAAW,EAAE,qBAAqB,CAAA;IAClC,yEAAyE;IACzE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;IACpC;;;OAGG;IACH,cAAc,EAAE,MAAM,OAAO,CAAA;CAC9B;AAOD,qBAAa,sBAAsB;IAOrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANjC,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,GAAG,CAA+B;IAC1C,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;gBAElB,IAAI,EAAE,uBAAuB;IAsB1D,OAAO,CAAC,EAAE;IAUV,OAAO,IAAI,IAAI;IAKf,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,cAAc,IAAI,IAAI;IAKtB,OAAO,CAAC,aAAa;IAoBrB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,WAAW;IAOnB;;;;;OAKG;IACH,OAAO,CAAC,OAAO;IAsBf,+DAA+D;IAC/D,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAO5C,wEAAwE;IACxE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAcpE;;;;;;;;;OASG;IACH,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAa3C,mEAAmE;IACnE,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;CAI9D"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/editorPreferences.d.ts b/apps/client/dist-types/editor/viewport/editorPreferences.d.ts
ddeleted file mode 100644
iindex 8478a10..0000000
---- a/apps/client/dist-types/editor/viewport/editorPreferences.d.ts
++++ /dev/null
@@@ -1,47 +0,0 @@
--/**
-- * Viewport preferences that outlive a session: gizmo coordinate space, snap
-- * increments, grid, and editor-only environment preview settings.
-- *
-- * These are EDITOR state, not map data. Persisting them in localStorage
-- * rather than the document means two people editing the same map can have
-- * different snap settings without fighting over a save.
-- */
--export type TransformSpace = 'world' | 'local';
--export interface SnapSettings {
-- translate: {
-- on: boolean;
-- step: number;
-- };
-- rotate: {
-- on: boolean;
-- step: number;
-- };
-- scale: {
-- on: boolean;
-- step: number;
-- };
--}
--export interface EditorPreferences {
-- space: TransformSpace;
-- snap: SnapSettings;
-- grid: {
-- on: boolean;
-- size: number;
-- };
--}
--export declare const DEFAULT_PREFERENCES: EditorPreferences;
--/** Tolerant of anything in storage: a corrupt value must not break boot. */
--export declare function loadPreferences(raw: string | null): EditorPreferences;
--export declare function savePreferences(prefs: EditorPreferences, storage?: Storage): void;
--export declare function readPreferences(storage?: Storage): EditorPreferences;
--/**
-- * Snap a value to a step. `bypass` is the held modifier — snapping you
-- * cannot temporarily switch off is worse than no snapping, because the one
-- * time you need an exact offset you have to go and change the setting.
-- */
--export declare function snapTo(value: number, step: number, on: boolean, bypass?: boolean): number;
--export declare const snapTranslation: (v: readonly [number, number, number], snap: SnapSettings, bypass: boolean) => [number, number, number];
--/** Rotation snap is authored in degrees and applied in radians. */
--export declare const snapRotationRadians: (radians: number, snap: SnapSettings, bypass: boolean) => number;
--export declare const snapScale: (v: readonly [number, number, number], snap: SnapSettings, bypass: boolean) => [number, number, number];
--//# sourceMappingURL=editorPreferences.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/editorPreferences.d.ts.map b/apps/client/dist-types/editor/viewport/editorPreferences.d.ts.map
ddeleted file mode 100644
iindex 73387f0..0000000
---- a/apps/client/dist-types/editor/viewport/editorPreferences.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorPreferences.d.ts","sourceRoot":"","sources":["../../../src/editor/viewport/editorPreferences.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,CAAA;AAE9C,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACxC,MAAM,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACrC,KAAK,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACrC;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,cAAc,CAAA;IACrB,IAAI,EAAE,YAAY,CAAA;IAClB,IAAI,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACpC;AAID,eAAO,MAAM,mBAAmB,EAAE,iBAUjC,CAAA;AAOD,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,CAoCrE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAMjF;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,iBAAiB,CAMpE;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,UAAQ,GAAG,MAAM,CAGvF;AAED,eAAO,MAAM,eAAe,GAC1B,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,MAAM,YAAY,EAClB,QAAQ,OAAO,KACd,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAIzB,CAAA;AAED,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,GAC9B,SAAS,MAAM,EACf,MAAM,YAAY,EAClB,QAAQ,OAAO,KACd,MAIF,CAAA;AAED,eAAO,MAAM,SAAS,GACpB,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACpC,MAAM,YAAY,EAClB,QAAQ,OAAO,KACd,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAIzB,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/editorPreferences.test.d.ts b/apps/client/dist-types/editor/viewport/editorPreferences.test.d.ts
ddeleted file mode 100644
iindex 7e85fb2..0000000
---- a/apps/client/dist-types/editor/viewport/editorPreferences.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=editorPreferences.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/editorPreferences.test.d.ts.map b/apps/client/dist-types/editor/viewport/editorPreferences.test.d.ts.map
ddeleted file mode 100644
iindex f970ea9..0000000
---- a/apps/client/dist-types/editor/viewport/editorPreferences.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorPreferences.test.d.ts","sourceRoot":"","sources":["../../../src/editor/viewport/editorPreferences.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/editorViewRegistry.d.ts b/apps/client/dist-types/editor/viewport/editorViewRegistry.d.ts
ddeleted file mode 100644
iindex 41e4c7c..0000000
---- a/apps/client/dist-types/editor/viewport/editorViewRegistry.d.ts
++++ /dev/null
@@@ -1,95 +0,0 @@
--/**
-- * The ONE place document objects become Babylon meshes, and the ONE place a
-- * picked mesh becomes a document id.
-- *
-- * Before this the editor kept five parallel `Map<Mesh, T>` tables plus a
-- * `terrainTargets` array, and every one of them had to be kept in step by
-- * hand: `deselect()` cleared some, a remote merge disposed others, undo
-- * rebuilt a mesh and left a third pointing at a disposed one. Meshes are a
-- * PROJECTION — they are rebuilt constantly — so nothing durable may point at
-- * them. Selection, history, locks and the Outliner all hold ids; this maps
-- * between the two and nothing else does.
-- *
-- * A view owns its meshes, materials and editor-only helper geometry, and is
-- * responsible for keeping them in sync with the document object. Destroying
-- * and rebuilding one cannot invalidate identity.
-- */
--import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js';
--import type { Mesh } from '@babylonjs/core/Meshes/mesh.js';
--import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
--import type { EditorDocument, EditorObject, EditorObjectKind } from '../document/editorDocument.js';
--/**
-- * One object's Babylon projection.
-- *
-- * `update` gets the keys that changed so a view can be cheap about it — a
-- * colour change must not rebuild a heightfield. Returning false means "I
-- * cannot apply this incrementally", and the registry rebuilds the view.
-- */
--export interface EditorView {
-- readonly id: string;
-- readonly kind: EditorObjectKind;
-- /** The node the gizmo attaches to and the Outliner frames. */
-- readonly root: TransformNode;
-- /** Meshes that should be pickable/highlightable for this object. */
-- meshes(): Mesh[];
-- /** Apply a document change. False = rebuild me. */
-- update(object: EditorObject, keys: readonly string[]): boolean;
-- /** Editor-only visibility (Outliner eye), not authored data. */
-- setVisible(on: boolean): void;
-- dispose(): void;
--}
--export type ViewFactory = (id: string, kind: EditorObjectKind, object: EditorObject) => EditorView;
--export declare class EditorViewRegistry {
-- private readonly doc;
-- private readonly factory;
-- private readonly views;
-- /** Child mesh (any depth) → owning object id. */
-- private readonly owners;
-- /**
-- * The reverse index. Without it, removing one view scanned every mesh in
-- * the scene to find its own — so deleting a selection of 50 objects on a
-- * 600-object map walked the whole table 50 times.
-- */
-- private readonly ownedMeshes;
-- /**
-- * `allMeshes()` runs on every hover pick. Rebuilding the array from every
-- * view each time allocated a scene-sized array several times a second, so
-- * it is cached and invalidated whenever the mesh set actually changes.
-- */
-- private meshCache;
-- private unsubscribe;
-- constructor(doc: EditorDocument, factory: ViewFactory);
-- /** Build views for everything currently in the document and follow it. */
-- start(): void;
-- stop(): void;
-- get size(): number;
-- viewOf(id: string): EditorView | null;
-- ids(): string[];
-- /** The document id that owns a picked mesh, walking up through children. */
-- ownerOf(mesh: AbstractMesh | null | undefined): string | null;
-- /** Primary node for `id` — what the gizmo rides and the camera frames. */
-- rootOf(id: string): TransformNode | null;
-- meshesOf(id: string): Mesh[];
-- /** Every renderable mesh, for scene-wide passes (hover picking filters). */
-- allMeshes(): Mesh[];
-- /**
-- * Force a view to be rebuilt from scratch — what a view asks for when a
-- * change it cannot apply incrementally arrives (a shape swap, a resolution
-- * change), and what a material rebuild goes through.
-- */
-- rebuild(id: string): void;
-- private apply;
-- private rebuildAll;
-- private create;
-- private index;
-- /** Drop one view's meshes from both indexes — without scanning the rest. */
-- private unindex;
-- private destroy;
-- private disposeAll;
-- /**
-- * Re-index a view's meshes after it changed them itself (a model finishing
-- * an async load, a mesh swapped for a new shape). Cheap and idempotent.
-- */
-- reindex(id: string): void;
--}
--//# sourceMappingURL=editorViewRegistry.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/editorViewRegistry.d.ts.map b/apps/client/dist-types/editor/viewport/editorViewRegistry.d.ts.map
ddeleted file mode 100644
iindex 84bd044..0000000
---- a/apps/client/dist-types/editor/viewport/editorViewRegistry.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorViewRegistry.d.ts","sourceRoot":"","sources":["../../../src/editor/viewport/editorViewRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAA;AAC1E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AAC5E,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAInG;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;IAC/B,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;IAC5B,oEAAoE;IACpE,MAAM,IAAI,IAAI,EAAE,CAAA;IAChB,mDAAmD;IACnD,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAA;IAC9D,gEAAgE;IAChE,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC7B,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,YAAY,KAAK,UAAU,CAAA;AAElG,qBAAa,kBAAkB;IAmB3B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAnB1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,iDAAiD;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;IACzD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;IAChE;;;;OAIG;IACH,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,WAAW,CAA4B;gBAG5B,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,WAAW;IAGvC,0EAA0E;IAC1E,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI;IAMZ,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAIrC,GAAG,IAAI,MAAM,EAAE;IAIf,4EAA4E;IAC5E,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI;IAQ7D,0EAA0E;IAC1E,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAIxC,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE;IAI5B,4EAA4E;IAC5E,SAAS,IAAI,IAAI,EAAE;IAOnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAQzB,OAAO,CAAC,KAAK;IAyCb,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,KAAK;IAiBb,4EAA4E;IAC5E,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,UAAU;IAQlB;;;OAGG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAM1B"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/editorViewRegistry.test.d.ts b/apps/client/dist-types/editor/viewport/editorViewRegistry.test.d.ts
ddeleted file mode 100644
iindex 6e70397..0000000
---- a/apps/client/dist-types/editor/viewport/editorViewRegistry.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=editorViewRegistry.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/editorViewRegistry.test.d.ts.map b/apps/client/dist-types/editor/viewport/editorViewRegistry.test.d.ts.map
ddeleted file mode 100644
iindex 5d794bf..0000000
---- a/apps/client/dist-types/editor/viewport/editorViewRegistry.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorViewRegistry.test.d.ts","sourceRoot":"","sources":["../../../src/editor/viewport/editorViewRegistry.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/gizmoController.d.ts b/apps/client/dist-types/editor/viewport/gizmoController.d.ts
ddeleted file mode 100644
iindex 2ce339f..0000000
---- a/apps/client/dist-types/editor/viewport/gizmoController.d.ts
++++ /dev/null
@@@ -1,64 +0,0 @@
--import type { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { InteractionController } from '../interaction/interactionController.js';
--import type { EditorTransform } from './transformMath.js';
--import type { TransformSpace } from './editorPreferences.js';
--export type GizmoMode = 'move' | 'rotate' | 'scale';
--export interface GizmoControllerOptions {
-- scene: Scene;
-- interaction: InteractionController;
-- /** Begin a transform over these ids; false = do not start the drag. */
-- onDragStart: (mode: GizmoMode) => boolean;
-- onDrag: (pivot: EditorTransform) => void;
-- onDragEnd: () => void;
--}
--export declare class GizmoController {
-- private readonly opts;
-- private readonly manager;
-- private readonly pivot;
-- private readonly wired;
-- private mode;
-- private space;
-- private snap;
-- constructor(opts: GizmoControllerOptions);
-- get active(): boolean;
-- /** The pivot's current pose — what a drag frame reports. */
-- pivotTransform(): EditorTransform;
-- /**
-- * Put the pivot at the group's centre with identity rotation and scale, so
-- * the gizmo reports DELTAS — which is what the group inspector labels them
-- * as, and what makes a group rotation rotate about the group rather than
-- * about whichever member happened to be first.
-- */
-- setPivot(centre: readonly [number, number, number], orientation?: EditorTransform): void;
-- attach(on: boolean): void;
-- setMode(mode: GizmoMode): void;
-- get currentMode(): GizmoMode;
-- /**
-- * World vs local handle orientation. This changes the MATHS, not only the
-- * label: in local space the handles align with the object's own axes, so
-- * dragging "X" on a rotated wall slides along the wall.
-- */
-- setSpace(space: TransformSpace): void;
-- /** Snap increments; 0 disables. Applied to Babylon's own drag steps. */
-- setSnap(translate: number, rotate: number, scale: number): void;
-- private applySnap;
-- /**
-- * A point on the given axis's handle, in screen pixels — what the browser
-- * harness aims a real mouse at.
-- *
-- * All three axis gizmos share the pivot's position, so projecting their
-- * roots gives the same point for X, Y and Z and a "drag the X handle" test
-- * would grab whichever one happened to be on top. Instead this spirals out
-- * from the origin picking the UTILITY LAYER until it lands on geometry
-- * owned by this axis, which works for arrows, planes and rotation rings
-- * alike without assuming where the handle art sits.
-- */
-- handleScreenPoint(axis: 'x' | 'y' | 'z', project: (p: Vector3) => [number, number]): [number, number] | null;
-- get utilityScene(): Scene | null;
-- private wire;
-- /** Frame helper: the pivot's world position as a Vector3. */
-- pivotPosition(): Vector3;
-- dispose(): void;
--}
--//# sourceMappingURL=gizmoController.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/gizmoController.d.ts.map b/apps/client/dist-types/editor/viewport/gizmoController.d.ts.map
ddeleted file mode 100644
iindex 5917f57..0000000
---- a/apps/client/dist-types/editor/viewport/gizmoController.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"gizmoController.d.ts","sourceRoot":"","sources":["../../../src/editor/viewport/gizmoController.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAGnE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAA;AACpF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAE5D,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEnD,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,KAAK,CAAA;IACZ,WAAW,EAAE,qBAAqB,CAAA;IAClC,uEAAuE;IACvE,WAAW,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAA;IACzC,MAAM,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,qBAAa,eAAe;IAQd,OAAO,CAAC,QAAQ,CAAC,IAAI;IAPjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,IAAI,CAAwC;gBAEvB,IAAI,EAAE,sBAAsB;IASzD,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,4DAA4D;IAC5D,cAAc,IAAI,eAAe;IASjC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,CAAC,EAAE,eAAe,GAAG,IAAI;IAexF,MAAM,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;IAKzB,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;IAS9B,IAAI,WAAW,IAAI,SAAS,CAE3B;IAED;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IASrC,wEAAwE;IACxE,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAK/D,OAAO,CAAC,SAAS;IASjB;;;;;;;;;;OAUG;IACH,iBAAiB,CACf,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,EACrB,OAAO,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,GACxC,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAqC1B,IAAI,YAAY,IAAI,KAAK,GAAG,IAAI,CAE/B;IAED,OAAO,CAAC,IAAI;IAiCZ,6DAA6D;IAC7D,aAAa,IAAI,OAAO;IAIxB,OAAO,IAAI,IAAI;CAIhB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/transformMath.d.ts b/apps/client/dist-types/editor/viewport/transformMath.d.ts
ddeleted file mode 100644
iindex a5c697a..0000000
---- a/apps/client/dist-types/editor/viewport/transformMath.d.ts
++++ /dev/null
@@@ -1,59 +0,0 @@
--/**
-- * Canonical transform maths for the editor.
-- *
-- * Every authored object — primitive static, imported model, terrain — has the
-- * same transform shape: position, ORIENTATION AS A QUATERNION, and scale.
-- * Euler triples are a display format only; accumulating rotations through
-- * them is what produced drifting angles and gimbal surprises before.
-- *
-- * Group edits are expressed as ONE delta matrix applied to each member's
-- * IMMUTABLE START transform:
-- *
-- * newWorld = delta × startWorld
-- *
-- * Deriving from the start snapshot (rather than from the current, already
-- * transformed state) is what makes a drag idempotent per frame and makes
-- * cancel an exact restore.
-- */
--import { Matrix } from '@babylonjs/core/Maths/math.vector.js';
--export interface EditorTransform {
-- position: [number, number, number];
-- /** Orientation as a quaternion [x, y, z, w]. */
-- rotation: [number, number, number, number];
-- scale: [number, number, number];
--}
--export declare const IDENTITY_TRANSFORM: EditorTransform;
--export declare const cloneTransform: (t: EditorTransform) => EditorTransform;
--/** Babylon uses YXZ order for its Euler helpers; match it exactly. */
--export declare function transformFromEuler(position: [number, number, number], euler: [number, number, number], scale?: [number, number, number]): EditorTransform;
--/** Degrees for the inspector; radians/quaternions everywhere else. */
--export declare function eulerOf(t: EditorTransform): [number, number, number];
--export declare function composeMatrix(t: EditorTransform): Matrix;
--export declare function decomposeMatrix(m: Matrix): EditorTransform;
--/**
-- * The delta that carries `from` onto `to`, i.e. `delta = to × from⁻¹`.
-- * Used to turn "where the gizmo pivot started / is now" into the single
-- * transform every selected object should receive.
-- */
--export declare function deltaBetween(from: EditorTransform, to: EditorTransform): Matrix;
--/** Apply a world-space delta to a start transform. */
--export declare function applyDelta(start: EditorTransform, delta: Matrix): EditorTransform;
--/**
-- * Apply one pivot delta to every member of a group, always from the members'
-- * START transforms. Passing the same pivot pair twice yields the same result,
-- * which is what stops a drag from compounding frame over frame.
-- */
--export declare function applyGroupDelta(starts: readonly EditorTransform[], pivotStart: EditorTransform, pivotNow: EditorTransform): EditorTransform[];
--/** Centroid of a set of transforms — the default group pivot. */
--export declare function centroidOf(items: readonly EditorTransform[]): [number, number, number];
--/** Reject NaN/Infinity and non-positive scale before they reach the document. */
--export declare function isFiniteTransform(t: EditorTransform): boolean;
--/** Snap a position to a grid; `step <= 0` disables snapping. */
--export declare function snapPosition(p: [number, number, number], step: number): [number, number, number];
--/** Mixed-value detection for the multi-selection inspector. */
--export declare function mixedComponents(items: readonly EditorTransform[]): {
-- position: [boolean, boolean, boolean];
-- scale: [boolean, boolean, boolean];
-- rotation: boolean;
--};
--//# sourceMappingURL=transformMath.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/transformMath.d.ts.map b/apps/client/dist-types/editor/viewport/transformMath.d.ts.map
ddeleted file mode 100644
iindex c1cd5b4..0000000
---- a/apps/client/dist-types/editor/viewport/transformMath.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"transformMath.d.ts","sourceRoot":"","sources":["../../../src/editor/viewport/transformMath.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,MAAM,EAAuB,MAAM,sCAAsC,CAAA;AAElF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,gDAAgD;IAChD,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC1C,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,eAAO,MAAM,kBAAkB,EAAE,eAIhC,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,GAAG,eAAe,KAAG,eAIlD,CAAA;AAEF,sEAAsE;AACtE,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAClC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAC/B,KAAK,GAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAa,GAC1C,eAAe,CAGjB;AAED,sEAAsE;AACtE,wBAAgB,OAAO,CAAC,CAAC,EAAE,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAQpE;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAMxD;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,eAAe,CAM1D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,eAAe,EAAE,EAAE,EAAE,eAAe,GAAG,MAAM,CAE/E;AAED,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,CAEjF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,UAAU,EAAE,eAAe,EAC3B,QAAQ,EAAE,eAAe,GACxB,eAAe,EAAE,CAGnB;AAED,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAYtF;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,OAAO,CAI7D;AAED,gEAAgE;AAChE,wBAAgB,YAAY,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAOhG;AAED,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,GAAG;IAClE,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACrC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IAClC,QAAQ,EAAE,OAAO,CAAA;CAClB,CAYA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/transformMath.test.d.ts b/apps/client/dist-types/editor/viewport/transformMath.test.d.ts
ddeleted file mode 100644
iindex 638df80..0000000
---- a/apps/client/dist-types/editor/viewport/transformMath.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=transformMath.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/transformMath.test.d.ts.map b/apps/client/dist-types/editor/viewport/transformMath.test.d.ts.map
ddeleted file mode 100644
iindex 64d56d6..0000000
---- a/apps/client/dist-types/editor/viewport/transformMath.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"transformMath.test.d.ts","sourceRoot":"","sources":["../../../src/editor/viewport/transformMath.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/transformSession.d.ts b/apps/client/dist-types/editor/viewport/transformSession.d.ts
ddeleted file mode 100644
iindex 7577cdd..0000000
---- a/apps/client/dist-types/editor/viewport/transformSession.d.ts
++++ /dev/null
@@@ -1,72 +0,0 @@
--/**
-- * One transform gesture, from grab to commit.
-- *
-- * Replaces the previous workflow of parenting heterogeneous Babylon meshes to
-- * a pivot TransformNode, dragging it, unparenting, and then baking each object
-- * type back by hand in `bakeMulti()` — which had a separate branch per kind,
-- * silently skipped scale for anything but statics, and read the CURRENT mesh
-- * pose (already transformed) as its source.
-- *
-- * Here the session owns immutable START snapshots and recomputes every member
-- * from them on each update, so a drag cannot compound, cancel is exact, and
-- * the whole gesture commits as a single history entry.
-- */
--import { type EditorTransform } from './transformMath.js';
--import type { CommandHistory } from '../history/commandHistory.js';
--/** How the session reads and writes an object's canonical transform. */
--export interface TransformAccessor {
-- get(id: string): EditorTransform | null;
-- set(id: string, t: EditorTransform): void;
--}
--export type TransformMode = 'move' | 'rotate' | 'scale';
--export interface TransformSessionOptions {
-- /** Called before the session starts; false aborts (e.g. lock denied). */
-- acquireLocks?: (ids: string[]) => boolean;
-- /** Objects that may be moved but not written (read-only members). */
-- isWritable?: (id: string) => boolean;
-- label?: string;
--}
--export interface StartedSession {
-- ids: string[];
-- pivotStart: EditorTransform;
--}
--export declare class TransformSession<TDoc = unknown> {
-- private readonly accessor;
-- private readonly history;
-- private _ids;
-- private _starts;
-- private _pivotStart;
-- private _active;
-- private _label;
-- constructor(accessor: TransformAccessor, history: CommandHistory<TDoc>);
-- get active(): boolean;
-- get ids(): readonly string[];
-- get pivotStart(): EditorTransform | null;
-- /**
-- * Begin a gesture over `ids`. Returns null when nothing is transformable or
-- * a required lock is refused — in that case NOTHING has been mutated and no
-- * transaction is open, so the selection simply stays put.
-- */
-- begin(ids: readonly string[], mode: TransformMode, opts?: TransformSessionOptions): StartedSession | null;
-- /**
-- * Live preview: recompute every member from its START transform using the
-- * pivot's current pose. Safe to call every frame.
-- */
-- update(pivotNow: EditorTransform): void;
-- /** Directly set one member (numeric inspector entry during a session). */
-- setOne(id: string, t: EditorTransform): void;
-- /**
-- * Finish: record ONE command carrying the start→end transforms of every
-- * member. Returns false when nothing actually changed, in which case no
-- * history entry is produced (a click on a handle is not an edit).
-- */
-- commit(): boolean;
-- /**
-- * Abort: restore every member to its exact start snapshot and record
-- * nothing. Used by Escape, a lost lock lease and a disconnect.
-- */
-- cancel(): void;
-- private _reset;
--}
--export declare function sameTransform(a: EditorTransform, b: EditorTransform): boolean;
--//# sourceMappingURL=transformSession.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/transformSession.d.ts.map b/apps/client/dist-types/editor/viewport/transformSession.d.ts.map
ddeleted file mode 100644
iindex f17023a..0000000
---- a/apps/client/dist-types/editor/viewport/transformSession.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"transformSession.d.ts","sourceRoot":"","sources":["../../../src/editor/viewport/transformSession.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,8BAA8B,CAAA;AAEjF,wEAAwE;AACxE,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAAA;IACvC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,eAAe,GAAG,IAAI,CAAA;CAC1C;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEvD,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAA;IACzC,qEAAqE;IACrE,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;IACpC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,EAAE,CAAA;IACb,UAAU,EAAE,eAAe,CAAA;CAC5B;AAED,qBAAa,gBAAgB,CAAC,IAAI,GAAG,OAAO;IAQxC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,MAAM,CAAc;gBAGT,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAGhD,IAAI,MAAM,IAAI,OAAO,CAEpB;IACD,IAAI,GAAG,IAAI,SAAS,MAAM,EAAE,CAE3B;IACD,IAAI,UAAU,IAAI,eAAe,GAAG,IAAI,CAEvC;IAED;;;;OAIG;IACH,KAAK,CACH,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,IAAI,EAAE,aAAa,EACnB,IAAI,GAAE,uBAA4B,GACjC,cAAc,GAAG,IAAI;IA+BxB;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAYvC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,eAAe,GAAG,IAAI;IAK5C;;;;OAIG;IACH,MAAM,IAAI,OAAO;IA8BjB;;;OAGG;IACH,MAAM,IAAI,IAAI;IAQd,OAAO,CAAC,MAAM;CAMf;AAGD,wBAAgB,aAAa,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,eAAe,GAAG,OAAO,CAM7E"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/transformSession.test.d.ts b/apps/client/dist-types/editor/viewport/transformSession.test.d.ts
ddeleted file mode 100644
iindex ad71432..0000000
---- a/apps/client/dist-types/editor/viewport/transformSession.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=transformSession.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/transformSession.test.d.ts.map b/apps/client/dist-types/editor/viewport/transformSession.test.d.ts.map
ddeleted file mode 100644
iindex 62e2f3c..0000000
---- a/apps/client/dist-types/editor/viewport/transformSession.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"transformSession.test.d.ts","sourceRoot":"","sources":["../../../src/editor/viewport/transformSession.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/views/index.d.ts b/apps/client/dist-types/editor/viewport/views/index.d.ts
ddeleted file mode 100644
iindex 275b439..0000000
---- a/apps/client/dist-types/editor/viewport/views/index.d.ts
++++ /dev/null
@@@ -1,71 +0,0 @@
--import { Quaternion } from '@babylonjs/core/Maths/math.vector.js';
--import { Mesh } from '@babylonjs/core/Meshes/mesh.js';
--import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import { type ContentRegistry, type MapLightV2, type SurfaceMaterialData, type TerrainObjectV2 } from '@openvibe/content';
--import { LayeredSurfaceMaterial } from '../../../render/layeredSurface.js';
--import type { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture.js';
--import type { ModelCache } from '../../../render/modelCache.js';
--import type { EditorObject, EditorObjectKind } from '../../document/editorDocument.js';
--import type { EditorView } from '../editorViewRegistry.js';
--/** Everything a view needs that is not the object itself. */
--export interface ViewContext {
-- scene: Scene;
-- /**
-- * The live mask for a surface, created on demand. Views ask for one
-- * rather than owning it: a box has six independently paintable faces and
-- * a model has one per slot, so a mask belongs to a SURFACE.
-- */
-- maskFor: (ownerId: string, surfaceId: string, data: SurfaceMaterialData) => DynamicTexture;
-- content: ContentRegistry;
-- modelCache: ModelCache;
-- /** Shared wireframe material for terrain sculpt overlays. */
-- wireMat: StandardMaterial;
-- /** Highest authored terrain at (x, z); 0 where none covers it. */
-- sampleGround: (x: number, z: number) => number;
-- /** glb source for an imported model id, or null. */
-- modelSource: (id: string) => string | null;
-- /** Ask for a rebuild after async work changed the mesh set. */
-- reindex: (id: string) => void;
-- newId: (prefix: string) => string;
--}
--export declare const lightQuat: (l: MapLightV2) => Quaternion;
--/**
-- * A terrain's heights live in the document as base64 and in the view as a
-- * decoded `Float32Array`. The decoded copy is WORKING STATE for sculpting —
-- * decoding 260 000 floats per brush dab is not viable — and the view is the
-- * only thing that holds it. `flushHeights()` is the one path back to the
-- * document, so the two cannot silently disagree.
-- */
--export declare class TerrainView implements EditorView {
-- readonly id: string;
-- private readonly ctx;
-- readonly kind: "terrain";
-- readonly root: Mesh;
-- readonly wire: Mesh;
-- heights: Float32Array;
-- sub: number;
-- halfExtent: number;
-- surface: LayeredSurfaceMaterial;
-- private maskTexture;
-- private terrain;
-- constructor(id: string, terrain: TerrainObjectV2, ctx: ViewContext);
-- /** The document surface, defaulted so a plain terrain still paints. */
-- surfaceData(): SurfaceMaterialData;
-- private applyPose;
-- /** Push the working heightfield into the mesh (and its wire overlay). */
-- refreshHeights(): void;
-- /** The encoded heights, for writing back to the document. */
-- encodedHeights(): string;
-- /** Re-read the material after a base/layer change. */
-- refreshSurface(data: SurfaceMaterialData): void;
-- setWireVisible(on: boolean): void;
-- wireVisible(): boolean;
-- meshes(): Mesh[];
-- update(object: EditorObject, keys: readonly string[]): boolean;
-- setVisible(on: boolean): void;
-- dispose(): void;
--}
--/** The factory the registry uses. */
--export declare function createViewFactory(ctx: ViewContext): (id: string, kind: EditorObjectKind, object: EditorObject) => EditorView;
--//# sourceMappingURL=index.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/editor/viewport/views/index.d.ts.map b/apps/client/dist-types/editor/viewport/views/index.d.ts.map
ddeleted file mode 100644
iindex 45e44a1..0000000
---- a/apps/client/dist-types/editor/viewport/views/index.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/editor/viewport/views/index.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAW,MAAM,sCAAsC,CAAA;AAI1E,OAAO,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAA;AAGrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+CAA+C,CAAA;AAChF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAErD,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,UAAU,EAKf,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAA;AAI1B,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAA;AAC1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sDAAsD,CAAA;AAI1F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAA;AACtF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAE1D,6DAA6D;AAC7D,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,KAAK,CAAA;IACZ;;;;OAIG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,KAAK,cAAc,CAAA;IAC1F,OAAO,EAAE,eAAe,CAAA;IACxB,UAAU,EAAE,UAAU,CAAA;IACtB,6DAA6D;IAC7D,OAAO,EAAE,gBAAgB,CAAA;IACzB,kEAAkE;IAClE,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9C,oDAAoD;IACpD,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;IAC1C,+DAA+D;IAC/D,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC;AAsQD,eAAO,MAAM,SAAS,GAAI,GAAG,UAAU,KAAG,UAOf,CAAA;AAoL3B;;;;;;GAMG;AACH,qBAAa,WAAY,YAAW,UAAU;IAY1C,QAAQ,CAAC,EAAE,EAAE,MAAM;IAEnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAbtB,QAAQ,CAAC,IAAI,EAAG,SAAS,CAAS;IAClC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;IACnB,OAAO,EAAE,YAAY,CAAA;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,sBAAsB,CAAA;IAC/B,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,OAAO,CAAiB;gBAGrB,EAAE,EAAE,MAAM,EACnB,OAAO,EAAE,eAAe,EACP,GAAG,EAAE,WAAW;IA0CnC,uEAAuE;IACvE,WAAW,IAAI,mBAAmB;IAalC,OAAO,CAAC,SAAS;IAKjB,yEAAyE;IACzE,cAAc,IAAI,IAAI;IAgBtB,6DAA6D;IAC7D,cAAc,IAAI,MAAM;IAIxB,sDAAsD;IACtD,cAAc,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI;IAK/C,cAAc,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;IAIjC,WAAW,IAAI,OAAO;IAItB,MAAM,IAAI,IAAI,EAAE;IAIhB,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO;IAgB9D,UAAU,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;IAI7B,OAAO,IAAI,IAAI;CAMhB;AAED,qCAAqC;AACrC,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,WAAW,IACxC,IAAI,MAAM,EAAE,MAAM,gBAAgB,EAAE,QAAQ,YAAY,KAAG,UAAU,CAkB9E"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/game/interactionController.d.ts b/apps/client/dist-types/game/interactionController.d.ts
ddeleted file mode 100644
iindex 2617d7c..0000000
---- a/apps/client/dist-types/game/interactionController.d.ts
++++ /dev/null
@@@ -1,100 +0,0 @@
--import { type PhysicsWorld } from '@openvibe/physics';
--import { type ContentRegistry } from '@openvibe/content';
--import type { Connection } from '../net/connection.js';
--import type { EntityView } from '../render/entityView.js';
--import type { ClientState } from '../state/clientState.js';
--import type { InputAction, InputTracker } from '../input/inputTracker.js';
--import type { LocalPlayer } from './localPlayer.js';
--import type { WeaponSettings } from '../weapons/registry.js';
--export interface AimTarget {
-- entityId: string;
-- kind: 'prop' | 'resource' | 'player' | 'npc';
-- def: string | undefined;
-- frozen: boolean;
-- point: {
-- x: number;
-- y: number;
-- z: number;
-- };
-- /** Surface normal at the hit (rigging tool takes joint axes from it). */
-- normal: {
-- x: number;
-- y: number;
-- z: number;
-- };
--}
--/** First endpoint picked with the rigging tool (awaiting the second). */
--export interface RiggingPick {
-- entityId: string;
-- point: {
-- x: number;
-- y: number;
-- z: number;
-- };
--}
--export declare class InteractionController {
-- private readonly physics;
-- private readonly player;
-- private readonly view;
-- private readonly state;
-- private readonly content;
-- private readonly connection;
-- private readonly input;
-- private readonly weaponSettings;
-- physgunActive: boolean;
-- /** Hold-E rotate mode while carrying (mouse steers the prop, not the view). */
-- rotating: boolean;
-- /** Cosmetic hook: a swing was sent (viewmodel + body animation). */
-- onSwing: (() => void) | null;
-- /** True when the last 'use' this client sent targeted another player. */
-- lastTargetWasPlayer: boolean;
-- /** Hook: player pressed E on a trading post (entity id passed along). */
-- onShopOpen: ((targetId: string) => void) | null;
-- /** Rigging tool: first selected endpoint (highlight + prompt read this). */
-- riggingFirst: RiggingPick | null;
-- /** Ghost preview pose supplier (wired by main; null = no valid ghost). */
-- placementPose: (() => {
-- x: number;
-- y: number;
-- z: number;
-- yaw: number;
-- } | null) | null;
-- /** Wheel rotates the placement ghost while a placeable is equipped. */
-- onPlacementRotate: ((delta: number) => void) | null;
-- private lastSwingMs;
-- private lastFireMs;
-- private pendingRotate;
-- private gridOn;
-- constructor(physics: PhysicsWorld, player: LocalPlayer, view: EntityView, state: ClientState, content: ContentRegistry, connection: Connection, input: InputTracker, weaponSettings: WeaponSettings);
-- /** Chassis entity id currently driven (server-authoritative). */
-- drivingId(): string | null;
-- equippedToolKind(): 'physgun' | 'axe' | 'pickaxe' | 'rigging' | null;
-- /** What the crosshair points at right now (client-side, UX only). */
-- aim(): AimTarget | null;
-- /**
-- * Where the beam visually ends right now: first surface (world or prop)
-- * under the crosshair, else max range. The beam always fires — hitting
-- * nothing is not an error, it just shines (GMod).
-- */
-- beamTarget(out: {
-- x: number;
-- y: number;
-- z: number;
-- }): void;
-- handle(action: InputAction): void;
-- private endCarry;
-- /** Called once per fixed tick: flush coalesced rotate + grid-lock state. */
-- flushTick(): void;
-- onWheel(delta: number): void;
-- onHotbarChanged(): void;
-- /**
-- * Rigging tool LMB: first click marks an endpoint, second click sends the
-- * constraint request built from the equipment-panel settings. The server
-- * re-validates everything (points, ownership, skill, materials, zone).
-- */
-- private riggingPick;
-- private swing;
-- /** Standing in water (thirst refill by drinking). */
-- standingInWater(): boolean;
--}
--//# sourceMappingURL=interactionController.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/game/interactionController.d.ts.map b/apps/client/dist-types/game/interactionController.d.ts.map
ddeleted file mode 100644
iindex 0876ec8..0000000
---- a/apps/client/dist-types/game/interactionController.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"interactionController.d.ts","sourceRoot":"","sources":["../../src/game/interactionController.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/game/localPlayer.d.ts b/apps/client/dist-types/game/localPlayer.d.ts
ddeleted file mode 100644
iindex c63dcc7..0000000
---- a/apps/client/dist-types/game/localPlayer.d.ts
++++ /dev/null
@@@ -1,56 +0,0 @@
--import { UniversalCamera } from '@babylonjs/core/Cameras/universalCamera.js';
--import { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import { type PlayerMoveState } from '@openvibe/gameplay';
--import { type PhysicsWorld } from '@openvibe/physics';
--import type { ServerSnapshot } from '@openvibe/protocol';
--import type { Connection } from '../net/connection.js';
--import type { InputTracker } from '../input/inputTracker.js';
--import type { ClientState } from '../state/clientState.js';
--export declare class LocalPlayer {
-- private readonly physics;
-- private readonly input;
-- private readonly connection;
-- private readonly state;
-- readonly camera: UniversalCamera;
-- readonly move: PlayerMoveState;
-- private readonly queries;
-- private pending;
-- private seq;
-- /** Previous/current tick positions for render interpolation. */
-- private prevPos;
-- private currPos;
-- /** Interpolated render position (capsule center), updated each frame. */
-- readonly renderPos: Vector3;
-- /** Smoothed eye height so stance changes glide instead of popping. */
-- private eyeSmooth;
-- /** Reconciliation error, blended away over ~100ms instead of snapping —
-- * this is what makes standing on moving props watchable. */
-- private readonly corr;
-- /** Vertical step-up smoothing: stairs move the FEET instantly but the
-- * EYES glide (Source's smoothed stair climb). */
-- private stepOffset;
-- constructor(scene: Scene, physics: PhysicsWorld, input: InputTracker, connection: Connection, state: ClientState, spawn: {
-- x: number;
-- y: number;
-- z: number;
-- });
-- /** One fixed simulation tick: capture, send, predict. */
-- fixedUpdate(): void;
-- private applyInput;
-- /** Reconcile against an authoritative snapshot. */
-- /** Vehicle being driven (server-authoritative; disables prediction). */
-- driving: string | null;
-- onSnapshot(snap: ServerSnapshot): void;
-- /** Per-frame: camera follows interpolated predicted position. */
-- frameUpdate(alpha: number, dt: number): void;
-- get viewYaw(): number;
-- get viewPitch(): number;
-- get eye(): Vector3;
-- viewDir(out: {
-- x: number;
-- y: number;
-- z: number;
-- }): void;
--}
--//# sourceMappingURL=localPlayer.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/game/localPlayer.d.ts.map b/apps/client/dist-types/game/localPlayer.d.ts.map
ddeleted file mode 100644
iindex 2cbdd52..0000000
---- a/apps/client/dist-types/game/localPlayer.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"localPlayer.d.ts","sourceRoot":"","sources":["../../src/game/localPlayer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAA;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAQL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrE,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAe1D,qBAAa,WAAW;IAsBpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK;IAxBxB,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,OAAO,CAAoB;IACnC,OAAO,CAAC,GAAG,CAAI;IACf,gEAAgE;IAChE,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,OAAO,CAAgB;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,SAAS,UAAgB;IAClC,sEAAsE;IACtE,OAAO,CAAC,SAAS,CAA6B;IAC9C;gEAC4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IACrC;qDACiD;IACjD,OAAO,CAAC,UAAU,CAAI;gBAGpB,KAAK,EAAE,KAAK,EACK,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,YAAY,EACnB,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,WAAW,EACnC,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE;IA8B5C,yDAAyD;IACzD,WAAW,IAAI,IAAI;IAqCnB,OAAO,CAAC,UAAU;IAWlB,mDAAmD;IACnD,wEAAwE;IACxE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAO;IAE7B,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAmDtC,iEAAiE;IACjE,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAc5C,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,IAAI,GAAG,IAAI,OAAO,CAEjB;IAED,OAAO,CAAC,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAMxD"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/game/placementGhost.d.ts b/apps/client/dist-types/game/placementGhost.d.ts
ddeleted file mode 100644
iindex 33a1809..0000000
---- a/apps/client/dist-types/game/placementGhost.d.ts
++++ /dev/null
@@@ -1,49 +0,0 @@
--import type { Scene } from '@babylonjs/core/scene.js';
--import { type PhysicsWorld } from '@openvibe/physics';
--import { type ContentRegistry } from '@openvibe/content';
--/**
-- * Placement preview: a translucent ghost of the equipped placeable item,
-- * following the crosshair onto world surfaces. Green = the server should
-- * accept; red = zone forbids building here. Purely client-side comfort —
-- * the placement itself goes through the `place` message and the server
-- * re-validates everything.
-- */
--export interface GhostPose {
-- x: number;
-- y: number;
-- z: number;
-- yaw: number;
--}
--declare const _dir: import("@openvibe/shared").Vec3;
--export declare class PlacementGhost {
-- private readonly scene;
-- private readonly physics;
-- private readonly content;
-- private mesh;
-- private meshDef;
-- private readonly okMat;
-- private readonly badMat;
-- private readonly zones;
-- /** Extra yaw applied by the player (wheel), relative to their facing. */
-- private spin;
-- /** Current pose when visible + buildable; null otherwise. */
-- pose: GhostPose | null;
-- /** Pose is valid to send (zone allows building). */
-- valid: boolean;
-- constructor(scene: Scene, physics: PhysicsWorld, content: ContentRegistry);
-- /** Map edits can change zones live. */
-- refreshZones(): void;
-- rotate(delta: number): void;
-- /**
-- * Per-frame: shows the ghost for the equipped placeable def (or hides it
-- * for null). Shift snaps position to the item's snapStep grid.
-- */
-- update(defId: string | null, eye: {
-- x: number;
-- y: number;
-- z: number;
-- }, viewDir: (out: typeof _dir) => void, playerYaw: number, snap: boolean): void;
-- hide(): void;
--}
--export {};
--//# sourceMappingURL=placementGhost.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/game/placementGhost.d.ts.map b/apps/client/dist-types/game/placementGhost.d.ts.map
ddeleted file mode 100644
iindex c666b91..0000000
---- a/apps/client/dist-types/game/placementGhost.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"placementGhost.d.ts","sourceRoot":"","sources":["../../src/game/placementGhost.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,EAAiC,KAAK,eAAe,EAAmB,MAAM,mBAAmB,CAAA;AAIxG;;;;;;GAMG;AAEH,MAAM,WAAW,SAAS;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,GAAG,EAAE,MAAM,CAAA;CACZ;AAGD,QAAA,MAAM,IAAI,iCAAS,CAAA;AAGnB,qBAAa,cAAc;IAcvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAf1B,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IACjC,yEAAyE;IACzE,OAAO,CAAC,IAAI,CAAI;IAChB,6DAA6D;IAC7D,IAAI,EAAE,SAAS,GAAG,IAAI,CAAO;IAC7B,oDAAoD;IACpD,KAAK,UAAQ;gBAGM,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,eAAe;IAc3C,uCAAuC;IACvC,YAAY,IAAI,IAAI;IAIpB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI3B;;;OAGG;IACH,MAAM,CACJ,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EACxC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,IAAI,KAAK,IAAI,EACnC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,OAAO,GACZ,IAAI;IAgDP,IAAI,IAAI,IAAI;CAOb"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/game/staticPhysics.d.ts b/apps/client/dist-types/game/staticPhysics.d.ts
ddeleted file mode 100644
iindex cb7a5e8..0000000
---- a/apps/client/dist-types/game/staticPhysics.d.ts
++++ /dev/null
@@@ -1,12 +0,0 @@
--import { type ContentRegistry } from '@openvibe/content';
--import { type PhysicsWorld } from '@openvibe/physics';
--/** Reconcile prediction terrain collision, touching only what changed. */
--export declare function reconcilePatchPhysics(physics: PhysicsWorld): void;
--/** How many terrain colliders prediction currently holds (tests). */
--export declare const patchBodyCount: () => number;
--export declare function buildStaticPhysics(physics: PhysicsWorld, content: ContentRegistry): void;
--export declare function reconcileMapStaticPhysics(physics: PhysicsWorld): void;
--export declare const mapStaticBodyCount: () => number;
--/** Live map edit: swap the prediction terrain body for the new grid. */
--export declare function rebuildTerrainPhysics(physics: PhysicsWorld, content: ContentRegistry): void;
--//# sourceMappingURL=staticPhysics.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/game/staticPhysics.d.ts.map b/apps/client/dist-types/game/staticPhysics.d.ts.map
ddeleted file mode 100644
iindex 3367d08..0000000
---- a/apps/client/dist-types/game/staticPhysics.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"staticPhysics.d.ts","sourceRoot":"","sources":["../../src/game/staticPhysics.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,eAAe,EAMrB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAA+B,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAqDlF,0EAA0E;AAC1E,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAcjE;AAED,qEAAqE;AACrE,eAAO,MAAM,cAAc,QAAO,MAA0B,CAAA;AAE5D,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CA8BxF;AAiCD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAiBrE;AAED,eAAO,MAAM,kBAAkB,QAAO,MAA8B,CAAA;AAIpE,wEAAwE;AACxE,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAK3F"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/input/inputTracker.d.ts b/apps/client/dist-types/input/inputTracker.d.ts
ddeleted file mode 100644
iindex 692512a..0000000
---- a/apps/client/dist-types/input/inputTracker.d.ts
++++ /dev/null
@@@ -1,77 +0,0 @@
--/**
-- * Raw input capture: pointer lock, mouse look, key states, wheel, and
-- * edge-triggered action callbacks. Gameplay semantics live elsewhere —
-- * this module only reports what the hands are doing.
-- *
-- * Pointer events are bound on `document` (not the canvas): while the
-- * pointer is locked some browsers retarget events inconsistently, and a
-- * document listener sees them regardless.
-- */
--export declare class InputTracker {
-- private readonly canvas;
-- yaw: number;
-- pitch: number;
-- sensitivity: number;
-- private keys;
-- private locked;
-- /** UI-open state suppresses look/keys feeding the simulation. */
-- uiCapture: boolean;
-- onAction: ((action: InputAction) => void) | null;
-- onWheel: ((delta: number) => void) | null;
-- /** When this returns true, mouse motion is redirected to rotate_held. */
-- captureLook: (() => boolean) | null;
-- private lookDx;
-- private lookDy;
-- /** Last seen mouse `buttons` bitmask (chorded press/release detection). */
-- private buttonsState;
-- /** Emits press/release actions from `buttons` bitmask transitions. */
-- private diffButtons;
-- constructor(canvas: HTMLCanvasElement);
-- /** Accumulated look deltas since the last call (viewmodel sway). */
-- consumeLookDelta(): {
-- dx: number;
-- dy: number;
-- };
-- keyDown(code: string): boolean;
-- get shiftHeld(): boolean;
-- get pointerLocked(): boolean;
-- exitLock(): void;
-- /** Re-engage pointer lock (used when closing menus — needs a user gesture). */
-- requestLock(): void;
-- /** Test harness only: headless browsers cannot pointer-lock. */
-- debugForceLock(): void;
--}
--export type InputAction = {
-- kind: 'use_down';
--} | {
-- kind: 'use_up';
--} | {
-- kind: 'drop';
--} | {
-- kind: 'toggle_menu';
--} | {
-- kind: 'primary_down';
--} | {
-- kind: 'primary_up';
--} | {
-- kind: 'rmb_down';
--} | {
-- kind: 'hotbar1';
--} | {
-- kind: 'hotbar2';
--} | {
-- kind: 'hotbar3';
--} | {
-- kind: 'hotbar4';
--} | {
-- kind: 'hotbar5';
--} | {
-- kind: 'hotbar6';
--} | {
-- kind: 'reload';
--} | {
-- kind: 'rotate_held';
-- dyaw: number;
-- dpitch: number;
--};
--//# sourceMappingURL=inputTracker.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/input/inputTracker.d.ts.map b/apps/client/dist-types/input/inputTracker.d.ts.map
ddeleted file mode 100644
iindex 4e3f68a..0000000
---- a/apps/client/dist-types/input/inputTracker.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"inputTracker.d.ts","sourceRoot":"","sources":["../../src/input/inputTracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,qBAAa,YAAY;IAgCX,OAAO,CAAC,QAAQ,CAAC,MAAM;IA/BnC,GAAG,SAAI;IACP,KAAK,SAAI;IACT,WAAW,SAAS;IACpB,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,MAAM,CAAQ;IACtB,iEAAiE;IACjE,SAAS,UAAQ;IAEjB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,CAAO;IACvD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAAO;IAChD,yEAAyE;IACzE,WAAW,EAAE,CAAC,MAAM,OAAO,CAAC,GAAG,IAAI,CAAO;IAC1C,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,MAAM,CAAI;IAClB,2EAA2E;IAC3E,OAAO,CAAC,YAAY,CAAI;IAExB,sEAAsE;IACtE,OAAO,CAAC,WAAW;gBAaU,MAAM,EAAE,iBAAiB;IAkGtD,oEAAoE;IACpE,gBAAgB,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE;IAO9C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI9B,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED,QAAQ,IAAI,IAAI;IAIhB,+EAA+E;IAC/E,WAAW,IAAI,IAAI;IAInB,gEAAgE;IAChE,cAAc,IAAI,IAAI;CAGvB;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,GACvB;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,GACxB;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,GACtB;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/main.d.ts b/apps/client/dist-types/main.d.ts
ddeleted file mode 100644
iindex 371115b..0000000
---- a/apps/client/dist-types/main.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=main.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/main.d.ts.map b/apps/client/dist-types/main.d.ts.map
ddeleted file mode 100644
iindex 28b87d8..0000000
---- a/apps/client/dist-types/main.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/net/connection.d.ts b/apps/client/dist-types/net/connection.d.ts
ddeleted file mode 100644
iindex f2d5e94..0000000
---- a/apps/client/dist-types/net/connection.d.ts
++++ /dev/null
@@@ -1,26 +0,0 @@
--import { type Appearance, type ClientMessage, type ServerMessage } from '@openvibe/protocol';
--/**
-- * WebSocket connection to the dedicated server. Message handling is a
-- * callback so the network layer stays independent of game/state code.
-- */
--export declare class Connection {
-- private ws;
-- onMessage: ((msg: ServerMessage) => void) | null;
-- onClose: (() => void) | null;
-- connect(url: string, token: string, name: string, appearance: Appearance, slot?: number, auth?: string): Promise<void>;
-- send(msg: ClientMessage): void;
-- get open(): boolean;
--}
--/** Persistent anonymous identity (interim auth — see ADR-0004). */
--/**
-- * Guest identity token: localStorage primary, a long-lived cookie as
-- * backup (survives localStorage wipes), and the server additionally maps
-- * the token to the client IP — three chances to keep a guest's scrapper.
-- */
--export declare function getIdentity(): {
-- token: string;
-- name: string | null;
--};
--export declare function saveName(name: string): void;
--export declare function gameSocketUrl(): string;
--//# sourceMappingURL=connection.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/net/connection.d.ts.map b/apps/client/dist-types/net/connection.d.ts.map
ddeleted file mode 100644
iindex e7bbd84..0000000
---- a/apps/client/dist-types/net/connection.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/net/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAA;AAE3B;;;GAGG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAAyB;IACnC,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,CAAC,GAAG,IAAI,CAAO;IACvD,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAO;IAE7B,OAAO,CACX,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,UAAU,EACtB,IAAI,SAAI,EACR,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC;IAuBhB,IAAI,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI;IAM9B,IAAI,IAAI,IAAI,OAAO,CAElB;CACF;AAED,mEAAmE;AACnE;;;;GAIG;AACH,wBAAgB,WAAW,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAYpE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE3C;AAED,wBAAgB,aAAa,IAAI,MAAM,CAGtC"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/animator.d.ts b/apps/client/dist-types/render/avatar/animator.d.ts
ddeleted file mode 100644
iindex c64fc6b..0000000
---- a/apps/client/dist-types/render/avatar/animator.d.ts
++++ /dev/null
@@@ -1,41 +0,0 @@
--import type { RigJoints } from './rig.js';
--/**
-- * Procedural animation for the avatar rig — no baked clips, so every state
-- * blends smoothly into every other by construction: each frame computes a
-- * TARGET pose (walk/run cycle, idle sway, airborne, tool aim) and the live
-- * pose exponentially damps toward it. Locomotion phase advances with actual
-- * horizontal speed, so foot cadence always matches the (predicted or
-- * interpolated) motion driving it.
-- */
--export interface AnimatorInput {
-- dt: number;
-- /** Wall time (s) for idle sway. */
-- time: number;
-- /** Horizontal speed m/s. */
-- speed: number;
-- grounded: boolean;
-- /** View pitch (radians, +up) for head/torso aim. */
-- pitch: number;
-- /** Stance: 0 stand, 1 crouch, 2 prone. */
-- stance: number;
-- /** Equipped tool kind for arm posing. */
-- tool: 'physgun' | 'axe' | 'pickaxe' | 'rigging' | null;
-- /** Physgun beam currently latched (two-hand aim pose). */
-- beamActive: boolean;
--}
--export declare class AvatarAnimator {
-- private readonly joints;
-- private phase;
-- private pose;
-- /** One-shot swing timer (s remaining); drives axe/pickaxe chop. */
-- private swingT;
-- /** One-shot hurt flinch timer. */
-- private flinchT;
-- /** Smoothed grounded factor so landings ease instead of snapping. */
-- private groundBlend;
-- constructor(joints: RigJoints);
-- triggerSwing(): void;
-- triggerFlinch(): void;
-- update(input: AnimatorInput): void;
--}
--//# sourceMappingURL=animator.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/animator.d.ts.map b/apps/client/dist-types/render/avatar/animator.d.ts.map
ddeleted file mode 100644
iindex dd13e7a..0000000
---- a/apps/client/dist-types/render/avatar/animator.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"animator.d.ts","sourceRoot":"","sources":["../../../src/render/avatar/animator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEzC;;;;;;;GAOG;AAEH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,OAAO,CAAA;IACjB,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAA;IACd,yCAAyC;IACzC,IAAI,EAAE,SAAS,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,IAAI,CAAA;IACtD,0DAA0D;IAC1D,UAAU,EAAE,OAAO,CAAA;CACpB;AA0ED,qBAAa,cAAc;IAUb,OAAO,CAAC,QAAQ,CAAC,MAAM;IATnC,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,IAAI,CAAa;IACzB,mEAAmE;IACnE,OAAO,CAAC,MAAM,CAAI;IAClB,kCAAkC;IAClC,OAAO,CAAC,OAAO,CAAI;IACnB,qEAAqE;IACrE,OAAO,CAAC,WAAW,CAAI;gBAEM,MAAM,EAAE,SAAS;IAE9C,YAAY,IAAI,IAAI;IAIpB,aAAa,IAAI,IAAI;IAIrB,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;CA+LnC"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/avatar.d.ts b/apps/client/dist-types/render/avatar/avatar.d.ts
ddeleted file mode 100644
iindex e11b53f..0000000
---- a/apps/client/dist-types/render/avatar/avatar.d.ts
++++ /dev/null
@@@ -1,57 +0,0 @@
--import '@babylonjs/core/Rendering/outlineRenderer.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { ContentRegistry } from '@openvibe/content';
--import type { Appearance } from '@openvibe/protocol';
--/**
-- * A complete animated player character: parametric rig + procedural
-- * animator + held-item prop. Used identically for remote players, the
-- * local first-person body (head hidden), and the customization preview.
-- */
--export interface AvatarUpdate {
-- dt: number;
-- time: number;
-- x: number;
-- /** FEET height (capsule bottom). */
-- y: number;
-- z: number;
-- yaw: number;
-- pitch: number;
-- speed: number;
-- grounded: boolean;
-- /** Stance: 0 stand, 1 crouch, 2 prone. */
-- stance?: number;
-- /** Equipped item def id (drives held prop + arm pose). */
-- itemDef?: string | undefined;
-- beamActive?: boolean;
--}
--export declare class Avatar {
-- private readonly scene;
-- private readonly content;
-- private appearance;
-- private readonly name;
-- private rig;
-- private animator;
-- private toolProp;
-- private toolItemDef;
-- private headVisible;
-- private armsVisible;
-- constructor(scene: Scene, content: ContentRegistry, appearance: Appearance, name: string);
-- static appearanceOrDefault(a: Appearance | undefined): Appearance;
-- get eyeHeight(): number;
-- get rootPosition(): Vector3;
-- /** Rebuilds the rig (customization preview edits). Preserves pose state loosely. */
-- setAppearance(appearance: Appearance): void;
-- setHeadVisible(visible: boolean): void;
-- setArmsVisible(visible: boolean): void;
-- triggerFlinch(): void;
-- private hurtTimer;
-- /** Red overlay pulse when this avatar takes damage. */
-- flashHurt(strength: number): void;
-- triggerSwing(): void;
-- /** World position the physgun beam should start from (hand/muzzle). */
-- beamOrigin(): Vector3;
-- update(u: AvatarUpdate): void;
-- dispose(): void;
--}
--//# sourceMappingURL=avatar.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/avatar.d.ts.map b/apps/client/dist-types/render/avatar/avatar.d.ts.map
ddeleted file mode 100644
iindex 0e81bfa..0000000
---- a/apps/client/dist-types/render/avatar/avatar.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/render/avatar/avatar.ts"],"names":[],"mappings":"AAAA,OAAO,8CAA8C,CAAA;AAErD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAMpD;;;;GAIG;AAEH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,CAAC,EAAE,MAAM,CAAA;IACT,oCAAoC;IACpC,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,OAAO,CAAA;IACjB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAID,qBAAa,MAAM;IASf,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXvB,OAAO,CAAC,GAAG,CAAW;IACtB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,WAAW,CAAO;IAC1B,OAAO,CAAC,WAAW,CAAO;gBAGP,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,eAAe,EACjC,UAAU,EAAE,UAAU,EACb,IAAI,EAAE,MAAM;IAM/B,MAAM,CAAC,mBAAmB,CAAC,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU;IAIjE,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,oFAAoF;IACpF,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAY3C,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAKtC,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAKtC,aAAa,IAAI,IAAI;IAIrB,OAAO,CAAC,SAAS,CAA6C;IAE9D,uDAAuD;IACvD,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAYjC,YAAY,IAAI,IAAI;IAIpB,uEAAuE;IACvE,UAAU,IAAI,OAAO;IAKrB,MAAM,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI;IAoC7B,OAAO,IAAI,IAAI;CAIhB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/palettes.d.ts b/apps/client/dist-types/render/avatar/palettes.d.ts
ddeleted file mode 100644
iindex b12ef63..0000000
---- a/apps/client/dist-types/render/avatar/palettes.d.ts
++++ /dev/null
@@@ -1,12 +0,0 @@
--/**
-- * Avatar color palettes. The wire format carries indices; only the client
-- * knows colors, so palettes can be retuned freely. Index ranges must stay
-- * >= the bounds in @openvibe/protocol AppearanceSchema.
-- */
--export declare const SKIN_TONES: readonly ["#f2d0b4", "#eabf9c", "#d9a878", "#c68e5f", "#a96f45", "#8a5433", "#6b3f26", "#54301c"];
--export declare const HAIR_COLORS: readonly ["#241a12", "#3d2a1a", "#5c3f24", "#8a5a2e", "#b8863f", "#d8b169", "#9c3b22", "#8d8d93"];
--export declare const OUTFIT_COLORS: readonly ["#e8e4da", "#b8b4ac", "#5f6a72", "#3c4854", "#6d5a3e", "#4a6b52", "#7a3b32", "#3e5a7a", "#8a7a2e", "#2e2a28"];
--export declare function skinTone(i: number): string;
--export declare function hairColor(i: number): string;
--export declare function outfitColor(i: number): string;
--//# sourceMappingURL=palettes.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/palettes.d.ts.map b/apps/client/dist-types/render/avatar/palettes.d.ts.map
ddeleted file mode 100644
iindex ac93d05..0000000
---- a/apps/client/dist-types/render/avatar/palettes.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"palettes.d.ts","sourceRoot":"","sources":["../../../src/render/avatar/palettes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,UAAU,mGASb,CAAA;AAEV,eAAO,MAAM,WAAW,mGASd,CAAA;AAEV,eAAO,MAAM,aAAa,yHAWhB,CAAA;AAEV,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1C;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/rig.d.ts b/apps/client/dist-types/render/avatar/rig.d.ts
ddeleted file mode 100644
iindex 5f88719..0000000
---- a/apps/client/dist-types/render/avatar/rig.d.ts
++++ /dev/null
@@@ -1,50 +0,0 @@
--import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { Appearance } from '@openvibe/protocol';
--/**
-- * Parametric low-poly humanoid rig.
-- *
-- * A joint hierarchy of TransformNodes with flat-shaded tapered-box segments
-- * parented to them — no skinning, fully data-driven from Appearance.
-- *
-- * Assembly rules that keep the body seamless:
-- * - every segment's top extends PAST its joint (OVERLAP) into the segment
-- * above, so bending never exposes gaps or interior top faces;
-- * - adjoining segment widths match at the junction (thigh bottom == shin
-- * top, etc.) so silhouettes stay continuous;
-- * - thigh + shin + foot exactly total the leg length, so feet stand ON the
-- * ground instead of clipping through it.
-- *
-- * Conventions: root origin at the FEET (ground). +Z faces forward (player
-- * yaw). Limb joints rotate at the top of their segment.
-- */
--export interface RigJoints {
-- root: TransformNode;
-- /** Vertical bob/lean node between root and pelvis. */
-- bob: TransformNode;
-- spine: TransformNode;
-- chest: TransformNode;
-- neck: TransformNode;
-- head: TransformNode;
-- shoulderL: TransformNode;
-- shoulderR: TransformNode;
-- elbowL: TransformNode;
-- elbowR: TransformNode;
-- hipL: TransformNode;
-- hipR: TransformNode;
-- kneeL: TransformNode;
-- kneeR: TransformNode;
-- /** Attachment for held tools (right hand). */
-- handR: TransformNode;
--}
--export interface AvatarRig {
-- joints: RigJoints;
-- /** Eye height above the root (for sanity checks / camera alignment). */
-- eyeHeight: number;
-- setHeadVisible(visible: boolean): void;
-- /** First person hides the arms too — the viewmodel represents them. */
-- setArmsVisible(visible: boolean): void;
-- dispose(): void;
--}
--export declare function buildAvatarRig(scene: Scene, appearance: Appearance, name: string): AvatarRig;
--//# sourceMappingURL=rig.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/rig.d.ts.map b/apps/client/dist-types/render/avatar/rig.d.ts.map
ddeleted file mode 100644
iindex f910422..0000000
---- a/apps/client/dist-types/render/avatar/rig.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"rig.d.ts","sourceRoot":"","sources":["../../../src/render/avatar/rig.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AAEvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAKpD;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,aAAa,CAAA;IACnB,sDAAsD;IACtD,GAAG,EAAE,aAAa,CAAA;IAClB,KAAK,EAAE,aAAa,CAAA;IACpB,KAAK,EAAE,aAAa,CAAA;IACpB,IAAI,EAAE,aAAa,CAAA;IACnB,IAAI,EAAE,aAAa,CAAA;IACnB,SAAS,EAAE,aAAa,CAAA;IACxB,SAAS,EAAE,aAAa,CAAA;IACxB,MAAM,EAAE,aAAa,CAAA;IACrB,MAAM,EAAE,aAAa,CAAA;IACrB,IAAI,EAAE,aAAa,CAAA;IACnB,IAAI,EAAE,aAAa,CAAA;IACnB,KAAK,EAAE,aAAa,CAAA;IACpB,KAAK,EAAE,aAAa,CAAA;IACpB,8CAA8C;IAC9C,KAAK,EAAE,aAAa,CAAA;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,SAAS,CAAA;IACjB,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACtC,uEAAuE;IACvE,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACtC,OAAO,IAAI,IAAI,CAAA;CAChB;AAiED,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAkX5F"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/taperedBox.d.ts b/apps/client/dist-types/render/avatar/taperedBox.d.ts
ddeleted file mode 100644
iindex 7b643a7..0000000
---- a/apps/client/dist-types/render/avatar/taperedBox.d.ts
++++ /dev/null
@@@ -1,22 +0,0 @@
--import { Mesh } from '@babylonjs/core/Meshes/mesh.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--/**
-- * Flat-shaded tapered box — the single primitive the whole low-poly avatar
-- * is assembled from (equal top/bottom sizes = plain box). Origin sits at
-- * the anchor face so segments rotate naturally from their joint:
-- * `anchor: 'top'` hangs downward (limbs), `'bottom'` grows upward (torso).
-- * Optional forward offsets shear the profile for a hand-modeled feel.
-- */
--export interface TaperedBoxOpts {
-- topWidth: number;
-- topDepth: number;
-- bottomWidth: number;
-- bottomDepth: number;
-- height: number;
-- anchor: 'top' | 'bottom';
-- /** Shift (m) of the top/bottom face along +Z — gives shear/posture. */
-- topShiftZ?: number;
-- bottomShiftZ?: number;
--}
--export declare function createTaperedBox(name: string, opts: TaperedBoxOpts, scene: Scene): Mesh;
--//# sourceMappingURL=taperedBox.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/taperedBox.d.ts.map b/apps/client/dist-types/render/avatar/taperedBox.d.ts.map
ddeleted file mode 100644
iindex 02df6d1..0000000
---- a/apps/client/dist-types/render/avatar/taperedBox.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"taperedBox.d.ts","sourceRoot":"","sources":["../../../src/render/avatar/taperedBox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAA;AAErD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAErD;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,KAAK,GAAG,QAAQ,CAAA;IACxB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAoDvF"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/toolProps.d.ts b/apps/client/dist-types/render/avatar/toolProps.d.ts
ddeleted file mode 100644
iindex d5fc326..0000000
---- a/apps/client/dist-types/render/avatar/toolProps.d.ts
++++ /dev/null
@@@ -1,17 +0,0 @@
--import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--/**
-- * Procedural third-person held-item props, attached to the avatar's right
-- * hand. Deliberately chunky/readable at distance; the first-person
-- * viewmodel is a separate, more detailed presentation.
-- */
--export type ToolPropKind = 'physgun' | 'axe' | 'pickaxe' | 'generic';
--export interface ToolProp {
-- root: TransformNode;
-- /** Beam origin for physgun-style tools. */
-- muzzle: TransformNode;
-- dispose(): void;
--}
--export declare function createToolProp(scene: Scene, kind: ToolPropKind, name: string): ToolProp;
--export declare function toolPropKindFor(itemDefId: string | undefined, toolKind: string | undefined): ToolPropKind | null;
--//# sourceMappingURL=toolProps.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/avatar/toolProps.d.ts.map b/apps/client/dist-types/render/avatar/toolProps.d.ts.map
ddeleted file mode 100644
iindex 31d1224..0000000
---- a/apps/client/dist-types/render/avatar/toolProps.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"toolProps.d.ts","sourceRoot":"","sources":["../../../src/render/avatar/toolProps.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAIrD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAA;AAEpE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,aAAa,CAAA;IACnB,2CAA2C;IAC3C,MAAM,EAAE,aAAa,CAAA;IACrB,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAsIvF;AAED,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,YAAY,GAAG,IAAI,CAMrB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/beams.d.ts b/apps/client/dist-types/render/beams.d.ts
ddeleted file mode 100644
iindex 4dd1280..0000000
---- a/apps/client/dist-types/render/beams.d.ts
++++ /dev/null
@@@ -1,37 +0,0 @@
--import { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--/**
-- * Physgun beam visuals, GMod-style: the beam leaves the muzzle ALONG THE
-- * BARREL and bends toward the held point on a cubic Bezier — carrying a
-- * prop off to the side visibly flexes the beam instead of pivoting a
-- * straight stick. Idle (unlatched) beams are a dim, nearly-straight ray.
-- * Tubes are updatable meshes: same point count every frame, zero realloc.
-- */
--export interface BeamState {
-- from: Vector3;
-- to: Vector3;
-- /** Direction the beam LEAVES the muzzle (usually the view/barrel axis). */
-- tangent?: Vector3;
-- /** A prop is latched: bright beam + flare; otherwise dim searching ray. */
-- latched: boolean;
--}
--export declare class BeamRenderer {
-- private readonly scene;
-- private readonly beams;
-- private readonly idleMat;
-- private readonly strongMat;
-- private readonly flareMat;
-- private time;
-- constructor(scene: Scene);
-- /** Reconcile active beams: key -> beam state. */
-- update(dt: number, active: Map<string, BeamState>): void;
-- /**
-- * Cubic Bezier from the muzzle: P0 at the tip, P1 pushed along the barrel
-- * tangent (so the beam LEAVES straight out of the gun), P2 eased back
-- * toward the target's approach, P3 at the grab point. Control lengths
-- * scale with distance, giving a taut short beam and a lazy long arc.
-- */
-- private fillPath;
-- dispose(): void;
--}
--//# sourceMappingURL=beams.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/beams.d.ts.map b/apps/client/dist-types/render/beams.d.ts.map
ddeleted file mode 100644
iindex df961d0..0000000
---- a/apps/client/dist-types/render/beams.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"beams.d.ts","sourceRoot":"","sources":["../../src/render/beams.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAI9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAErD;;;;;;GAMG;AAEH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,OAAO,CAAA;IACb,EAAE,EAAE,OAAO,CAAA;IACX,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAA;CACjB;AAUD,qBAAa,YAAY;IAOX,OAAO,CAAC,QAAQ,CAAC,KAAK;IANlC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,IAAI,CAAI;gBAEa,KAAK,EAAE,KAAK;IAmBzC,iDAAiD;IACjD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,IAAI;IA4CxD;;;;;OAKG;IACH,OAAO,CAAC,QAAQ;IA0BhB,OAAO,IAAI,IAAI;CAUhB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/constraintLines.d.ts b/apps/client/dist-types/render/constraintLines.d.ts
ddeleted file mode 100644
iindex 5887502..0000000
---- a/apps/client/dist-types/render/constraintLines.d.ts
++++ /dev/null
@@@ -1,16 +0,0 @@
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { ClientState } from '../state/clientState.js';
--import type { EntityView } from './entityView.js';
--export declare class ConstraintLineRenderer {
-- private readonly scene;
-- private readonly state;
-- private readonly view;
-- private readonly lines;
-- private readonly ropeMat;
-- private readonly springMat;
-- constructor(scene: Scene, state: ClientState, view: EntityView);
-- update(): void;
-- private hide;
-- dispose(): void;
--}
--//# sourceMappingURL=constraintLines.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/constraintLines.d.ts.map b/apps/client/dist-types/render/constraintLines.d.ts.map
ddeleted file mode 100644
iindex 0e52762..0000000
---- a/apps/client/dist-types/render/constraintLines.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"constraintLines.d.ts","sourceRoot":"","sources":["../../src/render/constraintLines.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAgBjD,qBAAa,sBAAsB;IAM/B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAPvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IACpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;gBAGzB,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,UAAU;IAYnC,MAAM,IAAI,IAAI;IA4Cd,OAAO,CAAC,IAAI;IAIZ,OAAO,IAAI,IAAI;CAMhB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/entityView.d.ts b/apps/client/dist-types/render/entityView.d.ts
ddeleted file mode 100644
iindex 57b576e..0000000
---- a/apps/client/dist-types/render/entityView.d.ts
++++ /dev/null
@@@ -1,41 +0,0 @@
--import '@babylonjs/core/Rendering/outlineRenderer.js';
--import { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { ContentRegistry } from '@openvibe/content';
--import { type BodyId, type PhysicsWorld } from '@openvibe/physics';
--import type { ServerSnapshot } from '@openvibe/protocol';
--import type { ClientState } from '../state/clientState.js';
--import { Avatar } from './avatar/avatar.js';
--export declare class EntityView {
-- private readonly scene;
-- private readonly physics;
-- private readonly content;
-- private readonly state;
-- private readonly visuals;
-- private readonly entityByBody;
-- /** EMA of (serverTime - localTime) for the interpolation clock. */
-- private clockOffset;
-- private lastUpdateTime;
-- constructor(scene: Scene, physics: PhysicsWorld, content: ContentRegistry, state: ClientState);
-- entityIdForBody(bodyId: BodyId): string | undefined;
-- avatarFor(entityId: string): Avatar | null;
-- private add;
-- /** Static capsule matching the server's player body (short + lifted). */
-- private addPlayerBody;
-- private remove;
-- /** Authoritative pin: freeze/settle transforms, resource depletion state. */
-- private applyAuthoritative;
-- /** Feed a snapshot into interpolation buffers. */
-- onSnapshot(snap: ServerSnapshot, localTime: number): void;
-- /** Per-frame: interpolate meshes/avatars toward buffered samples. */
-- update(localTime: number): void;
-- /** Rigging tool first-pick highlight (null = nothing selected). */
-- selectedId: string | null;
-- /**
-- * World position of a grab point given in an entity's local space (beam
-- * endpoints stick to the touched spot, not the prop center).
-- */
-- grabPointOf(id: string, local: [number, number, number] | undefined): Vector3 | null;
-- positionOf(id: string): Vector3 | null;
--}
--//# sourceMappingURL=entityView.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/entityView.d.ts.map b/apps/client/dist-types/render/entityView.d.ts.map
ddeleted file mode 100644
iindex 2240412..0000000
---- a/apps/client/dist-types/render/entityView.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"entityView.d.ts","sourceRoot":"","sources":["../../src/render/entityView.ts"],"names":[],"mappings":"AAAA,OAAO,8CAA8C,CAAA;AAErD,OAAO,EAAc,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAM1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAc,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EAAkB,KAAK,MAAM,EAAE,KAAK,YAAY,EAAkB,MAAM,mBAAmB,CAAA;AAClG,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,oBAAoB,CAAA;AAEpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAyC3C,qBAAa,UAAU;IAQnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAVxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA4B;IACzD,mEAAmE;IACnE,OAAO,CAAC,WAAW,CAAsB;IACzC,OAAO,CAAC,cAAc,CAAI;gBAGP,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,WAAW;IAarC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAInD,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAI1C,OAAO,CAAC,GAAG;IA8GX,yEAAyE;IACzE,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,MAAM;IAgBd,6EAA6E;IAC7E,OAAO,CAAC,kBAAkB;IAwB1B,kDAAkD;IAClD,UAAU,CAAC,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAsCzD,qEAAqE;IACrE,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IA+H/B,mEAAmE;IACnE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAO;IAEhC;;;OAGG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI;IAUpF,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;CAOvC"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/environment.d.ts b/apps/client/dist-types/render/environment.d.ts
ddeleted file mode 100644
iindex d19c039..0000000
---- a/apps/client/dist-types/render/environment.d.ts
++++ /dev/null
@@@ -1,43 +0,0 @@
--import type { AbstractEngine } from '@babylonjs/core/Engines/abstractEngine.js';
--import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight.js';
--import '@babylonjs/core/LensFlares/lensFlareSystemSceneComponent.js';
--import '@babylonjs/core/Culling/ray.js';
--import { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { Camera } from '@babylonjs/core/Cameras/camera.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--export declare class Environment {
-- private readonly scene;
-- readonly sun: DirectionalLight;
-- private readonly hemi;
-- private pipeline;
-- private readonly flareEmitter;
-- private readonly flares;
-- private readonly skyMat;
-- private readonly clouds;
-- private readonly cloudMat;
-- private readonly moon;
-- private readonly moonMat;
-- private t;
-- private targetT;
-- constructor(scene: Scene, _engine: AbstractEngine);
-- /** Attach the HDR tonemapping pipeline to the active gameplay camera. */
-- attachCamera(camera: Camera): void;
-- /** Sync toward the server's shared day fraction (smoothed, no sun jumps). */
-- setDayFraction(frac: number): void;
-- /** Server-authoritative weather: light/haze/cloud response (render only). */
-- private weather;
-- /** True while the weather system owns scene fog (vs the underwater look). */
-- private ownsFog;
-- /** Set by main each frame; underwater fog wins over weather haze. */
-- underwater: boolean;
-- private weatherDim;
-- private weatherDimTarget;
-- private weatherHaze;
-- private weatherHazeTarget;
-- private weatherCloud;
-- private weatherCloudTarget;
-- setWeather(kind: 'clear' | 'cloudy' | 'rain' | 'storm' | 'fog'): void;
-- /** Advance time of day; call once per frame. */
-- update(dt: number, cameraPos: Vector3): void;
--}
--//# sourceMappingURL=environment.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/environment.d.ts.map b/apps/client/dist-types/render/environment.d.ts.map
ddeleted file mode 100644
iindex 9ea0aea..0000000
---- a/apps/client/dist-types/render/environment.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/render/environment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2CAA2C,CAAA;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAA;AAE7E,OAAO,6DAA6D,CAAA;AAGpE,OAAO,gCAAgC,CAAA;AAMvC,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAO9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAA;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAqBrD,qBAAa,WAAW;IAepB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAdxB,QAAQ,CAAC,GAAG,EAAE,gBAAgB,CAAA;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAkB;IACvC,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAM;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,CAAC,CAAqB;IAC9B,OAAO,CAAC,OAAO,CAAsB;gBAGlB,KAAK,EAAE,KAAK,EAC7B,OAAO,EAAE,cAAc;IAoEzB,yEAAyE;IACzE,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAUlC,6EAA6E;IAC7E,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIlC,6EAA6E;IAC7E,OAAO,CAAC,OAAO,CAAyD;IACxE,6EAA6E;IAC7E,OAAO,CAAC,OAAO,CAAQ;IACvB,qEAAqE;IACrE,UAAU,UAAQ;IAClB,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,WAAW,CAAI;IACvB,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,kBAAkB,CAAI;IAE9B,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,IAAI;IAcrE,gDAAgD;IAChD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI;CA6E7C"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/firstPersonBody.d.ts b/apps/client/dist-types/render/firstPersonBody.d.ts
ddeleted file mode 100644
iindex 70be86f..0000000
---- a/apps/client/dist-types/render/firstPersonBody.d.ts
++++ /dev/null
@@@ -1,21 +0,0 @@
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { ContentRegistry } from '@openvibe/content';
--import type { Appearance } from '@openvibe/protocol';
--import type { LocalPlayer } from '../game/localPlayer.js';
--import type { ClientState } from '../state/clientState.js';
--export declare class FirstPersonBody {
-- private readonly player;
-- private readonly state;
-- private readonly avatar;
-- constructor(scene: Scene, content: ContentRegistry, appearance: Appearance, player: LocalPlayer, state: ClientState);
-- setAppearance(appearance: Appearance): void;
-- triggerSwing(): void;
-- beamOrigin(): import("@babylonjs/core").Vector3;
-- update(dt: number, time: number, renderPos: {
-- x: number;
-- y: number;
-- z: number;
-- }, beamActive: boolean): void;
-- dispose(): void;
--}
--//# sourceMappingURL=firstPersonBody.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/firstPersonBody.d.ts.map b/apps/client/dist-types/render/firstPersonBody.d.ts.map
ddeleted file mode 100644
iindex 045afd0..0000000
---- a/apps/client/dist-types/render/firstPersonBody.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"firstPersonBody.d.ts","sourceRoot":"","sources":["../../src/render/firstPersonBody.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAExD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAa1D,qBAAa,eAAe;IAOxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAPxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;gBAG7B,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,eAAe,EACxB,UAAU,EAAE,UAAU,EACL,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,WAAW;IASrC,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAM3C,YAAY,IAAI,IAAI;IAIpB,UAAU;IAIV,MAAM,CACJ,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAC9C,UAAU,EAAE,OAAO,GAClB,IAAI;IAgCP,OAAO,IAAI,IAAI;CAGhB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/heldItem.d.ts b/apps/client/dist-types/render/heldItem.d.ts
ddeleted file mode 100644
iindex ce2854a..0000000
---- a/apps/client/dist-types/render/heldItem.d.ts
++++ /dev/null
@@@ -1,27 +0,0 @@
--import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { ContentRegistry } from '@openvibe/content';
--/**
-- * The one way ANY item becomes a visible in-hand model: tools use their
-- * authored prop; everything else shows its actual world model (the same
-- * shape/color it has when dropped), auto-fitted to the hand:
-- *
-- * - uniformly scaled so its LONGEST dimension hits the requested grip
-- * size (viewmodel and third-person pass different sizes),
-- * - reoriented so elongated shapes lie across the palm (a log stands
-- * upright in the world but is carried horizontally),
-- * - grip-centered so the mesh pivots around where the hand holds it.
-- *
-- * Third-person hands and the first-person viewmodel both build from this,
-- * so an item always looks like itself everywhere.
-- */
--export interface HeldItemNode {
-- root: TransformNode;
-- /** Beam origin for physgun-style tools (null for ordinary items). */
-- muzzle: TransformNode | null;
-- dispose(): void;
--}
--/** Default grip size (third-person avatars). */
--export declare const HAND_SIZE = 0.34;
--export declare function createHeldItemNode(scene: Scene, content: ContentRegistry, defId: string, name: string, gripSize?: number): HeldItemNode | null;
--//# sourceMappingURL=heldItem.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/heldItem.d.ts.map b/apps/client/dist-types/render/heldItem.d.ts.map
ddeleted file mode 100644
iindex db92637..0000000
---- a/apps/client/dist-types/render/heldItem.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"heldItem.d.ts","sourceRoot":"","sources":["../../src/render/heldItem.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AACvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAc,MAAM,mBAAmB,CAAA;AAIpE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,aAAa,CAAA;IACnB,qEAAqE;IACrE,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,gDAAgD;AAChD,eAAO,MAAM,SAAS,OAAO,CAAA;AAE7B,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,QAAQ,SAAY,GACnB,YAAY,GAAG,IAAI,CAsCrB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/layeredSurface.d.ts b/apps/client/dist-types/render/layeredSurface.d.ts
ddeleted file mode 100644
iindex 4a48427..0000000
---- a/apps/client/dist-types/render/layeredSurface.d.ts
++++ /dev/null
@@@ -1,51 +0,0 @@
--/**
-- * The renderer for a layered surface: one base texture plus up to four paint
-- * layers blended by the RGBA channels of a single mask.
-- *
-- * Built on Babylon's `CustomMaterial`, which extends StandardMaterial and
-- * lets us inject GLSL at `CUSTOM_FRAGMENT_UPDATE_DIFFUSE`. That matters:
-- * lighting, shadows, fog and the day/night rig keep working exactly as they
-- * do for every other surface in the game, and the SAME material class runs in
-- * the editor and in play — so painting is WYSIWYG rather than an editor-only
-- * approximation.
-- *
-- * Replaces TerrainMaterial, whose three diffuse slots were hard-wired to
-- * grass/rock/mud and which had no concept of a base texture at all.
-- */
--import { CustomMaterial } from '@babylonjs/materials/custom/customMaterial.js';
--import { Texture } from '@babylonjs/core/Materials/Textures/texture.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import { type SurfaceMaterialData } from '@openvibe/content';
--export interface LayeredSurfaceOptions {
-- /** UV tiles across the surface for the BASE texture. */
-- baseTiling?: number;
-- /** Default tiling for layers that do not specify their own. */
-- layerTiling?: number;
-- backFaceCulling?: boolean;
--}
--/**
-- * A material bound to one surface's data. `update()` re-reads the data, so a
-- * paint stroke or a base-texture change never needs the mesh rebuilt.
-- */
--export declare class LayeredSurfaceMaterial {
-- private readonly scene;
-- private data;
-- private readonly opts;
-- readonly material: CustomMaterial;
-- private layerTextures;
-- private maskTexture;
-- private readonly fallbackMask;
-- constructor(scene: Scene, name: string, data: SurfaceMaterialData, opts?: LayeredSurfaceOptions);
-- get surface(): SurfaceMaterialData;
-- /** Point the mask sampler at a live canvas texture (the editor's brush). */
-- setMaskTexture(tex: Texture | null): void;
-- update(data: SurfaceMaterialData): void;
-- private scales;
-- private enabled;
-- private tints;
-- private bind;
-- dispose(): void;
--}
--/** Load a persisted mask image (data URL or /map-assets path) as a texture. */
--export declare function maskTextureFrom(scene: Scene, mask: string | undefined): Texture | null;
--//# sourceMappingURL=layeredSurface.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/layeredSurface.d.ts.map b/apps/client/dist-types/render/layeredSurface.d.ts.map
ddeleted file mode 100644
iindex 76cd216..0000000
---- a/apps/client/dist-types/render/layeredSurface.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"layeredSurface.d.ts","sourceRoot":"","sources":["../../src/render/layeredSurface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,+CAA+C,CAAA;AAE9E,OAAO,EAAE,OAAO,EAAE,MAAM,+CAA+C,CAAA;AAGvE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,mBAAmB,CAAA;AAoD1B,MAAM,WAAW,qBAAqB;IACpC,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED;;;GAGG;AACH,qBAAa,sBAAsB;IAO/B,OAAO,CAAC,QAAQ,CAAC,KAAK;IAEtB,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,QAAQ,CAAC,IAAI;IATvB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;IACjC,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAGnB,KAAK,EAAE,KAAK,EAC7B,IAAI,EAAE,MAAM,EACJ,IAAI,EAAE,mBAAmB,EAChB,IAAI,GAAE,qBAA0B;IAkBnD,IAAI,OAAO,IAAI,mBAAmB,CAEjC;IAED,4EAA4E;IAC5E,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI;IAKzC,MAAM,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI;IAmDvC,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,KAAK,CAA6E;IAE1F,OAAO,CAAC,IAAI;IA4BZ,OAAO,IAAI,IAAI;CAKhB;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,CAMtF"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/mapRuntimeLayers.d.ts b/apps/client/dist-types/render/mapRuntimeLayers.d.ts
ddeleted file mode 100644
iindex d87f3c1..0000000
---- a/apps/client/dist-types/render/mapRuntimeLayers.d.ts
++++ /dev/null
@@@ -1,56 +0,0 @@
--/**
-- * The game client's map layers, reconciled by stable id.
-- *
-- * A `map_reload` used to rebuild every category wholesale: dispose every
-- * terrain mesh, every terrain collider, every map static, every map light,
-- * and make them all again. Moving one box therefore re-parsed and re-uploaded
-- * the geometry of the entire map to the GPU, and rebuilt collision the player
-- * was standing on — which is visible as a hitch and, mid-jump, as a fall.
-- *
-- * The same `diffMapFileV2` the server uses says exactly what changed, and
-- * `affectsCollision` says whether a change is appearance-only. Everything
-- * here keys on the document's stable ids, so client rendering, client
-- * prediction physics and server physics stay in agreement about which object
-- * is which.
-- */
--import type { Scene } from '@babylonjs/core/scene.js';
--import { type MapDiff, type MapFileV2 } from '@openvibe/content';
--export type { MapDiff };
--/** Compute the diff once and hand each layer only its own category. */
--export declare const mapDiff: (previous: MapFileV2, next: MapFileV2) => MapDiff;
--/**
-- * Which terrains need their COLLISION rebuilt, and which only their look.
-- * Returned separately because the two have very different costs.
-- */
--export declare function terrainWork(diff: MapDiff): {
-- collision: string[];
-- appearanceOnly: string[];
-- removed: string[];
-- added: string[];
--};
--export declare function staticWork(diff: MapDiff): {
-- added: string[];
-- removed: string[];
-- collision: string[];
-- appearanceOnly: string[];
--};
--export declare class MapLightLayer {
-- private readonly scene;
-- private readonly lights;
-- constructor(scene: Scene);
-- get size(): number;
-- has(id: string): boolean;
-- /** Build (or rebuild) every light — boot, and a whole-document replace. */
-- reconcile(next: MapFileV2['lights']): void;
-- /**
-- * Only the lights the diff says changed. A texture edit used to make every
-- * light in the scene blink, because the whole set was disposed and rebuilt.
-- */
-- reconcileFromDiff(diff: MapDiff, next: MapFileV2['lights']): void;
-- private drop;
-- private create;
-- dispose(): void;
--}
--/** Dispose meshes whose name matches a prefix + id, for the visual layers. */
--export declare function disposeById(scene: Scene, prefix: string, ids: readonly string[]): void;
--//# sourceMappingURL=mapRuntimeLayers.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/mapRuntimeLayers.d.ts.map b/apps/client/dist-types/render/mapRuntimeLayers.d.ts.map
ddeleted file mode 100644
iindex 4138e59..0000000
---- a/apps/client/dist-types/render/mapRuntimeLayers.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapRuntimeLayers.d.ts","sourceRoot":"","sources":["../../src/render/mapRuntimeLayers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAGrD,OAAO,EAAmC,KAAK,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAKjG,YAAY,EAAE,OAAO,EAAE,CAAA;AAEvB,uEAAuE;AACvE,eAAO,MAAM,OAAO,GAAI,UAAU,SAAS,EAAE,MAAM,SAAS,KAAG,OAChC,CAAA;AAE/B;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG;IAC1C,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAOA;AAKD,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG;IACzC,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,cAAc,EAAE,MAAM,EAAE,CAAA;CACzB,CAYA;AAcD,qBAAa,aAAa;IAMZ,OAAO,CAAC,QAAQ,CAAC,KAAK;IALlC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGpB;gBAE0B,KAAK,EAAE,KAAK;IAEzC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB,2EAA2E;IAC3E,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,GAAG,IAAI;IAK1C;;;OAGG;IACH,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,GAAG,IAAI;IAcjE,OAAO,CAAC,IAAI;IASZ,OAAO,CAAC,MAAM;IAqBd,OAAO,IAAI,IAAI;CAGhB;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAMtF"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/mapStyle.d.ts b/apps/client/dist-types/render/mapStyle.d.ts
ddeleted file mode 100644
iindex 354e36c..0000000
---- a/apps/client/dist-types/render/mapStyle.d.ts
++++ /dev/null
@@@ -1,58 +0,0 @@
--import { Texture } from '@babylonjs/core/Materials/Textures/texture.js';
--import type { Mesh } from '@babylonjs/core/Meshes/mesh.js';
--import type { Light } from '@babylonjs/core/Lights/light.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { FaceStyle, MapLight, MapTextureEntry, WorldShape } from '@openvibe/content';
--/**
-- * Shared surface-styling + lighting for editor AND game: custom texture
-- * registry (server-hosted or embedded), Hammer-style per-face materials on
-- * box statics, and editor-placed map lights (all five Babylon light types,
-- * optional shadow generators). Both scenes call the same functions so what
-- * you see in the editor is what ships in game.
-- */
--export interface CustomTexInfo {
-- url: string;
-- /** World meters covered by one texture tile (default 2). */
-- scale?: number;
--}
--export declare function registerCustomTextures(entries: MapTextureEntry[] | undefined): void;
--/** Resolve a texture reference ('red_brick' | 'custom:<name>' | 'none'). */
--export declare function resolveTexInfo(name: string | undefined): CustomTexInfo | null;
--/** 'red_brick_diff_4k' → 'Red Brick' (display name, no prefixes/suffixes). */
--export declare function prettyTexName(name: string): string;
--interface StyledBody {
-- shape: WorldShape;
-- color: string;
-- tex?: string | undefined;
-- uv?: FaceStyle | undefined;
-- faces?: Record<string, FaceStyle> | undefined;
--}
--/**
-- * Apply the body's surface styling (texture/uv/per-face) to its mesh.
-- * Plain-color bodies keep their shared cached material — zero cost.
-- */
--export declare function applyStaticStyle(scene: Scene, mesh: Mesh, s: StyledBody): void;
--/**
-- * Terrain patch tiling: default keeps the classic ~4m tile; custom
-- * textures honor their own meters-per-tile scale; uv overrides win.
-- */
--export declare function applyPatchTexture(tx: Texture, halfExtent: number, info: CustomTexInfo, uv?: FaceStyle): void;
--export declare const LIGHT_DEFAULTS: {
-- color: string;
-- intensity: {
-- point: number;
-- spot: number;
-- directional: number;
-- hemi: number;
-- rect: number;
-- };
-- range: number;
-- angle: number;
-- exponent: number;
-- ground: string;
-- size: [number, number];
--};
--/** Instantiate ONE map light (editor uses this for live preview too). */
--export declare function instantiateMapLight(scene: Scene, l: MapLight): Light | null;
--export {};
--//# sourceMappingURL=mapStyle.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/mapStyle.d.ts.map b/apps/client/dist-types/render/mapStyle.d.ts.map
ddeleted file mode 100644
iindex 3797c78..0000000
---- a/apps/client/dist-types/render/mapStyle.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapStyle.d.ts","sourceRoot":"","sources":["../../src/render/mapStyle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,+CAA+C,CAAA;AAGvE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAA;AAO1D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iCAAiC,CAAA;AAC5D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEzF;;;;;;GAMG;AAEH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAID,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,SAAS,GAAG,IAAI,CAMnF;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,IAAI,CAI7E;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASlD;AAED,UAAU,UAAU;IAClB,KAAK,EAAE,UAAU,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxB,EAAE,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,SAAS,CAAA;CAC9C;AAiED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,UAAU,GAAG,IAAI,CAqB9E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,OAAO,EACX,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,aAAa,EACnB,EAAE,CAAC,EAAE,SAAS,GACb,IAAI,CAWN;AAID,eAAO,MAAM,cAAc;;;;;;;;;;;;;UAOT,CAAC,MAAM,EAAE,MAAM,CAAC;CACjC,CAAA;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,IAAI,CAiD3E"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/modelCache.d.ts b/apps/client/dist-types/render/modelCache.d.ts
ddeleted file mode 100644
iindex 1c4cb4c..0000000
---- a/apps/client/dist-types/render/modelCache.d.ts
++++ /dev/null
@@@ -1,24 +0,0 @@
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
--export interface ModelInstance {
-- /** Root of the instantiated hierarchy, ready to parent. */
-- root: TransformNode;
-- dispose: () => void;
--}
--export declare class ModelCache {
-- readonly scene: Scene;
-- private readonly containers;
-- constructor(scene: Scene);
-- get size(): number;
-- /** Parse a model once; repeat callers share the same in-flight promise. */
-- private load;
-- /**
-- * Instantiate one copy. Returns null when the model cannot be parsed, so
-- * the caller can raise an Issue instead of leaving a silent hole.
-- */
-- instantiate(id: string, glb: string): Promise<ModelInstance | null>;
-- /** Drop a model (deleted import) and everything derived from it. */
-- forget(id: string): void;
-- dispose(): void;
--}
--//# sourceMappingURL=modelCache.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/modelCache.d.ts.map b/apps/client/dist-types/render/modelCache.d.ts.map
ddeleted file mode 100644
iindex 85afbff..0000000
---- a/apps/client/dist-types/render/modelCache.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"modelCache.d.ts","sourceRoot":"","sources":["../../src/render/modelCache.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AAE5E,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,IAAI,EAAE,aAAa,CAAA;IACnB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,qBAAa,UAAU;IAGT,QAAQ,CAAC,KAAK,EAAE,KAAK;IAFjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoD;gBAE1D,KAAK,EAAE,KAAK;IAEjC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,2EAA2E;IAC3E,OAAO,CAAC,IAAI;IAgBZ;;;OAGG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAczE,oEAAoE;IACpE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAMxB,OAAO,IAAI,IAAI;CAGhB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/paintedStatic.d.ts b/apps/client/dist-types/render/paintedStatic.d.ts
ddeleted file mode 100644
iindex a67b9c3..0000000
---- a/apps/client/dist-types/render/paintedStatic.d.ts
++++ /dev/null
@@@ -1,43 +0,0 @@
--import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js';
--import type { Mesh } from '@babylonjs/core/Meshes/mesh.js';
--import type { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { Texture } from '@babylonjs/core/Materials/Textures/texture.js';
--import type { StaticObjectV2, SurfaceMaterialData } from '@openvibe/content';
--/** Resolves the live (or loaded) mask texture for one surface. */
--export type MaskResolver = (ownerId: string, surfaceId: string, data: SurfaceMaterialData) => Texture | null;
--export interface PaintedStaticResult {
-- /** Materials created here, so the caller can dispose them. */
-- dispose: () => void;
--}
--/** Does this static have any authored v2 surface at all? */
--export declare function hasPaintedSurface(body: StaticObjectV2): boolean;
--/**
-- * Apply painted surfaces to a static's mesh. Returns null when the static has
-- * no v2 surface data, in which case the caller's ordinary styling stands.
-- */
--export declare function applyPaintedStatic(scene: Scene, mesh: Mesh, body: StaticObjectV2, maskFor: MaskResolver): PaintedStaticResult | null;
--/**
-- * A transparent paint overlay for one child mesh of an imported model.
-- *
-- * The overlay is a geometry clone sitting a hair proud of the original, with
-- * a layered material whose alpha IS the mask coverage. The original glTF
-- * material underneath is untouched, so an unpainted model looks exactly as
-- * its author exported it — and painting one instance cannot affect another,
-- * because the overlay belongs to the instance rather than to the cached
-- * template.
-- */
--export declare function createModelPaintOverlay(scene: Scene, source: AbstractMesh, ownerId: string, surfaceId: string, data: SurfaceMaterialData, maskFor: MaskResolver,
--/**
-- * How to project a mesh that shipped with no usable UVs. Must be the space
-- * the BRUSH used, or the paint would be shown somewhere other than where it
-- * was applied.
-- */
--fallback?: {
-- root: TransformNode;
-- extent: readonly [number, number, number];
--}): {
-- mesh: Mesh;
-- dispose: () => void;
--} | null;
--//# sourceMappingURL=paintedStatic.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/paintedStatic.d.ts.map b/apps/client/dist-types/render/paintedStatic.d.ts.map
ddeleted file mode 100644
iindex bf0b1cb..0000000
---- a/apps/client/dist-types/render/paintedStatic.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"paintedStatic.d.ts","sourceRoot":"","sources":["../../src/render/paintedStatic.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAA;AAC1E,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AAC5E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+CAA+C,CAAA;AAC5E,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAI5E,kEAAkE;AAClE,MAAM,MAAM,YAAY,GAAG,CACzB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,mBAAmB,KACtB,OAAO,GAAG,IAAI,CAAA;AAEnB,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAUD,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAG/D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,YAAY,GACpB,mBAAmB,GAAG,IAAI,CA6C5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,YAAY;AACrB;;;;GAIG;AACH,QAAQ,CAAC,EAAE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAC5E;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,GAAG,IAAI,CAsC5C"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/paintedStatic.test.d.ts b/apps/client/dist-types/render/paintedStatic.test.d.ts
ddeleted file mode 100644
iindex 0df70b9..0000000
---- a/apps/client/dist-types/render/paintedStatic.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--import '@babylonjs/core/Meshes/instancedMesh.js';
--//# sourceMappingURL=paintedStatic.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/paintedStatic.test.d.ts.map b/apps/client/dist-types/render/paintedStatic.test.d.ts.map
ddeleted file mode 100644
iindex 5c46203..0000000
---- a/apps/client/dist-types/render/paintedStatic.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"paintedStatic.test.d.ts","sourceRoot":"","sources":["../../src/render/paintedStatic.test.ts"],"names":[],"mappings":"AAoBA,OAAO,yCAAyC,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/sceneSetup.d.ts b/apps/client/dist-types/render/sceneSetup.d.ts
ddeleted file mode 100644
iindex 6802600..0000000
---- a/apps/client/dist-types/render/sceneSetup.d.ts
++++ /dev/null
@@@ -1,50 +0,0 @@
--import type { AbstractEngine } from '@babylonjs/core/Engines/abstractEngine.js';
--import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js';
--import { Mesh } from '@babylonjs/core/Meshes/mesh.js';
--import { Scene } from '@babylonjs/core/scene.js';
--import { type ContentRegistry, type MapTextureEntry, type WorldShape } from '@openvibe/content';
--import { Water } from './water.js';
--/**
-- * Engine + scene bootstrap and static world construction. WebGPU when the
-- * browser supports it, WebGL otherwise — nothing else in the client cares
-- * which one is active.
-- */
--export declare function createEngine(canvas: HTMLCanvasElement): Promise<AbstractEngine>;
--export declare function createScene(engine: AbstractEngine): Scene;
--export declare function materialFor(scene: Scene, hex: string): StandardMaterial;
--export declare function meshForShape(scene: Scene, name: string, shape: WorldShape, color: string): Mesh;
--export declare function registerMapAssets(map: {
-- textures?: MapTextureEntry[];
-- models?: {
-- id: string;
-- name: string;
-- glb: string;
-- bounds: [number, number, number];
-- }[];
--} | null): void;
--export declare function buildTerrainPatches(scene: Scene, content: ContentRegistry, onlyIds?: readonly string[]): Mesh[];
--/**
-- * Rebuild only the terrain meshes named in `ids` (all of them when omitted).
-- *
-- * Disposing and rebuilding every terrain because one was retinted re-uploads
-- * the geometry of the whole map to the GPU; with stable ids only the ones
-- * that changed need to move.
-- */
--export declare function rebuildTerrainPatchVisuals(scene: Scene, content: ContentRegistry, ids?: readonly string[]): void;
--export declare function buildStaticWorld(scene: Scene, content: ContentRegistry): void;
--/**
-- * The MAP's statics, rendered under their stable ids in their own layer.
-- * They used to be pushed into `content.world.statics` at boot, which merged
-- * authored geometry into base content permanently — so a live save could add
-- * a mesh but never move or remove one, and re-applying a map drew a second
-- * copy on top of the first.
-- */
--export declare function buildMapStaticVisuals(scene: Scene, water?: Water, onlyIds?: readonly string[]): void;
--/**
-- * Live map save: rebuild only the map statics named in `ids` (all of them
-- * when omitted), leaving base content alone.
-- */
--export declare function rebuildMapStaticVisuals(scene: Scene, ids?: readonly string[]): void;
--/** Live map edit: replace the rendered terrain with the new grid + paint. */
--export declare function rebuildTerrainVisual(scene: Scene, content: ContentRegistry): void;
--//# sourceMappingURL=sceneSetup.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/sceneSetup.d.ts.map b/apps/client/dist-types/render/sceneSetup.d.ts.map
ddeleted file mode 100644
iindex aff3634..0000000
---- a/apps/client/dist-types/render/sceneSetup.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"sceneSetup.d.ts","sourceRoot":"","sources":["../../src/render/sceneSetup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2CAA2C,CAAA;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+CAA+C,CAAA;AAQhF,OAAO,EAAE,IAAI,EAAE,MAAM,gCAAgC,CAAA;AAErD,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAShD,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,UAAU,EAMhB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAQlC;;;;GAIG;AAEH,wBAAsB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC,CAOrF;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,KAAK,CAMzD;AAMD,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAevE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAoB/F;AAMD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE;IACH,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAA;IAC5B,MAAM,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EAAE,CAAA;CACvF,GAAG,IAAI,GACP,IAAI,CAIN;AAsED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAC1B,IAAI,EAAE,CAkER;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,eAAe,EACxB,GAAG,CAAC,EAAE,SAAS,MAAM,EAAE,GACtB,IAAI,CAQN;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAQ7E;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,EACZ,KAAK,CAAC,EAAE,KAAK,EACb,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAC1B,IAAI,CAMN;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAQnF;AA8KD,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAQjF"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/surfaceProjection.d.ts b/apps/client/dist-types/render/surfaceProjection.d.ts
ddeleted file mode 100644
iindex ec3558f..0000000
---- a/apps/client/dist-types/render/surfaceProjection.d.ts
++++ /dev/null
@@@ -1,95 +0,0 @@
--/**
-- * Surface projections — the geometry of "where on this thing did the brush
-- * land", shared by the editor that paints and the renderer that displays it.
-- *
-- * These live under render/ rather than under the editor because BOTH sides
-- * need them and the dependency has to point one way: the game must never
-- * import editor code. If the two disagreed by even a sign, paint would appear
-- * in one place in the editor and another in the game.
-- *
-- * Pure functions over plain numbers: no Babylon values, no scene, no state.
-- */
--import type { StaticObjectV2 } from '@openvibe/content';
--/**
-- * Which box face a normal belongs to, in Babylon's face order
-- * (0 +z, 1 -z, 2 +x, 3 -x, 4 +y, 5 -y). Painting one wall of a room must
-- * not paint the other five.
-- */
--export declare function faceIndexFromNormal(n: {
-- x: number;
-- y: number;
-- z: number;
--}): number;
--/** Local point on a box face → 0..1 UV across that face. */
--export declare function faceUV(local: {
-- x: number;
-- y: number;
-- z: number;
--}, size: readonly [number, number, number], face: number): {
-- u: number;
-- v: number;
--};
--/** Cylindrical wrap: angle around Y, height along it. Seam at -X. */
--export declare function cylindricalUV(local: {
-- x: number;
-- y: number;
-- z: number;
--}, height: number): {
-- u: number;
-- v: number;
--};
--/**
-- * Spherical wrap. The poles compress to a point, so a stamp there covers a
-- * wide band of u — clamped rather than left to smear, which is the least
-- * surprising of the available wrong answers.
-- */
--export declare function sphericalUV(local: {
-- x: number;
-- y: number;
-- z: number;
--}): {
-- u: number;
-- v: number;
--};
--/** Terrain: local metres → 0..1 across the patch. */
--export declare function planarUV(local: {
-- x: number;
-- z: number;
--}, halfExtent: number): {
-- u: number;
-- v: number;
--};
--/**
-- * Box projection for meshes with no usable UV0.
-- *
-- * The alternative is for Paint to silently do nothing on an imported model,
-- * which is the worst outcome: the user cannot tell whether they missed, the
-- * texture failed to load, or the feature does not work. Projecting from the
-- * dominant axis is approximate — it stretches on faces oblique to that axis
-- * — so the Inspector says so and the Issues panel raises it.
-- */
--export declare function boxProjectionUV(local: {
-- x: number;
-- y: number;
-- z: number;
--}, normal: {
-- x: number;
-- y: number;
-- z: number;
--}, extents: readonly [number, number, number]): {
-- u: number;
-- v: number;
--};
--/** True when a mesh can be painted in its own UV space. */
--export declare function hasUsableUV0(mesh: {
-- isVerticesDataPresent: (kind: string) => boolean;
-- getVerticesData: (kind: string) => Float32Array | number[] | null;
--}): boolean;
--/**
-- * The world-space size of a static's authored shape. For an imported model
-- * this is the proxy volume the importer measured, which is also the space the
-- * box-projection fallback is expressed in — so the brush and the renderer
-- * agree about scale.
-- */
--export declare const staticExtent: (shape: StaticObjectV2["shape"], scale?: readonly number[]) => [number, number, number];
--//# sourceMappingURL=surfaceProjection.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/surfaceProjection.d.ts.map b/apps/client/dist-types/render/surfaceProjection.d.ts.map
ddeleted file mode 100644
iindex d01b300..0000000
---- a/apps/client/dist-types/render/surfaceProjection.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"surfaceProjection.d.ts","sourceRoot":"","sources":["../../src/render/surfaceProjection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAEvD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAOlF;AAED,4DAA4D;AAC5D,wBAAgB,MAAM,CACpB,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC1C,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACvC,IAAI,EAAE,MAAM,GACX;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAgB1B;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAC3B,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC1C,MAAM,EAAE,MAAM,GACb;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAG1B;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAKhG;AAED,qDAAqD;AACrD,wBAAgB,QAAQ,CACtB,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC/B,UAAU,EAAE,MAAM,GACjB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAK1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC1C,MAAM,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC3C,OAAO,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GACzC;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAE1B;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,qBAAqB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAChD,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,YAAY,GAAG,MAAM,EAAE,GAAG,IAAI,CAAA;CAClE,GAAG,OAAO,CAWV;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GACvB,OAAO,cAAc,CAAC,OAAO,CAAC,EAC9B,QAAO,SAAS,MAAM,EAAc,KACnC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAKwE,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/viewmodel.d.ts b/apps/client/dist-types/render/viewmodel.d.ts
ddeleted file mode 100644
iindex 2cb5ab3..0000000
---- a/apps/client/dist-types/render/viewmodel.d.ts
++++ /dev/null
@@@ -1,66 +0,0 @@
--import { Vector3 } from '@babylonjs/core/Maths/math.vector.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import '@babylonjs/loaders/OBJ/objFileLoader.js';
--import type { UniversalCamera } from '@babylonjs/core/Cameras/universalCamera.js';
--import type { ContentRegistry } from '@openvibe/content';
--import type { Appearance } from '@openvibe/protocol';
--/**
-- * First-person viewmodel: the equipped tool rendered at the camera with
-- * smooth sway (mouse lag), movement bob, and equip/swing motions. The
-- * physgun uses the imported OBJ + texture; other tools use the procedural
-- * props. Purely cosmetic — no gameplay reads anything from here.
-- */
--export declare class Viewmodel {
-- private readonly scene;
-- private readonly content;
-- private readonly rig;
-- private physgunMeshes;
-- private toolProp;
-- private currentItem;
-- private physgunLoaded;
-- private swayYaw;
-- private swayPitch;
-- private bobPhase;
-- private equipT;
-- private swingT;
-- constructor(scene: Scene, content: ContentRegistry, camera: UniversalCamera, appearance: Appearance);
-- /**
-- * First-person hands in the player's own skin tone: a forearm reaching in
-- * from the lower right, gripping under the tool. Shown whenever an item
-- * is equipped.
-- */
-- private hands;
-- /**
-- * One first-person arm, matching the avatar's clean low-poly look: a
-- * single straight chain (mitt fist -> skin wrist -> outfit sleeve) that
-- * shares ONE rotation, so there are no open seams or stray flaps. `m`
-- * mirrors offsets for the left arm (never negative scaling — that flips
-- * winding and culls the mesh). Rig is yawed PI: local +x renders LEFT,
-- * local -z is world-forward.
-- */
-- private buildArm;
-- private leftFist;
-- private rightArm;
-- /** Alternating punch: which fist jabs next, and the live thrust node. */
-- private punchLeft;
-- private punchingFist;
-- private buildHands;
-- /** Armed: one support hand under the tool. Unarmed: boxer guard. */
-- private layoutHands;
-- private loadPhysgun;
-- private setPhysgunVisible;
-- triggerSwing(): void;
-- /** Per-frame update. mouseDx/Dy are this frame's look deltas (radians). */
-- update(dt: number, speed: number, grounded: boolean, mouseDx: number, mouseDy: number): void;
-- /** Switch displayed tool when the equipped item changes. */
-- setItem(itemDef: string | null): void;
-- /**
-- * The whole viewmodel renders in group 1: drawn AFTER world geometry and
-- * beams (group 0), so the gun never clips into walls and beams never
-- * overlap the gun — they visually emerge from behind its tip.
-- */
-- private applyRenderGroup;
-- /** World-space beam origin (the tool's muzzle tip). */
-- beamOrigin(): Vector3;
--}
--//# sourceMappingURL=viewmodel.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/viewmodel.d.ts.map b/apps/client/dist-types/render/viewmodel.d.ts.map
ddeleted file mode 100644
iindex 7a85ab1..0000000
---- a/apps/client/dist-types/render/viewmodel.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"viewmodel.d.ts","sourceRoot":"","sources":["../../src/render/viewmodel.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAA;AAG9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,yCAAyC,CAAA;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAA;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAMpD;;;;;GAKG;AACH,qBAAa,SAAS;IAelB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAf1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,WAAW,CAAsB;IACzC,OAAO,CAAC,aAAa,CAAQ;IAG7B,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,MAAM,CAAI;gBAGC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,eAAe,EACzC,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,UAAU;IAYxB;;;;OAIG;IACH,OAAO,CAAC,KAAK,CAA6B;IAE1C;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ;IA4EhB,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,QAAQ,CAA6B;IAC7C,yEAAyE;IACzE,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,YAAY,CAA6B;IAEjD,OAAO,CAAC,UAAU;IAclB,oEAAoE;IACpE,OAAO,CAAC,WAAW;YAmBL,WAAW;IAkDzB,OAAO,CAAC,iBAAiB;IAIzB,YAAY,IAAI,IAAI;IAUpB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAgD5F,4DAA4D;IAC5D,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IA4CrC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAQxB,uDAAuD;IACvD,UAAU,IAAI,OAAO;CAQtB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/water.d.ts b/apps/client/dist-types/render/water.d.ts
ddeleted file mode 100644
iindex 82edd90..0000000
---- a/apps/client/dist-types/render/water.d.ts
++++ /dev/null
@@@ -1,25 +0,0 @@
--import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js';
--import type { Scene } from '@babylonjs/core/scene.js';
--import { WaterMaterial } from '@babylonjs/materials/water/waterMaterial.js';
--/**
-- * The island's water: ONE animated reflective sheet at WATER_LEVEL —
-- * terrain dips below it become lakes, and past the map edge it reads as
-- * the open ocean. Reflection/refraction sample the real scene (terrain,
-- * city, props), driven by Babylon's WaterMaterial and the official
-- * waterbump normal texture. Deeper systems (shore foam, underwater fog,
-- * swimming) layer on later — this is the v1 the rest builds on.
-- */
--export declare class Water {
-- readonly material: WaterMaterial;
-- private readonly mesh;
-- constructor(scene: Scene);
-- /** Meshes the water reflects/refracts (terrain, statics, skirt...). */
-- addToRenderList(mesh: AbstractMesh): void;
-- /**
-- * A still-water material (fountain pools, troughs): animated ripples via
-- * the bump texture only — zero wave displacement, which shreds small
-- * low-tessellation surfaces into spikes.
-- */
-- makeCalmSurface(name: string): WaterMaterial;
--}
--//# sourceMappingURL=water.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/render/water.d.ts.map b/apps/client/dist-types/render/water.d.ts.map
ddeleted file mode 100644
iindex 0cb516c..0000000
---- a/apps/client/dist-types/render/water.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"water.d.ts","sourceRoot":"","sources":["../../src/render/water.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAA;AAE1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,6CAA6C,CAAA;AAG3E;;;;;;;GAOG;AACH,qBAAa,KAAK;IAChB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAA;IAChC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAM;gBAEf,KAAK,EAAE,KAAK;IAoBxB,uEAAuE;IACvE,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAIzC;;;;OAIG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;CAa7C"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/state/clientState.d.ts b/apps/client/dist-types/state/clientState.d.ts
ddeleted file mode 100644
iindex 470f5d8..0000000
---- a/apps/client/dist-types/state/clientState.d.ts
++++ /dev/null
@@@ -1,157 +0,0 @@
--import type { ServerConstraintState, ServerMessage, WireEntity, WireInventory, WireSkill } from '@openvibe/protocol';
--import { TypedEmitter } from '@openvibe/shared';
--/**
-- * Replicated game state as this client knows it, decoupled from both the
-- * network layer (which writes it) and rendering/UI (which read it via
-- * events or polling). No Babylon types in here.
-- */
--export interface ClientStateEvents {
-- welcome: {
-- entityId: string;
-- tickRate: number;
-- };
-- entityAdded: WireEntity;
-- entityRemoved: string;
-- entityUpdated: WireEntity;
-- inventory: {
-- inv: WireInventory;
-- activeHotbar: number;
-- };
-- craftJobs: {
-- recipe: string;
-- readyTick: number;
-- }[];
-- actionResult: {
-- action: string;
-- ok: boolean;
-- error?: string;
-- };
-- physgunBeam: {
-- player: string;
-- target: string | null;
-- };
-- skills: WireSkill[];
-- levelUp: {
-- skill: string;
-- level: number;
-- };
-- constraintState: ServerConstraintState;
-- tracer: {
-- shooter: string;
-- from: [number, number, number];
-- to: [number, number, number];
-- hit: boolean;
-- };
-- armorChanged: undefined;
-- friendsChanged: {
-- id: string;
-- name: string;
-- }[];
-- stats: {
-- hp: number;
-- hunger: number;
-- thirst: number;
-- stamina: number;
-- temp: number;
-- statuses: string[];
-- died?: boolean;
-- };
-- timeSync: number;
-- weather: string;
-- container: {
-- id: string;
-- size: number;
-- slots: {
-- i: number;
-- def: string;
-- count: number;
-- }[];
-- };
-- announce: string;
-- market: unknown;
-- jobs: unknown;
-- reputation: {
-- id: string;
-- name: string;
-- value: number;
-- stance: string;
-- }[];
-- fx: {
-- kind: 'hurt' | 'death';
-- id: string;
-- };
-- disconnected: undefined;
-- [key: string]: unknown;
--}
--export declare class ClientState {
-- readonly events: TypedEmitter<ClientStateEvents>;
-- readonly entities: Map<string, WireEntity>;
-- myEntityId: string;
-- /** OpenVibe rank from welcome (owner/admin/moderator, null = player). */
-- myRank: 'owner' | 'admin' | 'moderator' | null;
-- myPlayerId: string;
-- /** Player ids I trust with my props. */
-- friends: {
-- id: string;
-- name: string;
-- }[];
-- tickRate: number;
-- snapshotRate: number;
-- serverTick: number;
-- ack: number;
-- inventory: WireInventory | null;
-- /** Worn armor stack (def/count/meta with dur), or null. */
-- armor: {
-- def: string;
-- count: number;
-- meta?: Record<string, number | string>;
-- } | null;
-- activeHotbar: number;
-- /** Mirrors the server's holster toggle (same deterministic rules). */
-- holstered: boolean;
-- craftJobs: {
-- recipe: string;
-- readyTick: number;
-- }[];
-- skills: WireSkill[];
-- /** Unlocked blueprint recipe ids. */
-- unlocks: Set<string>;
-- /** entityId -> holder player entityId, for beam/highlight rendering. */
-- readonly heldBy: Map<string, string>;
-- /** Live constraints touching entities this client knows (for visuals). */
-- readonly constraints: Map<string, ServerConstraintState>;
-- /** Holder entity id -> grab point in the held body's local space. */
-- readonly heldGrab: Map<string, [number, number, number]>;
-- stats: {
-- hp: number;
-- hunger: number;
-- thirst: number;
-- stamina: number;
-- temp: number;
-- statuses: string[];
-- };
-- /** Shared world clock (fraction of the day cycle). */
-- dayFraction: number;
-- /** Authoritative weather (rendering + prompts). */
-- weather: 'clear' | 'cloudy' | 'rain' | 'storm' | 'fog';
-- /** Faction standings (welcome + on change). */
-- reputation: {
-- id: string;
-- name: string;
-- value: number;
-- stance: string;
-- }[];
-- apply(msg: ServerMessage): void;
-- countOf(defId: string): number;
-- /** Item def id in the active hotbar slot (null when holstered). */
-- activeItemDef(): string | null;
-- skillLevel(id: string): number;
-- isFriend(playerId: string): boolean;
-- /** Online players (from replicated player entities with identity meta). */
-- onlinePlayers(): {
-- playerId: string;
-- name: string;
-- entityId: string;
-- }[];
--}
--//# sourceMappingURL=clientState.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/state/clientState.d.ts.map b/apps/client/dist-types/state/clientState.d.ts.map
ddeleted file mode 100644
iindex 599ae44..0000000
---- a/apps/client/dist-types/state/clientState.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"clientState.d.ts","sourceRoot":"","sources":["../../src/state/clientState.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/ui/characterSelect.d.ts b/apps/client/dist-types/ui/characterSelect.d.ts
ddeleted file mode 100644
iindex 6a74357..0000000
---- a/apps/client/dist-types/ui/characterSelect.d.ts
++++ /dev/null
@@@ -1,17 +0,0 @@
--import type { Appearance } from '@openvibe/protocol';
--/**
-- * MMO-style character select: an account token owns up to three characters.
-- * Pick one to play, or claim an empty slot (which runs the customization
-- * screen). Pure DOM overlay — resolves with the chosen slot and, for
-- * existing characters, their saved identity.
-- */
--export interface CharacterInfo {
-- slot: number;
-- name: string;
-- appearance: Appearance | null;
--}
--export declare function characterSelect(uiRoot: HTMLElement, characters: CharacterInfo[], authed: boolean): Promise<{
-- slot: number;
-- existing: CharacterInfo | null;
--}>;
--//# sourceMappingURL=characterSelect.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/ui/characterSelect.d.ts.map b/apps/client/dist-types/ui/characterSelect.d.ts.map
ddeleted file mode 100644
iindex 38cc6ec..0000000
---- a/apps/client/dist-types/ui/characterSelect.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"characterSelect.d.ts","sourceRoot":"","sources":["../../src/ui/characterSelect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,UAAU,GAAG,IAAI,CAAA;CAC9B;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,aAAa,EAAE,EAC3B,MAAM,EAAE,OAAO,GACd,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;CAAE,CAAC,CA6D3D"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/ui/customizeScreen.d.ts b/apps/client/dist-types/ui/customizeScreen.d.ts
ddeleted file mode 100644
iindex 89c23b6..0000000
---- a/apps/client/dist-types/ui/customizeScreen.d.ts
++++ /dev/null
@@@ -1,14 +0,0 @@
--import type { Scene } from '@babylonjs/core/scene.js';
--import type { ContentRegistry } from '@openvibe/content';
--import { type Appearance } from '@openvibe/protocol';
--/**
-- * Pre-join character customization: a live 3D preview of the parametric
-- * avatar in the world plaza with full appearance controls. Resolves with
-- * the chosen name + appearance (persisted to localStorage).
-- */
--export declare function customizeScreen(scene: Scene, content: ContentRegistry, uiRoot: HTMLElement, savedName: string | null, guest?: boolean): Promise<{
-- name: string;
-- appearance: Appearance;
-- releaseCamera: () => void;
--}>;
--//# sourceMappingURL=customizeScreen.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/ui/customizeScreen.d.ts.map b/apps/client/dist-types/ui/customizeScreen.d.ts.map
ddeleted file mode 100644
iindex 264d730..0000000
---- a/apps/client/dist-types/ui/customizeScreen.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"customizeScreen.d.ts","sourceRoot":"","sources":["../../src/ui/customizeScreen.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AASrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAA;AAI3B;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,KAAK,UAAQ,GACZ,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,UAAU,CAAC;IAAC,aAAa,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC,CAgP9E"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/ui/hud.d.ts b/apps/client/dist-types/ui/hud.d.ts
ddeleted file mode 100644
iindex adeb04e..0000000
---- a/apps/client/dist-types/ui/hud.d.ts
++++ /dev/null
@@@ -1,81 +0,0 @@
--import type { ContentRegistry } from '@openvibe/content';
--import type { Connection } from '../net/connection.js';
--import type { ClientState } from '../state/clientState.js';
--import type { IconFactory } from './iconFactory.js';
--import { type WeaponSettings } from '../weapons/registry.js';
--export declare class Hud {
-- private readonly state;
-- private readonly content;
-- private readonly connection;
-- private readonly icons;
-- private readonly weaponSettings;
-- private root;
-- private hotbarEl;
-- private menuEl;
-- private menuBodyEl;
-- private promptEl;
-- private statusEl;
-- private toastArea;
-- menuOpen: boolean;
-- private activeTab;
-- private dragFrom;
-- onUiCaptureChange: ((captured: boolean) => void) | null;
-- constructor(root: HTMLElement, state: ClientState, content: ContentRegistry, connection: Connection, icons: IconFactory, weaponSettings: WeaponSettings);
-- private build;
-- private openContainerId;
-- private containerData;
-- private lastHp;
-- private hitmarkerTimer;
-- /** Brief crosshair X when a melee hit lands on another player. */
-- flashHitmarker(): void;
-- private flashDamage;
-- private shopOpen;
-- private showDeathScreen;
-- /** Merchant trade sheet (content-driven; server validates every trade). */
-- /** Shop entity currently open (market requests target it). */
-- private shopTargetId;
-- /** Latest market data from the server. */
-- private marketData;
-- /** Contracts at the open trading post. */
-- private jobsData;
-- openShop(targetId: string): void;
-- setMarket(data: NonNullable<typeof this.marketData>): void;
-- closeShop(): void;
-- private renderShop;
-- private announceTimer;
-- private showAnnounce;
-- private renderVitals;
-- /** Opens (or refreshes) the storage panel for a container entity. */
-- showContainer(id: string, size: number, slots: {
-- i: number;
-- def: string;
-- count: number;
-- }[]): void;
-- closeContainer(): void;
-- get containerOpen(): boolean;
-- private renderContainer;
-- private byId;
-- toggleMenu(): void;
-- closeAll(): void;
-- private setTab;
-- private editMode;
-- private renderTabs;
-- renderMenu(): void;
-- /** Faction standings: who likes you, who wants you dead. */
-- private renderStanding;
-- private slotStack;
-- private dropToWorld;
-- private slotEl;
-- private firstFreeSlot;
-- renderHotbar(): void;
-- private renderInventory;
-- private renderCrafting;
-- /** Modular per-weapon settings/info — modules register per tool kind. */
-- private renderEquipment;
-- private renderSkills;
-- private renderPlayers;
-- setPrompt(text: string | null): void;
-- setStatus(text: string): void;
-- toast(text: string, isError?: boolean): void;
--}
--//# sourceMappingURL=hud.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/ui/hud.d.ts.map b/apps/client/dist-types/ui/hud.d.ts.map
ddeleted file mode 100644
iindex 5c963ad..0000000
---- a/apps/client/dist-types/ui/hud.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"hud.d.ts","sourceRoot":"","sources":["../../src/ui/hud.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/ui/iconFactory.d.ts b/apps/client/dist-types/ui/iconFactory.d.ts
ddeleted file mode 100644
iindex aec7f0c..0000000
---- a/apps/client/dist-types/ui/iconFactory.d.ts
++++ /dev/null
@@@ -1,14 +0,0 @@
--import { Scene } from '@babylonjs/core/scene.js';
--import type { ContentRegistry } from '@openvibe/content';
--export declare class IconFactory {
-- private readonly content;
-- onReady: (() => void) | null;
-- private readonly cache;
-- private readonly queued;
-- private chain;
-- private readonly iconScene;
-- constructor(scene: Scene, content: ContentRegistry);
-- iconFor(defId: string): string;
-- private generate;
--}
--//# sourceMappingURL=iconFactory.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/ui/iconFactory.d.ts.map b/apps/client/dist-types/ui/iconFactory.d.ts.map
ddeleted file mode 100644
iindex faa8e7c..0000000
---- a/apps/client/dist-types/ui/iconFactory.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"iconFactory.d.ts","sourceRoot":"","sources":["../../src/ui/iconFactory.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAmBxD,qBAAa,WAAW;IASpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAO;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4B;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,KAAK,CAAmC;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAO;gBAG/B,KAAK,EAAE,KAAK,EACK,OAAO,EAAE,eAAe;IAY3C,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;YAYhB,QAAQ;CAiDvB"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/weapons/physgunModule.d.ts b/apps/client/dist-types/weapons/physgunModule.d.ts
ddeleted file mode 100644
iindex 09289e8..0000000
---- a/apps/client/dist-types/weapons/physgunModule.d.ts
++++ /dev/null
@@@ -1,10 +0,0 @@
--import { type WeaponSettings } from './registry.js';
--/**
-- * Physgun equipment module: building-related tuning. Values are read by
-- * the interaction controller when it sends grid/rotate intents — the
-- * server clamps everything, so these are conveniences, not authority.
-- */
--export declare function physgunGridSize(settings: WeaponSettings): number;
--export declare function physgunSnapDeg(settings: WeaponSettings): number;
--export declare function registerPhysgunModule(): void;
--//# sourceMappingURL=physgunModule.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/weapons/physgunModule.d.ts.map b/apps/client/dist-types/weapons/physgunModule.d.ts.map
ddeleted file mode 100644
iindex dc842bd..0000000
---- a/apps/client/dist-types/weapons/physgunModule.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"physgunModule.d.ts","sourceRoot":"","sources":["../../src/weapons/physgunModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AAE/F;;;;GAIG;AAEH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAEhE;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAE/D;AA4CD,wBAAgB,qBAAqB,IAAI,IAAI,CAO5C"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/weapons/registry.d.ts b/apps/client/dist-types/weapons/registry.d.ts
ddeleted file mode 100644
iindex 4c191dd..0000000
---- a/apps/client/dist-types/weapons/registry.d.ts
++++ /dev/null
@@@ -1,37 +0,0 @@
--/**
-- * Modular weapon/equipment system (client side).
-- *
-- * Every tool kind can register a module: a settings/info panel that the
-- * Tab menu's Equipment tab renders for the currently equipped item, backed
-- * by a persistent per-weapon settings store. Future weapons (guns, melee,
-- * tool-gun modes...) plug in here without touching HUD or controller code —
-- * gameplay consequences of a setting always flow through the protocol so
-- * the server stays authoritative.
-- */
--export declare class WeaponSettings {
-- private data;
-- constructor();
-- get<T>(weapon: string, key: string, fallback: T): T;
-- set(weapon: string, key: string, value: unknown): void;
--}
--export interface WeaponModule {
-- kind: string;
-- title: string;
-- /** Renders the settings/info panel into `el`. */
-- buildPanel(el: HTMLElement, settings: WeaponSettings): void;
-- /**
-- * Weapon emits a muzzle-anchored beam/tracer while its trigger is held.
-- * The shared pipeline (viewmodel muzzle origin -> crosshair raycast end,
-- * bright + flare when latched) renders it — future beam/projectile
-- * weapons opt in here instead of adding bespoke render code.
-- */
-- firesBeam?: boolean;
--}
--export declare function registerWeaponModule(module: WeaponModule): void;
--export declare function weaponModuleFor(kind: string | null): WeaponModule | undefined;
--export declare function optionRow(el: HTMLElement, label: string, choices: {
-- label: string;
-- value: number;
--}[], current: number, onPick: (value: number) => void): void;
--export declare function infoLines(el: HTMLElement, lines: string[]): void;
--//# sourceMappingURL=registry.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/weapons/registry.d.ts.map b/apps/client/dist-types/weapons/registry.d.ts.map
ddeleted file mode 100644
iindex 2047d1c..0000000
---- a/apps/client/dist-types/weapons/registry.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/weapons/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAA8C;;IAW1D,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC;IAKnD,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;CAIvD;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,iDAAiD;IACjD,UAAU,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAA;IAC3D;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAID,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAE/D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,YAAY,GAAG,SAAS,CAE7E;AAID,wBAAgB,SAAS,CACvB,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,EAC3C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAC9B,IAAI,CAkBN;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAOhE"}
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/weapons/riggingModule.d.ts b/apps/client/dist-types/weapons/riggingModule.d.ts
ddeleted file mode 100644
iindex b37c734..0000000
---- a/apps/client/dist-types/weapons/riggingModule.d.ts
++++ /dev/null
@@@ -1,18 +0,0 @@
--import { type WeaponSettings } from './registry.js';
--/**
-- * Rigging tool equipment module: which constraint the next two clicks
-- * create, plus per-type tuning. Pure convenience — the server validates
-- * the type, skill gate, materials and every parameter.
-- */
--export type RiggingKind = 'weld' | 'rope' | 'hinge' | 'axis' | 'slider' | 'spring' | 'motor';
--export declare function riggingKind(settings: WeaponSettings): RiggingKind;
--/** Extra slack added to the measured anchor gap for ropes (fraction). */
--export declare function riggingRopeSlack(settings: WeaponSettings): number;
--/** Hinge swing limit in degrees each way (0 = free). */
--export declare function riggingHingeLimitDeg(settings: WeaponSettings): number;
--/** Slider travel in meters each way (0 = unbounded). */
--export declare function riggingSliderTravel(settings: WeaponSettings): number;
--export declare function riggingSpringStiffness(settings: WeaponSettings): number;
--export declare function riggingMotorSpeed(settings: WeaponSettings): number;
--export declare function registerRiggingModule(): void;
--//# sourceMappingURL=riggingModule.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/client/dist-types/weapons/riggingModule.d.ts.map b/apps/client/dist-types/weapons/riggingModule.d.ts.map
ddeleted file mode 100644
iindex 15c077e..0000000
---- a/apps/client/dist-types/weapons/riggingModule.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"riggingModule.d.ts","sourceRoot":"","sources":["../../src/weapons/riggingModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8C,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AAE/F;;;;GAIG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAA;AAY5F,wBAAgB,WAAW,CAAC,QAAQ,EAAE,cAAc,GAAG,WAAW,CAGjE;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAEjE;AAED,wDAAwD;AACxD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAErE;AAED,wDAAwD;AACxD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAEpE;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAEvE;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAElE;AAkHD,wBAAgB,qBAAqB,IAAI,IAAI,CAM5C"}
\\ No newline at end of file
ddiff --git a/apps/client/e2e/editorCheck.ts b/apps/client/e2e/editorCheck.ts
iindex e932dd7..d60d5f5 100644
---- a/apps/client/e2e/editorCheck.ts
++++ b/apps/client/e2e/editorCheck.ts
@@@ -960,39 +960,25 @@ await withMap(
)
// ════════════════════════════════════════════════════════════════════════
--section('N. v1 migration smoke — old maps still load')
++section('N. A v1 map is not a map — the editor opens empty')
{
-- const v1flat = new Float32Array((SUB + 1) * (SUB + 1)).fill(1.5)
-- const v1b64 = Buffer.from(
-- new Uint8Array(v1flat.buffer, v1flat.byteOffset, v1flat.byteLength),
-- ).toString('base64')
await withMap(
PORT + 2,
{
v: 1,
halfExtent: HALF,
sub: SUB,
-- heights: v1b64,
++ heights: '',
statics: [
{ shape: { type: 'box', size: [4, 4, 4] }, pos: [6, 2, 0], yaw: 0, color: '#c04040' },
],
},
async (pg) => {
++ // Map v2 is the only format (ADR-0007): nothing converts an old document.
const ids = await probeOf(pg, (p) => p.terrainIds())
-- // The old privileged heightfield is now ONE ordinary terrain object.
-- ok('the v1 main heightfield became an ordinary terrain', ids.includes('terrain-v1-main'), ids)
-- ok('exactly one terrain came across', ids.length === 1, ids)
++ ok('no terrain was made from the v1 document', ids.length === 0, ids)
const counts = await probeOf(pg, (p) => p.objectCounts())
-- ok('the v1 static came across', counts['statics'] === 1, counts)
-- // Nothing treats the migrated terrain as special: it selects like any
-- // other object, under its real id.
-- await probeOf(pg, (p) => p.setToolByName('select'))
-- await probeOf(pg, (p) => p.selectByIds(['terrain-v1-main']))
-- ok(
-- 'it selects under its real id, with no "main" special case',
-- (await probeOf(pg, (p) => p.selectionIds())).includes('terrain-v1-main'),
-- await probeOf(pg, (p) => p.selectionIds()),
-- )
++ ok('no static came across', counts['statics'] === 0, counts)
},
)
}
ddiff --git a/apps/client/src/editor/architectureAudit.audit.ts b/apps/client/src/editor/architectureAudit.audit.ts
iindex cdc5d4d..4a9b1bd 100644
---- a/apps/client/src/editor/architectureAudit.audit.ts
++++ b/apps/client/src/editor/architectureAudit.audit.ts
@@@ -8,8 +8,7 @@
* a test of its own or it quietly becomes permanent.
*
* This audit fails while any legacy construct is still ACTIVE runtime code.
-- * Migration code, migration tests and documentation are explicitly allowed:
-- * one-way v1 → v2 compatibility is the intended end state.
++ * A mention in prose is not active code, so comments do not trip it.
*/
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { extname, join, relative } from 'node:path'
@@@ -20,10 +19,6 @@ const SCAN_ROOTS = ['apps/client/src', 'apps/server/src', 'packages/content/src'
/** Files that legitimately mention legacy constructs. */
const ALLOWED = [
-- // One-way v1 → v2 compatibility lives here by design.
-- 'packages/content/src/mapFile.ts',
-- 'packages/content/src/mapFileV2.ts',
-- 'packages/content/src/mapFileV2.test.ts',
// The audit itself names everything it forbids.
'apps/client/src/editor/architectureAudit.audit.ts',
]
ddiff --git a/apps/client/src/editor/editorApp.ts b/apps/client/src/editor/editorApp.ts
iindex 3b68331..d2c7181 100644
---- a/apps/client/src/editor/editorApp.ts
++++ b/apps/client/src/editor/editorApp.ts
@@@ -110,9 +110,9 @@ export async function bootEditor(): Promise<void> {
env.attachCamera(camera)
// ── Document ────────────────────────────────────────────────────────
-- // /map.json is native v2; parseMapFile migrates a legacy v1 artifact on
-- // the way in, so the editor only ever holds v2 — and a missing or invalid
-- // map is emptyMapV2(), never a fabricated starter heightfield.
++ // /map.json is the v2 document, so the editor only ever holds v2 — and a
++ // missing or invalid map is emptyMapV2(), never a fabricated starter
++ // heightfield.
const bootResp = await fetch('/map.json')
const bootRevision = bootResp.headers.get('etag')?.replace(/"/g, '') ?? ''
const bootParsed = parseMapFile(await bootResp.json().catch(() => null))
ddiff --git a/apps/client/src/editor/net/saveController.ts b/apps/client/src/editor/net/saveController.ts
iindex 4b132ac..b0c8ed4 100644
---- a/apps/client/src/editor/net/saveController.ts
++++ b/apps/client/src/editor/net/saveController.ts
@@@ -113,8 +113,7 @@ export function createSaveController(opts: SaveControllerOptions): SaveControlle
const key = editorKey()
// Persist only a hand-typed override token — never the SSO session,
// which lives in ovg_sso and is resolved fresh on every request.
-- const manual =
-- (document.getElementById('key') as HTMLInputElement | null)?.value.trim() ?? ''
++ const manual = (document.getElementById('key') as HTMLInputElement | null)?.value.trim() ?? ''
localStorage.setItem('openvibe.editorkey', manual)
setMessage('saving…')
await uploadDirtyMasks()
@@@ -262,8 +261,7 @@ export function createSaveController(opts: SaveControllerOptions): SaveControlle
setMessage('⛔ that file is not JSON')
return
}
-- // v1 migrates, v2 validates; either way the issues are reported BEFORE
-- // anything local is replaced.
++ // The document is validated BEFORE anything local is replaced.
const parsed = parseMapFile(raw)
if (!parsed.ok) {
setMessage(`⛔ import rejected: ${parsed.issues.slice(0, 3).join('; ')}`)
@@@ -272,9 +270,7 @@ export function createSaveController(opts: SaveControllerOptions): SaveControlle
if (isDirty() && !confirm('Replace your unsaved work with the imported map?')) return
opts.onAdopt(parsed.map)
nonHistoryDirt = true
-- setMessage(
-- parsed.migrated ? '📥 imported (migrated from v1)' : '📥 imported — Save to publish',
-- )
++ setMessage('📥 imported — Save to publish')
notify()
},
conflict: () => ({ open: conflictOpen, summary: conflictSummary }),
ddiff --git a/apps/client/src/editor/viewport/views/index.ts b/apps/client/src/editor/viewport/views/index.ts
iindex 6620686..6fcc7ba 100644
---- a/apps/client/src/editor/viewport/views/index.ts
++++ b/apps/client/src/editor/viewport/views/index.ts
@@@ -24,7 +24,6 @@ import {
buildPatchGrid,
decodeHeights,
encodeHeights,
-- migrateLegacyMix,
type ContentRegistry,
type MapLightV2,
type MapNodeV2,
@@@ -580,15 +579,13 @@ export class TerrainView implements EditorView {
/** The document surface, defaulted so a plain terrain still paints. */
surfaceData(): SurfaceMaterialData {
-- const t = this.terrain as TerrainObjectV2 & { tex?: string; color?: string; mix?: string }
++ const t = this.terrain as TerrainObjectV2 & { tex?: string; color?: string }
if (t.surface) return t.surface as SurfaceMaterialData
-- let seq = 0
return {
base: {
...(t.tex && t.tex !== 'none' ? { tex: t.tex } : {}),
...(t.color ? { color: t.color } : {}),
},
-- ...(t.mix ? { paint: migrateLegacyMix(t.mix, () => `pl-${this.id}-${seq++}`)! } : {}),
}
}
ddiff --git a/apps/client/src/main.ts b/apps/client/src/main.ts
iindex e844c70..2c97b98 100644
---- a/apps/client/src/main.ts
++++ b/apps/client/src/main.ts
@@@ -65,8 +65,7 @@ async function start(): Promise<void> {
let liveMap: MapFileV2 = emptyMapV2()
let mapLightsBoot: MapLight[] | undefined
try {
-- // /map.json is native v2. parseMapFile still accepts a legacy v1 file
-- // and migrates it, so old artifacts keep working — one direction only.
++ // /map.json is the v2 document, parsed and validated on the way in.
const parsed = parseMapFile(await (await fetch('/map.json')).json())
if (parsed.ok) {
registerMapAssets({
@@@ -332,9 +331,9 @@ async function start(): Promise<void> {
// A stale cached bundle is talking to a newer server. A reload
// revalidates the page and pulls the new client. Guard against a
// reload loop if something still pins the old version.
-- const last = Number(localStorage.getItem('hq_reload_ts') ?? '0')
++ const last = Number(localStorage.getItem('openvibe.reload_ts') ?? '0')
if (Date.now() - last > 60_000) {
-- localStorage.setItem('hq_reload_ts', String(Date.now()))
++ localStorage.setItem('openvibe.reload_ts', String(Date.now()))
hud.setStatus('game updated — loading the new version…')
hud.toast('Game updated! Reloading…', false)
setTimeout(() => location.reload(), 1200)
@@@ -347,9 +346,9 @@ async function start(): Promise<void> {
localStorage.removeItem('ovg_sso')
document.cookie = 'ovg_sso=; Path=/; Max-Age=0'
localStorage.removeItem('openvibe.appearance')
-- const last = Number(localStorage.getItem('hq_reload_ts') ?? '0')
++ const last = Number(localStorage.getItem('openvibe.reload_ts') ?? '0')
if (Date.now() - last > 60_000) {
-- localStorage.setItem('hq_reload_ts', String(Date.now()))
++ localStorage.setItem('openvibe.reload_ts', String(Date.now()))
hud.setStatus('session data was out of date — reloading…')
setTimeout(() => location.reload(), 1200)
} else {
ddiff --git a/apps/client/src/net/connection.ts b/apps/client/src/net/connection.ts
iindex 7566759..8983c65 100644
---- a/apps/client/src/net/connection.ts
++++ b/apps/client/src/net/connection.ts
@@@ -33,11 +33,21 @@ export class Connection {
// An upgrade sent while the server restarts can wait with no answer (the proxy holds it):
// give up after CONNECT_TIMEOUT_MS so the page can wait for the server and reload instead.
const timer = setTimeout(() => {
-- try { ws.close() } catch { /* already closing */ }
++ try {
++ ws.close()
++ } catch {
++ /* already closing */
++ }
reject(new Error('connection timed out'))
}, CONNECT_TIMEOUT_MS)
-- ws.onopen = () => { clearTimeout(timer); resolve() }
-- ws.onerror = () => { clearTimeout(timer); reject(new Error('connection failed')) }
++ ws.onopen = () => {
++ clearTimeout(timer)
++ resolve()
++ }
++ ws.onerror = () => {
++ clearTimeout(timer)
++ reject(new Error('connection failed'))
++ }
})
ws.onmessage = (event) => {
const msg = decodeServerMessage(String(event.data))
@@@ -68,21 +78,20 @@ export class Connection {
/** Persistent anonymous identity (interim auth — see ADR-0004). */
/**
-- * Guest identity token: localStorage primary, a long-lived cookie as
-- * backup (survives localStorage wipes), and the server additionally maps
-- * the token to the client IP — three chances to keep a guest's scrapper.
++ * Guest identity token: localStorage primary, a long-lived cookie as backup
++ * (survives localStorage wipes).
*/
export function getIdentity(): { token: string; name: string | null } {
const cookieTok = document.cookie
.split('; ')
-- .find((c) => c.startsWith('hq_token='))
-- ?.slice('hq_token='.length)
++ .find((c) => c.startsWith('openvibe.token='))
++ ?.slice('openvibe.token='.length)
let token = localStorage.getItem('openvibe.token') ?? cookieTok ?? null
if (!token || !/^[a-z0-9]{8,64}$/i.test(token)) {
token = crypto.randomUUID().replaceAll('-', '').slice(0, 32)
}
localStorage.setItem('openvibe.token', token)
-- document.cookie = `hq_token=${token}; Path=/; Max-Age=${400 * 86400}; SameSite=Lax`
++ document.cookie = `openvibe.token=${token}; Path=/; Max-Age=${400 * 86400}; SameSite=Lax`
return { token, name: localStorage.getItem('openvibe.name') }
}
ddiff --git a/apps/client/src/render/sceneSetup.ts b/apps/client/src/render/sceneSetup.ts
iindex 957c438..c64591c 100644
---- a/apps/client/src/render/sceneSetup.ts
++++ b/apps/client/src/render/sceneSetup.ts
@@@ -29,7 +29,6 @@ import {
type WorldShape,
type StaticBody,
effectiveShape,
-- migrateLegacyMix,
type SurfaceMaterialData,
type StaticObjectV2,
} from '@openvibe/content'
@@@ -189,7 +188,6 @@ function childAtIndexPath(root: { getChildren?: () => unknown[] }, path: string)
}
/** Extra terrain patches (mountains, cave shells) from the edited map. */
--let legacyLayer = 0
export function buildTerrainPatches(
scene: Scene,
content: ContentRegistry,
@@@ -215,20 +213,8 @@ export function buildTerrainPatches(
// applies mesh scaling in the same S→R→T order, so the two agree.
if (patch.scale) mesh.scaling.set(patch.scale[0], patch.scale[1], patch.scale[2])
// v2 surfaces (base + paint layers) render with the SAME material class
-- // the editor uses, so what was painted is what players see. Legacy `mix`
-- // maps onto the same shader via migrateLegacyMix.
-- const surface: SurfaceMaterialData | null = patch.surface
-- ? patch.surface
-- : patch.mix
-- ? {
-- base: {
-- ...(patch.tex && patch.tex !== 'none' ? { tex: patch.tex } : {}),
-- ...(patch.color ? { color: patch.color } : {}),
-- ...(patch.uv ? { uv: patch.uv } : {}),
-- },
-- paint: migrateLegacyMix(patch.mix, () => `pl-${patch.id}-${legacyLayer++}`)!,
-- }
-- : null
++ // the editor uses, so what was painted is what players see.
++ const surface: SurfaceMaterialData | null = patch.surface ?? null
if (surface) {
const layered = new LayeredSurfaceMaterial(scene, `psurf:${patch.id}`, surface, {
baseTiling: Math.max(2, patch.halfExtent / 2),
ddiff --git a/apps/server/dist-types/config.d.ts b/apps/server/dist-types/config.d.ts
ddeleted file mode 100644
iindex 6020b60..0000000
---- a/apps/server/dist-types/config.d.ts
++++ /dev/null
@@@ -1,64 +0,0 @@
--export interface ServerConfig {
-- port: number;
-- host: string;
-- dbPath: string;
-- /** Directory of built client assets to serve, or null for API/WS only. */
-- staticDir: string | null;
-- mapPath: string;
-- editorKey: string | null;
-- networkAuthUrl: string | null;
-- /** Bind fresh guest tokens to client IPs (off in tests: peers share an IP). */
-- guestIpBinding: boolean;
-- /** openvibe.network OAuth client (SSO); null until the secret is configured. */
-- oauth: {
-- clientId: string;
-- clientSecret: string;
-- baseUrl: string;
-- selfUrl: string;
-- /** Public base of the play host (Host-routed game vhost). */
-- playUrl: string;
-- } | null;
-- /**
-- * Platform integration (roadmap Wave 12): Games calls OpenVibe services with
-- * a client-credentials token of its own `games` principal. Everything here
-- * is off until OV_OAUTH_CLIENT_SECRET (the same OAuth client as SSO) is set.
-- */
-- platform: PlatformConfig;
-- tickRate: number;
-- /** Send a snapshot every N ticks. */
-- snapshotEvery: number;
-- /** Entities beyond this distance from a player are not replicated to them. */
-- interestRadius: number;
-- maxPlayers: number;
-- persistFlushSeconds: number;
-- metricsLogSeconds: number;
-- /** Multiplier on world-event cadences (tests shrink it). */
-- eventIntervalScale: number;
--}
--export interface PlatformConfig {
-- /** OAuth client id of the `games` principal (shared with SSO). */
-- clientId: string;
-- /** Its client secret; null = no service calls at all. */
-- clientSecret: string | null;
-- /** Network base for /oauth/token, identity and the JWKS (host-internal in production). */
-- networkUrl: string;
-- /** OpenVibe.Events base; null = no durable events (the outbox is not even created). */
-- eventsUrl: string | null;
-- /** OpenVibe.Media base; null = map assets stay local only. */
-- mediaUrl: string | null;
-- /** Media namespace (tenant app id) Games writes objects into. */
-- mediaNamespace: string;
-- /** At most one `games.world.saved` checkpoint event per this many minutes. */
-- worldSavedEventMinutes: number;
-- /**
-- * GAMES_EVENTS_SECRET (comma-separated for rotation, 32+ characters each): signs Games' own Events
-- * subscriptions (network.user.token_valid_after → POST /internal/events). Empty = no subscription,
-- * and the route answers 503.
-- */
-- eventsSecrets: string[];
-- /** Where Events delivers (GAMES_EVENTS_ENDPOINT, else loopback on PORT). */
-- eventsEndpoint: string | null;
--}
--export declare function loadPlatformConfig(env: NodeJS.ProcessEnv): PlatformConfig;
--export declare function loadConfig(env: NodeJS.ProcessEnv): ServerConfig;
--//# sourceMappingURL=config.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/config.d.ts.map b/apps/server/dist-types/config.d.ts.map
ddeleted file mode 100644
iindex 7acb542..0000000
---- a/apps/server/dist-types/config.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,+EAA+E;IAC/E,cAAc,EAAE,OAAO,CAAA;IACvB,gFAAgF;IAChF,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAA;QAChB,YAAY,EAAE,MAAM,CAAA;QACpB,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,6DAA6D;QAC7D,OAAO,EAAE,MAAM,CAAA;KAChB,GAAG,IAAI,CAAA;IACR;;;;OAIG;IACH,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,iBAAiB,EAAE,MAAM,CAAA;IACzB,4DAA4D;IAC5D,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAA;IAClB,uFAAuF;IACvF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,sBAAsB,EAAE,MAAM,CAAA;IAC9B;;;;OAIG;IACH,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAMD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,cAAc,CAmBzE;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,YAAY,CAkC/D"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/eventManager.d.ts b/apps/server/dist-types/game/eventManager.d.ts
ddeleted file mode 100644
iindex c6c9d80..0000000
---- a/apps/server/dist-types/game/eventManager.d.ts
++++ /dev/null
@@@ -1,54 +0,0 @@
--import { type Logger } from '@openvibe/shared';
--import type { GameEntity } from '@openvibe/gameplay';
--import type { GameWorld } from './gameWorld.js';
--/**
-- * Generic world events: one server-owned lifecycle —
-- * scheduled → announced → active → completed/failed → cleanup —
-- * with type handlers plugged in as data + callbacks. Supply drops moved
-- * here from hardcoded server logic; extraction is the second citizen.
-- * Event state is deliberately ephemeral: a restart cancels the party.
-- */
--export type EventPhase = 'scheduled' | 'announced' | 'active' | 'done';
--export interface WorldEvent {
-- id: string;
-- type: string;
-- x: number;
-- z: number;
-- phase: EventPhase;
-- /** Phase transition times (epoch ms). */
-- announceAt: number;
-- activeAt: number;
-- endsAt: number;
-- /** Type-specific runtime state. */
-- data: Record<string, unknown>;
--}
--export interface EventHooks {
-- announce(text: string): void;
-- announceTo(playerEntityId: string, text: string): void;
-- /** Players currently inside a circle: [entityId, distance][] */
-- playersIn(x: number, z: number, radius: number): {
-- entityId: string;
-- alive: boolean;
-- }[];
-- /** Secure a player's carried valuables + recall them to the city. */
-- extractPlayer(entityId: string): void;
-- broadcastSpawn(entity: GameEntity): void;
-- broadcastDespawn(id: string): void;
--}
--export declare class EventManager {
-- private readonly world;
-- private readonly hooks;
-- private readonly intervalScale;
-- private readonly log;
-- private readonly events;
-- private readonly handlers;
-- private readonly nextSpawnAt;
-- constructor(world: GameWorld, hooks: EventHooks, intervalScale: number, log: Logger);
-- private register;
-- get activeCount(): number;
-- /** 1 Hz driver. */
-- tick(nowMs: number, playersOnline: number): void;
-- private supplyDropHandler;
-- private extractionHandler;
--}
--//# sourceMappingURL=eventManager.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/eventManager.d.ts.map b/apps/server/dist-types/game/eventManager.d.ts.map
ddeleted file mode 100644
iindex c3e8585..0000000
---- a/apps/server/dist-types/game/eventManager.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"eventManager.d.ts","sourceRoot":"","sources":["../../src/game/eventManager.ts"],"names":[],"mappings":"AAOA,OAAO,EAA+C,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C;;;;;;GAMG;AAEH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEtE,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,UAAU,CAAA;IACjB,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACtD,gEAAgE;IAChE,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;IACvF,qEAAqE;IACrE,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAA;IACxC,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;CACnC;AAeD,qBAAa,YAAY;IAMrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;gBAGrC,KAAK,EAAE,SAAS,EAChB,KAAK,EAAE,UAAU,EACjB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM;IAM9B,OAAO,CAAC,QAAQ;IAIhB,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,mBAAmB;IACnB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAsDhD,OAAO,CAAC,iBAAiB;IAwDzB,OAAO,CAAC,iBAAiB;CAqD1B"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/gameServer.d.ts b/apps/server/dist-types/game/gameServer.d.ts
ddeleted file mode 100644
iindex 9a87584..0000000
---- a/apps/server/dist-types/game/gameServer.d.ts
++++ /dev/null
@@@ -1,202 +0,0 @@
--import { RegionTracker } from '@openvibe/gameplay';
--import type { PersistenceStore } from '@openvibe/persistence';
--import { type ClientMessage } from '@openvibe/protocol';
--import { type Logger } from '@openvibe/shared';
--import type { ServerConfig } from '../config.js';
--import type { GameEventRecorder } from '../platform/gameEvents.js';
--import type { ProgressSummaryWriter } from '../platform/progressSummary.js';
--import type { ModRuntime } from '../mods/runtime.js';
--import type { ServerMetrics } from '../observability/metrics.js';
--import type { GameWorld } from './gameWorld.js';
--import { type PlayerSession } from './playerSession.js';
--import { NpcManager } from './npcManager.js';
--import { EventManager } from './eventManager.js';
--/** A network connection as the game sees it — transport-agnostic. */
--export interface GameConnection {
-- /** Real client IP (Cloudflare-aware) — guest identity hangs off this. */
-- ip: string;
-- send(text: string): void;
-- close(code: number, reason: string): void;
--}
--/**
-- * Platform adapters the server drives (roadmap Wave 12). Both optional: the
-- * game runs exactly as before without them.
-- */
--export interface GameIntegrations {
-- /** Durable lifecycle/progression events through the outbox. */
-- events?: GameEventRecorder;
-- /** Installed mods (content packs), reconciled every tick. */
-- mods?: ModRuntime;
-- /** The person's games.progress.summary user module on Network, written when a character leaves. */
-- progressSummary?: ProgressSummaryWriter;
--}
--export declare class GameServer {
-- private readonly config;
-- private readonly world;
-- private readonly store;
-- private readonly metrics;
-- private readonly log;
-- private readonly integrations;
-- private readonly sessions;
-- private readonly sessionsByConn;
-- private readonly sessionsByEntity;
-- private readonly playerBodies;
-- private readonly heldEntityIds;
-- /** Short-lived cache of OFFLINE owners' friend lists (prop protection). */
-- private readonly offlineFriendsCache;
-- private tick;
-- private readonly moveQueries;
-- /** Body excluded from the current movement sweep (the moving player's own). */
-- private sweepSelf;
-- private lastFlushTick;
-- /** Authoritative world environment: clock, weather, temperature. */
-- private readonly env;
-- /** Coarse activation regions (NPC LOD and event relevance hang off this). */
-- readonly regions: RegionTracker;
-- /** Server-authoritative NPC simulation (LOD-aware). */
-- readonly npcs: NpcManager;
-- /** Generic world events (supply drops, extraction). */
-- readonly events: EventManager;
-- /** Players who recently attacked someone (defensive NPCs respond). */
-- private readonly aggro;
-- /** Sound events (gunshots, fights) accumulated for NPC hearing. */
-- private sounds;
-- /** Players connected now (the readiness `sessions` check; a restart would disconnect them). */
-- onlineCount(): number;
-- constructor(config: ServerConfig, world: GameWorld, store: PersistenceStore, metrics: ServerMetrics, log: Logger, integrations?: GameIntegrations);
-- get currentTick(): number;
-- /**
-- * Prop protection: world props (no owner) are free; otherwise the owner
-- * or anyone the OWNER trusts may manipulate. Works for offline owners via
-- * a TTL-cached repository lookup.
-- */
-- private canManipulate;
-- onMessage(conn: GameConnection, msg: ClientMessage): void;
-- /** Live sell-stock per market: item -> remaining + next restock time. */
-- private readonly marketStock;
-- /** Lazily restocked stock entry for one market sell line. */
-- private stockOf;
-- private persistMarkets;
-- /**
-- * Market interactions: open/buy/sell against the market a shop prop
-- * references. Everything server-atomic: proximity, faction stance,
-- * live stock, coins and space all validated here.
-- */
-- private handleMarket;
-- /** Contract accept/turn-in at a trading post. */
-- private handleJob;
-- private sendJobs;
-- private sendReputation;
-- /** Range + prop-protection gate shared by all container operations. */
-- private containerAccessDenied;
-- private sendContainer;
-- private timeWire;
-- /**
-- * Utility + production sweep, once a second over prop entities. Plants
-- * stay timestamp-lazy (a slower cadence touches them); machines and
-- * generators are few, and their jobs are timestamp-based too — this
-- * sweep only notices completions/starts, it never simulates.
-- */
-- private tickProduction;
-- onDisconnect(conn: GameConnection): void;
-- private handleHello;
-- private handlePhysgun;
-- private handleTrust;
-- private sendFriends;
-- /** One grab attempt down the view ray; latches + broadcasts on success. */
-- private attemptGrab;
-- private releaseHeld;
-- /** Melee swing on another player: range + zone PvP rules + tool damage. */
-- private handleMelee;
-- /**
-- * Ranged fire: the client sent pure intent — the shot leaves the
-- * player's AUTHORITATIVE eye along their AUTHORITATIVE view (from
-- * movement inputs) plus server-rolled spread. Magazine, cadence and
-- * reload state live in the weapon stack's meta and are validated here.
-- */
-- private handleFire;
-- /**
-- * The one place player damage lands: armor mitigation (with durability
-- * wear), wound statuses, death handling, and hit feedback. Melee, bullets
-- * and future explosions all converge here.
-- */
-- private damagePlayer;
-- /**
-- * Melee swing on a damageable prop: range + zone build rules + the same
-- * stamina/cooldown economics as PvP. Structures are only destructible
-- * where building is legal (safe city props are untouchable).
-- */
-- private handlePropAttack;
-- /** Melee swing on an NPC: reach + the shared stamina economics. */
-- private handleNpcAttack;
-- /** NPC damage sink: aggro marking, death, loot scatter, feedback. */
-- private applyNpcDamage;
-- /** Damage application for props: resistance, destruction, feedback.
-- * Returns false when the prop has no health capability. */
-- private applyPropDamage;
-- /** Destruction: scatter salvage + stored contents as physical props. */
-- private destroyProp;
-- /**
-- * Extraction success: carried valuables become SECURED (they no longer
-- * drop on death) and the player is recalled to the safe city.
-- */
-- private extractPlayer;
-- /** E on a chassis: mount a valid assembly, or dismount if driving it. */
-- private handleVehicleUse;
-- private dismount;
-- /**
-- * Vehicle drive: intent (WASD) becomes thrust force + steering on the
-- * chassis body; wheels roll on their rigged bearings; fuel burns from
-- * the trunk like a generator. The rider is carried kinematically.
-- */
-- private driveVehicle;
-- /** Death/rescue respawn: back to the city with restored vitals. */
-- private respawn;
-- private statsWire;
-- step(): void;
-- private stepSessionMovement;
-- private replicate;
-- /**
-- * One transaction per flush: world rows, player rows and the outbox
-- * events describing them commit together (or not at all).
-- */
-- flush(reason?: 'checkpoint' | 'shutdown'): void;
-- /** Full save on shutdown. */
-- shutdown(): void;
-- /** Saves one character; `leaving` also records games.player.left in the same transaction. */
-- private savePlayer;
-- /**
-- * What mods may do to the world, lent to the mod runtime only. Mod props
-- * are owned by the mod id, so prop protection keeps players' hands off.
-- */
-- private readonly modHost;
-- private playerToDto;
-- private send;
-- private sendRaw;
-- private sendInventory;
-- private sendSkills;
-- private sendCraftState;
-- private broadcastAll;
-- private broadcastSpawn;
-- /** Constraint create/remove: tell every client that knows either prop. */
-- private broadcastConstraintState;
-- private broadcastDespawn;
-- private broadcastToKnowing;
-- /** Live map edit: every client refetches and rebuilds its terrain. */
-- /**
-- * Network moved this person's token cutoff (sign out everywhere, password changed, banned): close
-- * every session they opened with an older Network sign-in (4011 signed_out). The client's reconnect
-- * then fails its /api/auth/me check. Returns how many closed.
-- */
-- revokeSubject(subjectId: string, validAfterMs: number): number;
-- /**
-- * Graceful stop (WS-P lifecycle): everyone in the world is told the server is restarting. main.ts
-- * then closes every socket with 1012 (service restart); each close saves that character and records
-- * games.player.left, as when a player leaves, and the client reconnects on its own.
-- */
-- announceRestart(): void;
-- broadcastMapReload(): void;
-- /** Exposes crafting context for the client-facing recipe availability (welcome-time). */
-- workstationsNear(session: PlayerSession): ReadonlySet<string>;
--}
--//# sourceMappingURL=gameServer.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/gameServer.d.ts.map b/apps/server/dist-types/game/gameServer.d.ts.map
ddeleted file mode 100644
iindex 53b3541..0000000
---- a/apps/server/dist-types/game/gameServer.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"gameServer.d.ts","sourceRoot":"","sources":["../../src/game/gameServer.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/gameWorld.d.ts b/apps/server/dist-types/game/gameWorld.d.ts
ddeleted file mode 100644
iindex 7d2efc3..0000000
---- a/apps/server/dist-types/game/gameWorld.d.ts
++++ /dev/null
@@@ -1,174 +0,0 @@
--import type { ContentRegistry, WorldShape } from '@openvibe/content';
--import { ConstraintIslands, EntityStore, SpatialHash, ZoneIndex, type ConstraintParams, type ConstraintType, type GameEntity, type MotionState } from '@openvibe/gameplay';
--import type { PersistenceStore } from '@openvibe/persistence';
--import { type BodyId, type ConstraintId, type PhysicsWorld, type ShapeDesc } from '@openvibe/physics';
--import { type EntityId, type Logger, type PlayerId, type Quat, type Vec3 } from '@openvibe/shared';
--export interface ConstraintRecord {
-- id: string;
-- type: ConstraintType;
-- a: EntityId;
-- b: EntityId;
-- params: ConstraintParams;
-- physId: ConstraintId;
--}
--export declare class GameWorld {
-- readonly content: ContentRegistry;
-- readonly physics: PhysicsWorld;
-- private readonly log;
-- readonly entities: EntityStore;
-- readonly zones: ZoneIndex;
-- private readonly bodyByEntity;
-- private readonly entityByBody;
-- private readonly settled;
-- private readonly deletedIds;
-- private readonly constraintRecords;
-- private readonly constraintsByEntity;
-- private readonly constraintsDirty;
-- private readonly constraintsDeleted;
-- /** Connected-constraint structure tracking (metrics, group semantics). */
-- readonly islands: ConstraintIslands<EntityId>;
-- /**
-- * THE spatial index: every entity (props, resources, players) by ground
-- * position. Interest queries, workstation/shop lookups, sprinkler and
-- * power coupling, and NPC perception all consume this one structure.
-- * GameWorld maintains props/resources; the server maintains players.
-- */
-- readonly spatial: SpatialHash<EntityId>;
-- constructor(content: ContentRegistry, physics: PhysicsWorld, log: Logger);
-- /** Static level geometry — mirrored by the client from the same world def. */
-- private buildStaticWorld;
-- private addStaticBody;
-- /** Live map save: bring map-authored static collision up to date. */
-- reconcileMapStatics(): void;
-- private terrainBody;
-- private readonly mapTerrain;
-- private readonly mapStatics;
-- /** Trimesh bodies for the map's extra terrain patches (world-space verts). */
-- private buildTerrainBody;
-- /** Live map edit: swap terrain + patch collision for the new map. */
-- /**
-- * Live map save: bring terrain collision up to date.
-- *
-- * This used to remove and recreate EVERY terrain body, so retinting one
-- * surface rebuilt the collision for the whole map. The layer reconciles by
-- * stable id and its signature covers only what collision depends on, so a
-- * surface-only edit touches no body and a sculpt rebuilds exactly one.
-- */
-- reconcileMapTerrain(): void;
-- /** Live-apply diagnostics and tests: what the map layers currently hold. */
-- mapStaticCount(): number;
-- mapTerrainCount(): number;
-- /** Cumulative map-layer body rebuilds, for churn assertions. */
-- mapRebuildCount(): number;
-- mapZoneCount(): number;
-- bodyOf(id: EntityId): BodyId | undefined;
-- entityOfBody(body: BodyId): GameEntity | undefined;
-- /** NPC bodies register here so rays/attacks resolve to their entity. */
-- registerNpcBody(body: BodyId, entityId: EntityId): void;
-- unregisterNpcBody(body: BodyId): void;
-- spawnProp(opts: {
-- defId: string;
-- pos: Vec3;
-- rot: Quat;
-- motion: MotionState;
-- owner?: PlayerId;
-- id?: EntityId;
-- /** Items recovered on pickup; every prop defaults to carrying itself. */
-- lootCount?: number;
-- /** Initial toss velocity (dropping an item throws it forward). */
-- velocity?: Vec3;
-- /** Initial spin (felled trees tip over). */
-- angularVelocity?: Vec3;
-- }): GameEntity;
-- /** Spawns a resource node instance of a content-defined node type. */
-- spawnResource(opts: {
-- nodeTypeId: string;
-- pos: Vec3;
-- remaining: number;
-- depletedUntil?: number;
-- id?: EntityId;
-- }): GameEntity;
-- despawn(id: EntityId): void;
-- /**
-- * Swings a frozen door about its hinge edge (local -X). The whole pose
-- * (position AND rotation) pivots so it reads as a real hinge, not a
-- * center-spin. Returns false unless the prop is an installed (non-
-- * dynamic) door.
-- */
-- toggleDoor(entity: GameEntity): boolean;
-- setPropMotion(entity: GameEntity, motion: MotionState): void;
-- /** True when a constraint of this type already links the pair. */
-- hasConstraint(a: EntityId, b: EntityId, type: ConstraintType): boolean;
-- constraintCountFor(id: EntityId): number;
-- /** A weld/hinge/axis/slider/motor (collision-off joint) links the pair. */
-- private pairHasRigidJoint;
-- get constraintCount(): number;
-- /**
-- * Creates a validated constraint between two props. Params must already
-- * have passed `validateConstraintParams`; this maps them onto the physics
-- * engine, indexes the record and marks it for persistence.
-- */
-- addConstraintRecord(a: GameEntity, b: GameEntity, type: ConstraintType, params: ConstraintParams, id?: string): ConstraintRecord | null;
-- /** Removes every constraint touching the entity; returns removed records. */
-- removeConstraintsFor(entityId: EntityId): ConstraintRecord[];
-- private removeConstraintRecord;
-- private indexConstraint;
-- allConstraints(): IterableIterator<ConstraintRecord>;
-- constraintsFor(entityId: EntityId): ConstraintRecord[];
-- /** Refills depleted nodes whose respawn time passed. Returns refilled entities. */
-- respawnDueResources(nowMs: number): GameEntity[];
-- syncFromPhysics(events: {
-- onSettle?: (e: GameEntity) => void;
-- onWake?: (e: GameEntity) => void;
-- }): {
-- awake: number;
-- settledCount: number;
-- };
-- isSettledEntity(id: EntityId): boolean;
-- seedOrRestore(store: PersistenceStore): void;
-- private seedProps;
-- /**
-- * Called after boot-restore and after every live map save: any editor-
-- * placed resource node with no matching live resource nearby spawns.
-- * (Removal is by harvesting in game — reconcile never deletes.)
-- */
-- /**
-- * Live map save: bring map-authored resource nodes into line, BY IDENTITY.
-- *
-- * The previous rule was "a node of this type within a metre already
-- * exists", which is not identity: two deliberately adjacent authored nodes
-- * collapsed into one, and a moved seed spawned a duplicate instead of
-- * moving. Every entity a map object authored carries that object's id, so
-- * this can only ever touch what the map owns — a player's constructions
-- * have no provenance and are never considered.
-- *
-- * Gameplay state is preserved across an unrelated save: a depleted node
-- * stays depleted, because an admin retexturing a wall must not silently
-- * restock the map.
-- */
-- reconcileMapNodes(): void;
-- /** Reposition an authored entity onto the terrain, body and all. */
-- private placeAt;
-- /**
-- * Apply the map's authored zones. Idempotent by construction: the map layer
-- * is REPLACED, never appended, so saving the same map twice cannot stack
-- * duplicate rule volumes the way merging into `content.world.zones` would.
-- * The base content zones are untouched, so a map can add a restriction but
-- * never lift one the world def declared.
-- */
-- reconcileMapZones(): void;
-- /**
-- * Live map save: map-authored props, by identity.
-- *
-- * The old rule was "a prop of this item within two metres", so a player's
-- * crate dropped beside an authored one suppressed the authored one — and a
-- * player's constructions could be mistaken for map seeds. Props with no
-- * provenance are never touched here.
-- */
-- reconcileMapProps(): void;
-- private seedResources;
-- private restoreEntity;
-- flushDirty(store: PersistenceStore): number;
--}
--export declare function toShapeDesc(shape: WorldShape): ShapeDesc;
--//# sourceMappingURL=gameWorld.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/gameWorld.d.ts.map b/apps/server/dist-types/game/gameWorld.d.ts.map
ddeleted file mode 100644
iindex 039a005..0000000
---- a/apps/server/dist-types/game/gameWorld.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"gameWorld.d.ts","sourceRoot":"","sources":["../../src/game/gameWorld.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/interactions.d.ts b/apps/server/dist-types/game/interactions.d.ts
ddeleted file mode 100644
iindex b094ce4..0000000
---- a/apps/server/dist-types/game/interactions.d.ts
++++ /dev/null
@@@ -1,53 +0,0 @@
--import type { ClientConstraint, ClientCraft, ClientDrop, ClientInvMove, ClientPlace, ClientUse, ServerActionResult } from '@openvibe/protocol';
--import { type GameEntity, type LevelUp, type PlantContext } from '@openvibe/gameplay';
--import type { ItemDef } from '@openvibe/content';
--import type { ConstraintRecord, GameWorld } from './gameWorld.js';
--import { type PlayerSession } from './playerSession.js';
--export type ActionOutcome = ServerActionResult;
--/** The tool capability of the session's active hotbar item, if any. */
--export declare function equippedTool(session: PlayerSession): NonNullable<ItemDef['tool']> | undefined;
--export interface GatherResult {
-- outcome: ActionOutcome;
-- /** Entity whose remaining count changed (for replication), if any. */
-- changed: GameEntity | null;
-- /** Entity picked up and removed from the world, if any. */
-- pickedUp: GameEntity | null;
-- /** Entity spawned as a side effect (felled tree trunk), if any. */
-- spawned: GameEntity | null;
-- levelUps: LevelUp[];
-- xpChanged: boolean;
--}
--/**
-- * Gathering: E-use for hand nodes, tool swings for gated nodes. The node
-- * type (content) decides tool requirements, yield, XP and respawn.
-- */
--export declare function handleUse(session: PlayerSession, world: GameWorld, msg: ClientUse, nowMs: number, canManipulate: (entity: GameEntity) => boolean, env?: PlantContext): GatherResult;
--export declare function handleCraft(session: PlayerSession, world: GameWorld, msg: ClientCraft, tick: number, tickRate: number): ActionOutcome;
--export declare function nearbyWorkstationKinds(session: PlayerSession, world: GameWorld): ReadonlySet<string>;
--export declare function handleDrop(session: PlayerSession, world: GameWorld, msg: ClientDrop): {
-- outcome: ActionOutcome;
-- droppedId: string | null;
--};
--/**
-- * Ghost-preview placement: the stack leaves the inventory and becomes a
-- * DYNAMIC prop at the requested pose. Never frozen on spawn — if the
-- * client lied about a clear spot, physics depenetration resolves it
-- * honestly instead of leaving a teleported wall inside someone's head.
-- */
--export declare function handlePlace(session: PlayerSession, world: GameWorld, msg: ClientPlace): {
-- outcome: ActionOutcome;
-- placedId: string | null;
--};
--export interface ConstraintOutcome {
-- outcome: ActionOutcome;
-- created: ConstraintRecord | null;
--}
--/**
-- * Rigging tool: create a constraint between two props. Every field of the
-- * request is hostile until proven otherwise — tool, targets, ownership,
-- * reach, gap, zone, per-entity limits, skill level, materials and the
-- * parameter space itself are all validated server-side.
-- */
--export declare function handleConstraint(session: PlayerSession, world: GameWorld, msg: ClientConstraint, canManipulate: (entity: GameEntity) => boolean): ConstraintOutcome;
--export declare function handleInvMove(session: PlayerSession, msg: ClientInvMove): ActionOutcome;
--//# sourceMappingURL=interactions.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/interactions.d.ts.map b/apps/server/dist-types/game/interactions.d.ts.map
ddeleted file mode 100644
iindex 3889316..0000000
---- a/apps/server/dist-types/game/interactions.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"interactions.d.ts","sourceRoot":"","sources":["../../src/game/interactions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,aAAa,EACb,WAAW,EACX,SAAS,EACT,kBAAkB,EACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAYL,KAAK,UAAU,EACf,KAAK,OAAO,EACZ,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAIhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AACjE,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAWpE,MAAM,MAAM,aAAa,GAAG,kBAAkB,CAAA;AAM9C,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAK7F;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,aAAa,CAAA;IACtB,sEAAsE;IACtE,OAAO,EAAE,UAAU,GAAG,IAAI,CAAA;IAC1B,2DAA2D;IAC3D,QAAQ,EAAE,UAAU,GAAG,IAAI,CAAA;IAC3B,mEAAmE;IACnE,OAAO,EAAE,UAAU,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;CACnB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,SAAS,EACd,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,EAC9C,GAAG,GAAE,YAAsD,GAC1D,YAAY,CAoQd;AAED,wBAAgB,WAAW,CACzB,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,aAAa,CAiBf;AAID,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,GACf,WAAW,CAAC,MAAM,CAAC,CAiBrB;AASD,wBAAgB,UAAU,CACxB,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,UAAU,GACd;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAmCtD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,WAAW,GACf;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAgCrD;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,aAAa,CAAA;IACtB,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAA;CACjC;AA2BD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,gBAAgB,EACrB,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,GAC7C,iBAAiB,CA6GnB;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,aAAa,GAAG,aAAa,CAKvF"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapProvenance.d.ts b/apps/server/dist-types/game/mapProvenance.d.ts
ddeleted file mode 100644
iindex da4334f..0000000
---- a/apps/server/dist-types/game/mapProvenance.d.ts
++++ /dev/null
@@@ -1,69 +0,0 @@
--/**
-- * Which live entity a map-authored object owns.
-- *
-- * The old rule was spatial: "a resource node of this type within one metre"
-- * (props: two metres). Proximity is not identity, and it was wrong in every
-- * direction at once —
-- *
-- * - two authored objects standing close together collapsed into one, so the
-- * second was silently dropped on every save;
-- * - a player's crate dropped beside an authored one suppressed the authored
-- * one, and could itself be despawned when that map object was deleted;
-- * - moving a seed further than the radius spawned a duplicate and left the
-- * original behind, losing whatever had been mined out of it.
-- *
-- * Every entity a map object authors carries that object's stable id, so the
-- * question "is this mine?" is answered by provenance and nothing else.
-- * Entities with no provenance are player-created and are never touched here:
-- * player constructions are never deleted because a map seed disappeared.
-- *
-- * The planner is pure so both callers share one rule and it can be tested
-- * without standing up physics.
-- */
--/** How far an authored object must move before its entity is moved with it. */
--export declare const MAP_SEED_MOVE_EPSILON = 0.001;
--/**
-- * Provenance is persisted inside the entity's existing `state` blob, so it
-- * needs no schema migration. It has to survive a restart: without it the first
-- * save after one would find no entity for any authored seed and duplicate the
-- * whole map.
-- */
--export declare const provenanceState: (mapSourceId: string | undefined) => {
-- mapSourceId?: string;
--};
--/** Read provenance back off a persisted row, ignoring anything malformed. */
--export declare function readProvenance(state: unknown): string | undefined;
--/** The identity-bearing part of an authored map object. */
--export interface AuthoredSpec {
-- /** Stable document id of the map object. */
-- id: string;
-- /** Node type or item id — changing it makes this a different thing. */
-- defId: string;
-- pos: readonly [number, number, number];
--}
--/** The identity-bearing part of a live entity. */
--export interface LiveProvenance {
-- /** The map object that authored this entity; undefined = player-created. */
-- mapSourceId?: string | undefined;
-- defId: string;
-- x: number;
-- z: number;
--}
--export interface ProvenancePlan<E, S> {
-- /** Entities whose map object is gone, or became a different thing. */
-- despawn: E[];
-- /** Entities to reposition, KEEPING their gameplay state. */
-- move: {
-- entity: E;
-- spec: S;
-- }[];
-- /** Entities that are already correct — left completely alone. */
-- keep: E[];
-- /** Map objects with no entity yet. */
-- spawn: S[];
--}
--export declare function planMapProvenance<E, S>(authored: readonly S[], live: readonly E[], read: {
-- spec: (s: S) => AuthoredSpec;
-- entity: (e: E) => LiveProvenance;
--}): ProvenancePlan<E, S>;
--//# sourceMappingURL=mapProvenance.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapProvenance.d.ts.map b/apps/server/dist-types/game/mapProvenance.d.ts.map
ddeleted file mode 100644
iindex 0750e1a..0000000
---- a/apps/server/dist-types/game/mapProvenance.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapProvenance.d.ts","sourceRoot":"","sources":["../../src/game/mapProvenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,QAAO,CAAA;AAEzC;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CACpD,CAAA;AAEpC,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIjE;AAED,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,4CAA4C;IAC5C,EAAE,EAAE,MAAM,CAAA;IACV,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CACvC;AAED,kDAAkD;AAClD,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,EAAE,CAAC;IAClC,sEAAsE;IACtE,OAAO,EAAE,CAAC,EAAE,CAAA;IACZ,4DAA4D;IAC5D,IAAI,EAAE;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,IAAI,EAAE,CAAC,CAAA;KAAE,EAAE,CAAA;IAC9B,iEAAiE;IACjE,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,sCAAsC;IACtC,KAAK,EAAE,CAAC,EAAE,CAAA;CACX;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,CAAC,EACpC,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,IAAI,EAAE;IAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,YAAY,CAAC;IAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,cAAc,CAAA;CAAE,GACvE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAgCtB"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapProvenance.test.d.ts b/apps/server/dist-types/game/mapProvenance.test.d.ts
ddeleted file mode 100644
iindex ea1020c..0000000
---- a/apps/server/dist-types/game/mapProvenance.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=mapProvenance.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapProvenance.test.d.ts.map b/apps/server/dist-types/game/mapProvenance.test.d.ts.map
ddeleted file mode 100644
iindex d1eda9c..0000000
---- a/apps/server/dist-types/game/mapProvenance.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapProvenance.test.d.ts","sourceRoot":"","sources":["../../src/game/mapProvenance.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapStaticLayer.d.ts b/apps/server/dist-types/game/mapStaticLayer.d.ts
ddeleted file mode 100644
iindex f2c0a20..0000000
---- a/apps/server/dist-types/game/mapStaticLayer.d.ts
++++ /dev/null
@@@ -1,48 +0,0 @@
--/**
-- * The map-authored static collision layer, keyed by stable id.
-- *
-- * Both sides used to do `content.world.statics.push(...map.statics)` at boot.
-- * That merge is permanent: the map layer could never be replaced, so a live
-- * save added geometry the physics world never saw, moving a static changed
-- * nothing until restart, and re-applying a map appended a second copy of
-- * everything. The editor's Save button said "live" while most of the map
-- * was not.
-- *
-- * Held separately and reconciled by id, statics appear, move and vanish on
-- * save — and an identical repeated save touches no body at all, which matters
-- * because the editor saves often and a heightfield rebuild is not free.
-- */
--import type { StaticBody } from '@openvibe/content';
--import { type BodyId, type PhysicsWorld } from '@openvibe/physics';
--/** Canonical value of a static, so an unchanged one is left alone. */
--export declare function staticSignature(s: StaticBody): string;
--/** Only add/remove: a static body's transform is not edited in place. */
--type BodyPhysics = Pick<PhysicsWorld, 'addBody' | 'removeBody'>;
--export declare class MapStaticLayer {
-- private readonly physics;
-- private readonly bodies;
-- /**
-- * Cumulative bodies created and destroyed. Exposed so "an identical save
-- * causes no churn" is checkable from outside the process rather than
-- * inferred from a count that would look the same either way.
-- */
-- rebuilds: number;
-- constructor(physics: BodyPhysics);
-- get size(): number;
-- /** The body for a map static, for tests and diagnostics. */
-- bodyOf(id: string): BodyId | undefined;
-- /**
-- * Make the layer match `statics`. Bodies whose canonical value is unchanged
-- * are kept as-is; everything else is replaced or dropped.
-- *
-- * A static with no id cannot be reconciled — `parseMapFile` guarantees one,
-- * so this only bites hand-written override data, and skipping is safer than
-- * minting a body nothing can ever remove.
-- */
-- reconcile(statics: readonly StaticBody[]): void;
-- /** Drop every body (world teardown). */
-- clear(): void;
-- private addBody;
--}
--export {};
--//# sourceMappingURL=mapStaticLayer.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapStaticLayer.d.ts.map b/apps/server/dist-types/game/mapStaticLayer.d.ts.map
ddeleted file mode 100644
iindex 19e4a39..0000000
---- a/apps/server/dist-types/game/mapStaticLayer.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapStaticLayer.d.ts","sourceRoot":"","sources":["../../src/game/mapStaticLayer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAGnD,OAAO,EAAkB,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGlF,sEAAsE;AACtE,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAIrD;AAED,yEAAyE;AACzE,KAAK,WAAW,GAAG,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,YAAY,CAAC,CAAA;AAE/D,qBAAa,cAAc;IAUb,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyD;IAEhF;;;;OAIG;IACH,QAAQ,SAAI;gBAEiB,OAAO,EAAE,WAAW;IAEjD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,4DAA4D;IAC5D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAItC;;;;;;;OAOG;IACH,SAAS,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI;IAiB/C,wCAAwC;IACxC,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,OAAO;CAYhB"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapStaticLayer.test.d.ts b/apps/server/dist-types/game/mapStaticLayer.test.d.ts
ddeleted file mode 100644
iindex 5911f3e..0000000
---- a/apps/server/dist-types/game/mapStaticLayer.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=mapStaticLayer.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapStaticLayer.test.d.ts.map b/apps/server/dist-types/game/mapStaticLayer.test.d.ts.map
ddeleted file mode 100644
iindex 0243a79..0000000
---- a/apps/server/dist-types/game/mapStaticLayer.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapStaticLayer.test.d.ts","sourceRoot":"","sources":["../../src/game/mapStaticLayer.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapTerrainLayer.d.ts b/apps/server/dist-types/game/mapTerrainLayer.d.ts
ddeleted file mode 100644
iindex b5028b8..0000000
---- a/apps/server/dist-types/game/mapTerrainLayer.d.ts
++++ /dev/null
@@@ -1,42 +0,0 @@
--/**
-- * Map-authored terrain collision, keyed by stable id.
-- *
-- * `rebuildTerrain()` removed and recreated EVERY terrain body on every save,
-- * so retinting one surface tore down and rebuilt the collision for the whole
-- * map. A trimesh body is the single most expensive thing this server
-- * allocates, the editor saves constantly, and rendering and physics are
-- * separate costs — only one of them moved.
-- *
-- * Reconciled by id, with the signature covering only what collision actually
-- * depends on: a surface-only edit touches no body at all, a sculpt rebuilds
-- * exactly the terrain that was sculpted, and an identical repeated save does
-- * nothing.
-- */
--import { type TerrainPatchData } from '@openvibe/content';
--import { type BodyId, type PhysicsWorld } from '@openvibe/physics';
--/**
-- * What the collision body is made of. Deliberately NOT the whole object:
-- * `surface` (textures, tints, paint layers) is excluded, which is what makes
-- * a repaint free on the server.
-- */
--export declare function terrainCollisionSignature(t: TerrainPatchData): string;
--type BodyPhysics = Pick<PhysicsWorld, 'addBody' | 'removeBody'>;
--export declare class MapTerrainLayer {
-- private readonly physics;
-- private readonly bodies;
-- /**
-- * Cumulative bodies created and destroyed. Exposed so "an identical save
-- * causes no churn" is checkable from outside the process rather than
-- * inferred from a count that would look the same either way.
-- */
-- rebuilds: number;
-- constructor(physics: BodyPhysics);
-- get size(): number;
-- bodyOf(id: string): BodyId | undefined;
-- /** Bring collision into line with `terrains`, touching only what changed. */
-- reconcile(terrains: readonly TerrainPatchData[]): void;
-- clear(): void;
-- private addBody;
--}
--export {};
--//# sourceMappingURL=mapTerrainLayer.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapTerrainLayer.d.ts.map b/apps/server/dist-types/game/mapTerrainLayer.d.ts.map
ddeleted file mode 100644
iindex 91ca33f..0000000
---- a/apps/server/dist-types/game/mapTerrainLayer.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapTerrainLayer.d.ts","sourceRoot":"","sources":["../../src/game/mapTerrainLayer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAC9F,OAAO,EAAkB,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGlF;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAUrE;AAYD,KAAK,WAAW,GAAG,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,YAAY,CAAC,CAAA;AAE/D,qBAAa,eAAe;IAUd,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyD;IAEhF;;;;OAIG;IACH,QAAQ,SAAI;gBAEiB,OAAO,EAAE,WAAW;IAEjD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAItC,6EAA6E;IAC7E,SAAS,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI;IAiBtD,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,OAAO;CAgBhB"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapTerrainLayer.test.d.ts b/apps/server/dist-types/game/mapTerrainLayer.test.d.ts
ddeleted file mode 100644
iindex 5651ccb..0000000
---- a/apps/server/dist-types/game/mapTerrainLayer.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=mapTerrainLayer.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/mapTerrainLayer.test.d.ts.map b/apps/server/dist-types/game/mapTerrainLayer.test.d.ts.map
ddeleted file mode 100644
iindex decdbc9..0000000
---- a/apps/server/dist-types/game/mapTerrainLayer.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapTerrainLayer.test.d.ts","sourceRoot":"","sources":["../../src/game/mapTerrainLayer.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/npcManager.d.ts b/apps/server/dist-types/game/npcManager.d.ts
ddeleted file mode 100644
iindex 68ef1f4..0000000
---- a/apps/server/dist-types/game/npcManager.d.ts
++++ /dev/null
@@@ -1,49 +0,0 @@
--import { type NpcState, type PerceptionCandidate } from '@openvibe/gameplay';
--import type { PersistenceStore } from '@openvibe/persistence';
--import { type EntityId, type Logger } from '@openvibe/shared';
--import type { GameWorld } from './gameWorld.js';
--import type { RegionTracker } from '@openvibe/gameplay';
--export interface NpcHooks {
-- /** NPC melee lands on a player (id = player ENTITY id). */
-- onAttackPlayer(npcEntityId: EntityId, playerEntityId: string, damage: number): void;
-- /** Sound events this second (gunshots, fights) for hearing. */
-- soundsThisTick(): readonly {
-- x: number;
-- z: number;
-- }[];
-- /** Candidate actors near a point (players + hostile NPC logic later). */
-- hostilesNear(x: number, z: number, radius: number, faction: string): PerceptionCandidate[];
--}
--export declare class NpcManager {
-- private readonly world;
-- private readonly regions;
-- private readonly hooks;
-- private readonly log;
-- /** ALL NPC instances, materialized or not, by instance id. */
-- private readonly states;
-- /** Materialized subset: instance id -> physics body. */
-- private readonly bodies;
-- private readonly navigation;
-- private behaviorPhase;
-- constructor(world: GameWorld, regions: RegionTracker, hooks: NpcHooks, log: Logger);
-- /** Boot: restore persisted NPCs, then seed content spawns not yet known. */
-- seedOrRestore(store: PersistenceStore): void;
-- /** Counts for metrics: [full-sim, abstract]. */
-- counts(): [number, number];
-- npcStateOf(entityId: EntityId): NpcState | undefined;
-- /**
-- * Damage an NPC (melee/ranged already mitigated upstream). Returns the
-- * loot scattered on death, or null while it survives.
-- */
-- damage(entityId: EntityId, amount: number, nowMs: number): 'dead' | 'hurt' | null;
-- /** One simulation step; call every server tick. */
-- step(nowMs: number, tick: number, tickRate: number): void;
-- /** Entity ids of materialized NPCs whose pose changed (for snapshots). */
-- materializedIds(): IterableIterator<EntityId>;
-- private materialize;
-- private dematerialize;
-- private lineOfSight;
-- /** Persistence: NPCs write their ABSTRACT state (never bodies/entities). */
-- flush(store: PersistenceStore, now: number): void;
--}
--//# sourceMappingURL=npcManager.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/npcManager.d.ts.map b/apps/server/dist-types/game/npcManager.d.ts.map
ddeleted file mode 100644
iindex 98f1e4c..0000000
---- a/apps/server/dist-types/game/npcManager.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"npcManager.d.ts","sourceRoot":"","sources":["../../src/game/npcManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,QAAQ,EACb,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,uBAAuB,CAAA;AAC7E,OAAO,EAML,KAAK,QAAQ,EACb,KAAK,MAAM,EACZ,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAiBvD,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,cAAc,CAAC,WAAW,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACnF,+DAA+D;IAC/D,cAAc,IAAI,SAAS;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACrD,yEAAyE;IACzE,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,mBAAmB,EAAE,CAAA;CAC3F;AAED,qBAAa,UAAU;IASnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAXtB,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IACrD,wDAAwD;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkD;IAC7E,OAAO,CAAC,aAAa,CAAI;gBAGN,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,QAAQ,EACf,GAAG,EAAE,MAAM;IAG9B,4EAA4E;IAC5E,aAAa,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAkD5C,gDAAgD;IAChD,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;IAI1B,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS;IAIpD;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI;IAYjF,mDAAmD;IACnD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IA4GzD,0EAA0E;IACzE,eAAe,IAAI,gBAAgB,CAAC,QAAQ,CAAC;IAI9C,OAAO,CAAC,WAAW;IAyBnB,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,WAAW;IAYnB,4EAA4E;IAC5E,KAAK,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;CAsBlD"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/physgun.d.ts b/apps/server/dist-types/game/physgun.d.ts
ddeleted file mode 100644
iindex 9491317..0000000
---- a/apps/server/dist-types/game/physgun.d.ts
++++ /dev/null
@@@ -1,28 +0,0 @@
--import type { GameEntity } from '@openvibe/gameplay';
--import type { GameWorld } from './gameWorld.js';
--import { type PlayerSession } from './playerSession.js';
--/**
-- * Physgun mechanics, server-authoritative.
-- *
-- * A grabbed dynamic body is driven toward a target point on the player's
-- * view ray using velocity control (not teleportation), so it interacts
-- * honestly with the rest of the physics world. Rotation offsets accumulate
-- * from client intent commands but are applied here.
-- *
-- * This module is one *interaction* built on generic pieces (raycast, motion
-- * control, zone rules); constraint tools (weld, rope, ...) will be siblings,
-- * not extensions of a Physgun class.
-- */
--export declare const PHYSGUN_MAX_RANGE = 25;
--export declare const PHYSGUN_MIN_DIST = 1;
--export declare const PHYSGUN_MAX_DIST = 25;
--export type PhysgunDeny = 'no_target' | 'not_allowed' | 'not_owner' | 'zone' | 'already_held';
--export declare function tryGrab(session: PlayerSession, world: GameWorld, heldByOthers: ReadonlySet<string>, canManipulate: (entity: GameEntity) => boolean): GameEntity | PhysgunDeny;
--export declare function release(session: PlayerSession): void;
--export declare function adjustDistance(session: PlayerSession, delta: number): void;
--export declare function rotateHeld(session: PlayerSession, dyaw: number, dpitch: number, snap: boolean, snapStep?: number): void;
--/** Freezes the held prop in place (motion -> static) and releases the beam. */
--export declare function freezeHeld(session: PlayerSession, world: GameWorld): GameEntity | null;
--/** Called each tick for sessions holding a prop: drives the body toward the view target. */
--export declare function driveHeld(session: PlayerSession, world: GameWorld): void;
--//# sourceMappingURL=physgun.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/physgun.d.ts.map b/apps/server/dist-types/game/physgun.d.ts.map
ddeleted file mode 100644
iindex de353bd..0000000
---- a/apps/server/dist-types/game/physgun.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"physgun.d.ts","sourceRoot":"","sources":["../../src/game/physgun.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAmBpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAA8B,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAEnF;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,iBAAiB,KAAK,CAAA;AACnC,eAAO,MAAM,gBAAgB,IAAI,CAAA;AACjC,eAAO,MAAM,gBAAgB,KAAK,CAAA;AAoBlC,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,aAAa,GAAG,WAAW,GAAG,MAAM,GAAG,cAAc,CAAA;AAE7F,wBAAgB,OAAO,CACrB,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,EAChB,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,EACjC,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,GAC7C,UAAU,GAAG,WAAW,CAyC1B;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAEpD;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAG1E;AAED,wBAAgB,UAAU,CACxB,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAiBN;AAED,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,GAAG,UAAU,GAAG,IAAI,CAiBtF;AAED,4FAA4F;AAC5F,wBAAgB,SAAS,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CA4DxE"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/platformIntegration.test.d.ts b/apps/server/dist-types/game/platformIntegration.test.d.ts
ddeleted file mode 100644
iindex 0fb8eca..0000000
---- a/apps/server/dist-types/game/platformIntegration.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=platformIntegration.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/platformIntegration.test.d.ts.map b/apps/server/dist-types/game/platformIntegration.test.d.ts.map
ddeleted file mode 100644
iindex f7345f5..0000000
---- a/apps/server/dist-types/game/platformIntegration.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"platformIntegration.test.d.ts","sourceRoot":"","sources":["../../src/game/platformIntegration.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/playerSession.d.ts b/apps/server/dist-types/game/playerSession.d.ts
ddeleted file mode 100644
iindex f6918f6..0000000
---- a/apps/server/dist-types/game/playerSession.d.ts
++++ /dev/null
@@@ -1,140 +0,0 @@
--import type { Appearance, ClientInput } from '@openvibe/protocol';
--import type { ContentRegistry } from '@openvibe/content';
--import type { Inventory, SkillSet } from '@openvibe/gameplay';
--import { CraftQueue, type PlayerMoveState, type SurvivalStats } from '@openvibe/gameplay';
--import type { ItemStack, ReputationDto } from '@openvibe/gameplay';
--import type { EntityId, PlayerId, Quat, Vec3 } from '@openvibe/shared';
--export declare const INVENTORY_SIZE = 24;
--export declare const HOTBAR_SIZE = 6;
--/** Server-held physgun grab state. */
--export interface HeldProp {
-- entityId: EntityId;
-- /** Hold distance from the eye along the view ray (to the GRAB POINT). */
-- dist: number;
-- /** Grab point in the body's local space — the prop hangs from where you
-- * actually grabbed it, GMod-style, not from its center. */
-- localOffset: Vec3;
-- /** Angle snap (Shift+E): quantizes the FULL world orientation each drive
-- * tick — quantizing just the offsets is invisible on an arbitrary grab
-- * pose, which is why offset-snapping never visibly worked. */
-- snap: boolean;
-- snapStep: number;
-- /** Full body orientation at grab time, relative to the player's view yaw:
-- * the prop keeps its exact pose (incl. tilt/roll) and turns with the view. */
-- grabRot: Quat;
-- /** Grid-lock: quantize the drive target while held. */
-- grid: boolean;
-- /** Grid cell size (m). */
-- gridSize: number;
--}
--/**
-- * Per-connection authoritative player state. Everything gameplay-relevant
-- * lives here on the server; the client only ever sees replicated copies.
-- */
--export interface PlayerSession {
-- playerId: PlayerId;
-- entityId: EntityId;
-- token: string;
-- /** Canonical openvibe.network subject of the account; null for local guests. */
-- subjectId: string | null;
-- /**
-- * When the Network sign-in behind this session was issued (JWT iat, seconds); null for guests. A
-- * sign-out everywhere after it closes the session (network.user.token_valid_after).
-- */
-- authIat: number | null;
-- /** Character slot under the account token (0..2). */
-- charSlot: number;
-- /** openvibe.network rank; gates edit mode + moderation. */
-- rank: 'owner' | 'admin' | 'moderator' | null;
-- name: string;
-- move: PlayerMoveState;
-- /** Latest processed view angles (authoritative for ray origins). */
-- yaw: number;
-- pitch: number;
-- buttons: number;
-- inventory: Inventory;
-- skills: SkillSet;
-- /** Player ids THIS player trusts with their props (one-directional). */
-- friends: Set<string>;
-- appearance: Appearance;
-- craftQueue: CraftQueue;
-- activeHotbar: number;
-- /** Holstered: active slot's item is put away (empty hands). */
-- holstered: boolean;
-- held: HeldProp | null;
-- /** LMB held with the physgun out: the beam is firing. While nothing is
-- * latched the server re-tries the grab each tick (GMod sweep-to-grab). */
-- grabbing: boolean;
-- /** Worn armor piece (null = nothing). Durability lives in stack meta. */
-- armor: ItemStack | null;
-- /** Faction reputation scores (persistent). */
-- reputation: ReputationDto;
-- /** Unlocked blueprint recipe ids (persistent). */
-- unlocks: Set<string>;
-- /** Active contract + progress (persistent). */
-- activeJob: {
-- job: string;
-- progress: number;
-- } | null;
-- /** Chassis entity currently being driven (transient). */
-- driving: EntityId | null;
-- /** Survival vitals (server-authoritative; replicated only to the owner). */
-- stats: SurvivalStats;
-- /** Vitals changed since last stats message. */
-- statsDirty: boolean;
-- /** Container prop this player currently has open (pushed on change). */
-- openContainer: EntityId | null;
-- /** Most negative airborne vertical velocity (fall-damage tracking). */
-- fallVy: number;
-- /** Tick of the last accepted use/swing (server-side swing cooldown). */
-- lastUseTick: number;
-- /** Stance the kinematic physics body was last built for. */
-- bodyStance: number;
-- /** For weld feedback and equipment lookups without threading the registry. */
-- content: ContentRegistry;
-- /** Pending input commands (bounded queue: anti-speedup). */
-- inputQueue: ClientInput[];
-- lastInput: ClientInput | null;
-- /** Consecutive ticks simulated without a fresh input (jitter bridging). */
-- starvedTicks: number;
-- lastProcessedSeq: number;
-- /** Entity ids this client currently knows about (interest management). */
-- known: Set<EntityId>;
-- /** Player state changed since last persistence flush. */
-- dirty: boolean;
-- send(text: string): void;
-- closeConnection(code: number, reason: string): void;
--}
--export interface SessionInit {
-- playerId: PlayerId;
-- entityId: EntityId;
-- token: string;
-- subjectId?: string | null;
-- authIat?: number | null;
-- charSlot?: number;
-- rank?: 'owner' | 'admin' | 'moderator' | null;
-- name: string;
-- spawn: Vec3;
-- yaw: number;
-- inventory: Inventory;
-- skills: SkillSet;
-- friends: Set<string>;
-- appearance: Appearance;
-- stats?: Partial<SurvivalStats> | undefined;
-- armor?: ItemStack | null;
-- reputation?: ReputationDto;
-- unlocks?: string[];
-- activeJob?: {
-- job: string;
-- progress: number;
-- } | null;
-- content: ContentRegistry;
-- send(text: string): void;
-- closeConnection(code: number, reason: string): void;
--}
--export declare function createSession(init: SessionInit): PlayerSession;
--/** Eye position for view rays — stance-aware, matches the client camera. */
--export declare function eyePosition(session: PlayerSession, out: Vec3): Vec3;
--/** View direction from authoritative yaw/pitch. */
--export declare function viewDirection(session: PlayerSession, out: Vec3): Vec3;
--//# sourceMappingURL=playerSession.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/playerSession.d.ts.map b/apps/server/dist-types/game/playerSession.d.ts.map
ddeleted file mode 100644
iindex 79b9245..0000000
---- a/apps/server/dist-types/game/playerSession.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"playerSession.d.ts","sourceRoot":"","sources":["../../src/game/playerSession.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/replication.d.ts b/apps/server/dist-types/game/replication.d.ts
ddeleted file mode 100644
iindex de1c759..0000000
---- a/apps/server/dist-types/game/replication.d.ts
++++ /dev/null
@@@ -1,23 +0,0 @@
--import { type GameEntity } from '@openvibe/gameplay';
--import type { ServerSnapshot, WireEntity, WirePlayerState } from '@openvibe/protocol';
--import { type EntityId } from '@openvibe/shared';
--import type { GameWorld } from './gameWorld.js';
--import type { PlayerSession } from './playerSession.js';
--/**
-- * Interest management + snapshot building.
-- *
-- * Relevance is a radius test over the shared spatial hash: replication
-- * cost per client is proportional to NEARBY entities, never total
-- * entities. NPC perception and event relevance reuse the same index.
-- */
--export declare function wireEntityFor(world: GameWorld, entity: GameEntity): WireEntity;
--export declare function wirePlayerFor(session: PlayerSession): WirePlayerState;
--export interface InterestDiff {
-- entered: GameEntity[];
-- left: EntityId[];
--}
--/** Updates session.known in place and returns what changed. */
--export declare function updateInterest(session: PlayerSession, world: GameWorld, radius: number): InterestDiff;
--/** Snapshot for one session: relevant players + awake relevant prop bodies. */
--export declare function buildSnapshot(session: PlayerSession, world: GameWorld, sessions: Iterable<PlayerSession>, tick: number): ServerSnapshot;
--//# sourceMappingURL=replication.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/game/replication.d.ts.map b/apps/server/dist-types/game/replication.d.ts.map
ddeleted file mode 100644
iindex 39b4cc1..0000000
---- a/apps/server/dist-types/game/replication.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"replication.d.ts","sourceRoot":"","sources":["../../src/game/replication.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACpE,OAAO,KAAK,EACV,cAAc,EAEd,UAAU,EAEV,eAAe,EAChB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAEvD;;;;;;GAMG;AAEH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,UAAU,CA8B9E;AAgBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,eAAe,CAcrE;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,IAAI,EAAE,QAAQ,EAAE,CAAA;CACjB;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,CAC5B,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,GACb,YAAY,CAmBd;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,EACjC,IAAI,EAAE,MAAM,GACX,cAAc,CAsChB"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/havokLoader.d.ts b/apps/server/dist-types/havokLoader.d.ts
ddeleted file mode 100644
iindex 2414281..0000000
---- a/apps/server/dist-types/havokLoader.d.ts
++++ /dev/null
@@@ -1,6 +0,0 @@
--/**
-- * Loads the Havok wasm module in Node. Emscripten's default file resolution
-- * expects a browser, so the wasm binary is read from the package explicitly.
-- */
--export declare function loadHavok(): Promise<unknown>;
--//# sourceMappingURL=havokLoader.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/havokLoader.d.ts.map b/apps/server/dist-types/havokLoader.d.ts.map
ddeleted file mode 100644
iindex 0a8cb67..0000000
---- a/apps/server/dist-types/havokLoader.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"havokLoader.d.ts","sourceRoot":"","sources":["../src/havokLoader.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAKlD"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/main.d.ts b/apps/server/dist-types/main.d.ts
ddeleted file mode 100644
iindex 371115b..0000000
---- a/apps/server/dist-types/main.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=main.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/main.d.ts.map b/apps/server/dist-types/main.d.ts.map
ddeleted file mode 100644
iindex 28b87d8..0000000
---- a/apps/server/dist-types/main.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/contentPack.d.ts b/apps/server/dist-types/mods/contentPack.d.ts
ddeleted file mode 100644
iindex 0ee6c8b..0000000
---- a/apps/server/dist-types/mods/contentPack.d.ts
++++ /dev/null
@@@ -1,106 +0,0 @@
--/**
-- * `games-content@1`: the first mod kind — a declarative data pack, checked
-- * against the live `@openvibe/content` registry. It adds no code and no new
-- * definitions (the client ships the same registry, so a pack can only use
-- * what both sides already know). Each section needs one capability:
-- *
-- * announcements games.world.announce periodic server announcements
-- * props games.prop.place inert props placed in the world
-- *
-- * Mod props are owned by the mod, so prop protection keeps players from
-- * picking them up, moving or welding them, and only items without health,
-- * storage, shops, machines or vehicle parts may be placed: nothing a player
-- * could destroy for loot or use to move items into or out of the world.
-- */
--import type { ContentRegistry } from '@openvibe/content';
--import type { Validation } from './manifest.js';
--export declare const CAP_ANNOUNCE = "games.world.announce";
--export declare const CAP_PLACE_PROP = "games.prop.place";
--/** Capabilities the games-content@1 runtime can bind. Anything else is never granted here. */
--export declare const CONTENT_RUNTIME_CAPABILITIES: readonly string[];
--export interface ContentAnnouncement {
-- text: string;
-- everySeconds: number;
--}
--export interface ContentProp {
-- key: string;
-- item: string;
-- pos: [number, number, number];
-- yaw?: number;
--}
--export interface ContentPack {
-- announcements?: ContentAnnouncement[];
-- props?: ContentProp[];
--}
--export declare const CONTENT_PACK_SCHEMA: {
-- readonly $schema: "https://json-schema.org/draft/2020-12/schema";
-- readonly $id: "https://openvibe.games/schemas/games-content-pack.v1.json";
-- readonly title: "GamesContentPack";
-- readonly type: "object";
-- readonly additionalProperties: false;
-- readonly properties: {
-- readonly announcements: {
-- readonly type: "array";
-- readonly maxItems: 10;
-- readonly items: {
-- readonly type: "object";
-- readonly required: readonly ["text", "everySeconds"];
-- readonly additionalProperties: false;
-- readonly properties: {
-- readonly text: {
-- readonly type: "string";
-- readonly minLength: 1;
-- readonly maxLength: 200;
-- };
-- readonly everySeconds: {
-- readonly type: "integer";
-- readonly minimum: 60;
-- readonly maximum: 86400;
-- };
-- };
-- };
-- };
-- readonly props: {
-- readonly type: "array";
-- readonly maxItems: 50;
-- readonly items: {
-- readonly type: "object";
-- readonly required: readonly ["key", "item", "pos"];
-- readonly additionalProperties: false;
-- readonly properties: {
-- readonly key: {
-- readonly type: "string";
-- readonly pattern: "^[a-z0-9][a-z0-9_-]{0,39}$";
-- };
-- readonly item: {
-- readonly type: "string";
-- readonly minLength: 1;
-- readonly maxLength: 64;
-- };
-- readonly pos: {
-- readonly type: "array";
-- readonly minItems: 3;
-- readonly maxItems: 3;
-- readonly items: {
-- readonly type: "number";
-- readonly minimum: -4096;
-- readonly maximum: 4096;
-- };
-- };
-- readonly yaw: {
-- readonly type: "number";
-- readonly minimum: -10;
-- readonly maximum: 10;
-- };
-- };
-- };
-- };
-- };
--};
--/** Which capability each pack section needs. */
--export declare function capabilitiesUsedBy(pack: ContentPack): string[];
--/** Whether a content item may be placed as a mod prop (see the file comment). */
--export declare function placeableByMod(content: ContentRegistry, itemId: string): string | null;
--/** Structure (JSON Schema) plus the content-registry cross-checks. */
--export declare function validateContentPack(value: unknown, content: ContentRegistry): Validation<ContentPack>;
--//# sourceMappingURL=contentPack.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/contentPack.d.ts.map b/apps/server/dist-types/mods/contentPack.d.ts.map
ddeleted file mode 100644
iindex 10eb4b3..0000000
---- a/apps/server/dist-types/mods/contentPack.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"contentPack.d.ts","sourceRoot":"","sources":["../../src/mods/contentPack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAExD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C,eAAO,MAAM,YAAY,yBAAyB,CAAA;AAClD,eAAO,MAAM,cAAc,qBAAqB,CAAA;AAEhD,8FAA8F;AAC9F,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAAmC,CAAA;AAE7F,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,CAAC,EAAE,mBAAmB,EAAE,CAAA;IACrC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAA;CACtB;AAED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCtB,CAAA;AAKV,gDAAgD;AAChD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,EAAE,CAK9D;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAStF;AAED,sEAAsE;AACtE,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,eAAe,GACvB,UAAU,CAAC,WAAW,CAAC,CAmBzB"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/manifest.d.ts b/apps/server/dist-types/mods/manifest.d.ts
ddeleted file mode 100644
iindex d9ef089..0000000
---- a/apps/server/dist-types/mods/manifest.d.ts
++++ /dev/null
@@@ -1,55 +0,0 @@
--export interface ModManifest {
-- id: string;
-- name: string;
-- version: string;
-- description?: string;
-- publisher: {
-- type: string;
-- id: string;
-- };
-- target: string;
-- runtime: string;
-- permissions: {
-- capabilities: string[];
-- events?: string[];
-- modules?: string[];
-- mediaNamespaces?: string[];
-- };
-- resources: {
-- cpuMs: number;
-- memoryMb: number;
-- storageMb: number;
-- outboundHosts?: string[];
-- };
-- assets?: {
-- media_id: string;
-- role?: string;
-- variant?: string;
-- }[];
-- compatibility: {
-- runtime: string;
-- contracts?: string;
-- };
-- homepage?: string;
--}
--export type Validation<T> = {
-- ok: true;
-- value: T;
--} | {
-- ok: false;
-- errors: string[];
--};
--/** Structural validation against mods/mod-manifest.v1. */
--export declare function validateManifest(value: unknown): Validation<ModManifest>;
--/** The browser game's only mod target and runtime today. */
--export declare const GAMES_TARGET = "games.browser";
--export declare const CONTENT_RUNTIME = "games-content@1";
--/** Version of the content runtime, matched against compatibility.runtime. */
--export declare const CONTENT_RUNTIME_VERSION = "1.0.0";
--/**
-- * Whether this server can run the manifest at all. Executable runtimes
-- * (scripts) are refused here: they wait for OpenVibe.Host's sandbox
-- * (Stage C). Only declarative content packs run in Games today.
-- */
--export declare function checkForGames(m: ModManifest): string[];
--//# sourceMappingURL=manifest.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/manifest.d.ts.map b/apps/server/dist-types/mods/manifest.d.ts.map
ddeleted file mode 100644
iindex bb508a5..0000000
---- a/apps/server/dist-types/mods/manifest.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/mods/manifest.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE;QACX,YAAY,EAAE,MAAM,EAAE,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;QACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;QAClB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;KAC3B,CAAA;IACD,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IAC3F,MAAM,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAChE,aAAa,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAOD,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEpF,0DAA0D;AAC1D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAAC,WAAW,CAAC,CAMxE;AAED,4DAA4D;AAC5D,eAAO,MAAM,YAAY,kBAAkB,CAAA;AAC3C,eAAO,MAAM,eAAe,oBAAoB,CAAA;AAChD,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,UAAU,CAAA;AAE9C;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,EAAE,CAmBtD"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/manifest.test.d.ts b/apps/server/dist-types/mods/manifest.test.d.ts
ddeleted file mode 100644
iindex ced3f96..0000000
---- a/apps/server/dist-types/mods/manifest.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=manifest.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/manifest.test.d.ts.map b/apps/server/dist-types/mods/manifest.test.d.ts.map
ddeleted file mode 100644
iindex 7315d6c..0000000
---- a/apps/server/dist-types/mods/manifest.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"manifest.test.d.ts","sourceRoot":"","sources":["../../src/mods/manifest.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/manifestSchema.d.ts b/apps/server/dist-types/mods/manifestSchema.d.ts
ddeleted file mode 100644
iindex d5bb538..0000000
---- a/apps/server/dist-types/mods/manifestSchema.d.ts
++++ /dev/null
@@@ -1,163 +0,0 @@
--/**
-- * The mod manifest JSON Schema (mods/mod-manifest.v1) as Games validates it.
-- * The proposal for OpenVibe.Contracts is docs/contracts-proposal/mods/mod-manifest.v1.json;
-- * manifest.test.ts fails if the two ever differ. Once Contracts releases the
-- * schema, import it from openvibe-contracts instead of keeping this copy.
-- */
--export declare const MOD_MANIFEST_SCHEMA: {
-- readonly $schema: "https://json-schema.org/draft/2020-12/schema";
-- readonly $id: "https://openvibe.network/contracts/mods/mod-manifest.v1.json";
-- readonly title: "ModManifest";
-- readonly description: "A mod as the platform knows it (ADR-013): who publishes it, which runtime runs it, which capabilities it asks for and the resources it may use. Requested capabilities are only a request: the install's approved subset is the grant, and trust tiers are install metadata that never change a grant check. The runtime-specific payload (a data pack, a script bundle) is not part of the manifest.";
-- readonly type: "object";
-- readonly required: readonly ["id", "name", "version", "publisher", "target", "runtime", "permissions", "resources", "compatibility"];
-- readonly additionalProperties: false;
-- readonly properties: {
-- readonly id: {
-- readonly type: "string";
-- readonly pattern: "^mod_[0-9A-HJKMNP-TV-Z]{26}$";
-- readonly description: "Stable mod id; its principal subject is mod:<id>.";
-- };
-- readonly name: {
-- readonly type: "string";
-- readonly minLength: 1;
-- readonly maxLength: 80;
-- };
-- readonly version: {
-- readonly type: "string";
-- readonly pattern: "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$";
-- readonly description: "Semantic version of this release.";
-- };
-- readonly description: {
-- readonly type: "string";
-- readonly maxLength: 1000;
-- };
-- readonly publisher: {
-- readonly $ref: "../identity/subject-ref.v1.json";
-- readonly description: "Who publishes the mod (a user or an app subject).";
-- };
-- readonly target: {
-- readonly type: "string";
-- readonly pattern: "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$";
-- readonly description: "Where the mod runs: <service>.<surface>, e.g. games.browser, games.source, live.overlay.";
-- };
-- readonly runtime: {
-- readonly type: "string";
-- readonly pattern: "^[a-z][a-z0-9-]{1,39}@[1-9][0-9]{0,3}$";
-- readonly description: "Runtime adapter and its major version, e.g. games-content@1 (declarative data pack) or source-quickjs@1.";
-- };
-- readonly permissions: {
-- readonly type: "object";
-- readonly required: readonly ["capabilities"];
-- readonly additionalProperties: false;
-- readonly properties: {
-- readonly capabilities: {
-- readonly description: "Capability ids the mod asks for (3+ segments). The target runtime binds only those it implements, and only once granted.";
-- readonly type: "array";
-- readonly maxItems: 64;
-- readonly uniqueItems: true;
-- readonly items: {
-- readonly type: "string";
-- readonly pattern: "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$";
-- };
-- };
-- readonly events: {
-- readonly description: "Event types the mod wants delivered to it.";
-- readonly type: "array";
-- readonly maxItems: 64;
-- readonly uniqueItems: true;
-- readonly items: {
-- readonly type: "string";
-- readonly pattern: "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$";
-- };
-- };
-- readonly modules: {
-- readonly description: "User-module namespaces the mod wants to read or write (a trailing .* names a family).";
-- readonly type: "array";
-- readonly maxItems: 32;
-- readonly uniqueItems: true;
-- readonly items: {
-- readonly type: "string";
-- readonly pattern: "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*(\\.\\*)?$";
-- };
-- };
-- readonly mediaNamespaces: {
-- readonly description: "Media namespaces the mod wants to read or write.";
-- readonly type: "array";
-- readonly maxItems: 32;
-- readonly uniqueItems: true;
-- readonly items: {
-- readonly type: "string";
-- readonly pattern: "^[a-z][a-z0-9_-]*(\\.[a-z0-9_-]+)*$";
-- };
-- };
-- };
-- };
-- readonly resources: {
-- readonly type: "object";
-- readonly required: readonly ["cpuMs", "memoryMb", "storageMb"];
-- readonly additionalProperties: false;
-- readonly description: "The budget the mod asks for. Runtimes meter it; the sandbox that enforces it lives in OpenVibe.Host (Stage C).";
-- readonly properties: {
-- readonly cpuMs: {
-- readonly type: "number";
-- readonly minimum: 0;
-- readonly maximum: 1000;
-- readonly description: "CPU milliseconds per tick (game runtimes) or per request.";
-- };
-- readonly memoryMb: {
-- readonly type: "integer";
-- readonly minimum: 0;
-- readonly maximum: 4096;
-- };
-- readonly storageMb: {
-- readonly type: "integer";
-- readonly minimum: 0;
-- readonly maximum: 102400;
-- };
-- readonly outboundHosts: {
-- readonly description: "Hosts the mod may reach over the network. Empty or absent = none.";
-- readonly type: "array";
-- readonly maxItems: 32;
-- readonly uniqueItems: true;
-- readonly items: {
-- readonly type: "string";
-- readonly pattern: "^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$";
-- };
-- };
-- };
-- };
-- readonly assets: {
-- readonly description: "Assets the mod ships, as Media object references.";
-- readonly type: "array";
-- readonly maxItems: 256;
-- readonly items: {
-- readonly $ref: "../media/media-ref.v1.json";
-- };
-- };
-- readonly compatibility: {
-- readonly type: "object";
-- readonly required: readonly ["runtime"];
-- readonly additionalProperties: false;
-- readonly properties: {
-- readonly runtime: {
-- readonly type: "string";
-- readonly minLength: 1;
-- readonly maxLength: 100;
-- readonly description: "Semver range of the runtime this release works with, e.g. \">=1.0.0 <2.0.0\".";
-- };
-- readonly contracts: {
-- readonly type: "string";
-- readonly minLength: 1;
-- readonly maxLength: 100;
-- readonly description: "Semver range of openvibe-contracts releases it was built against.";
-- };
-- };
-- };
-- readonly homepage: {
-- readonly type: "string";
-- readonly pattern: "^https://[^\\s]{1,2000}$";
-- };
-- };
--};
--//# sourceMappingURL=manifestSchema.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/manifestSchema.d.ts.map b/apps/server/dist-types/mods/manifestSchema.d.ts.map
ddeleted file mode 100644
iindex 99f7e40..0000000
---- a/apps/server/dist-types/mods/manifestSchema.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"manifestSchema.d.ts","sourceRoot":"","sources":["../../src/mods/manifestSchema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4KtB,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/moderationEvents.test.d.ts b/apps/server/dist-types/mods/moderationEvents.test.d.ts
ddeleted file mode 100644
iindex 670e88a..0000000
---- a/apps/server/dist-types/mods/moderationEvents.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=moderationEvents.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/moderationEvents.test.d.ts.map b/apps/server/dist-types/mods/moderationEvents.test.d.ts.map
ddeleted file mode 100644
iindex e4db300..0000000
---- a/apps/server/dist-types/mods/moderationEvents.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"moderationEvents.test.d.ts","sourceRoot":"","sources":["../../src/mods/moderationEvents.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/registry.d.ts b/apps/server/dist-types/mods/registry.d.ts
ddeleted file mode 100644
iindex 5010345..0000000
---- a/apps/server/dist-types/mods/registry.d.ts
++++ /dev/null
@@@ -1,101 +0,0 @@
--/**
-- * The mod registry (ADR-013, roadmap Wave 12): installs, the approved subset
-- * of each install's requested capabilities, lifecycle and the audit log, all
-- * in Games' own SQLite database.
-- *
-- * install validate the manifest + pack, store it, grant the approved subset
-- * enable / disable
-- * grant / revokeGrant change one capability of an install
-- * revoke terminal: every grant ends and the runtime retracts the mod's
-- * effects on the next tick
-- *
-- * Every change is one transaction holding the registry rows, the audit rows
-- * and the `games.mod.*` outbox event. The in-memory view the runtime asks on
-- * its hot path (`isGranted`) is refreshed only after that commit. Taking down
-- * (revoke, disable) or putting back (enable after a disable) a mod someone
-- * else published also writes `games.moderation.action` for Network's
-- * moderation audit log (ADR-022) in that transaction.
-- *
-- * Trust tiers are metadata: `isGranted` never looks at them.
-- *
-- * ADR-013 puts grants in OpenVibe.Network (a `mod` principal per install).
-- * Until Network issues mod principals, the approved subset lives here; the
-- * shape (install -> approved capability ids, revocable at once) is the same
-- * so the source of truth can move without changing the runtime seam.
-- */
--import type { ContentRegistry } from '@openvibe/content';
--import type { ModAuditDto, ModInstallDto, ModTrustTier, PersistenceStore } from '@openvibe/persistence';
--import type { SubjectRef } from 'openvibe-sdk/core';
--import { type EventSink } from '../platform/gameEvents.js';
--import { type ContentPack } from './contentPack.js';
--import { type ModManifest } from './manifest.js';
--export declare const TRUST_TIERS: readonly ModTrustTier[];
--/** Who performed a registry change: an audit string and an event actor. */
--export interface ModActor {
-- /** `usr_…`, `ovn:<id>` for a pre-subject staff token, or `svc:<client>`. */
-- audit: string;
-- subject: SubjectRef;
--}
--export declare const SYSTEM_ACTOR: ModActor;
--export declare class ModError extends Error {
-- readonly code: string;
-- readonly status: number;
-- readonly errors: string[];
-- constructor(code: string, message: string, status?: number, errors?: string[]);
--}
--export interface InstallRequest {
-- manifest: unknown;
-- pack: unknown;
-- /** The approved subset of manifest.permissions.capabilities. */
-- approve?: unknown;
-- trustTier?: unknown;
-- enable?: unknown;
--}
--/** A mod as the runtime and the API see it. */
--export interface ModView {
-- mod: ModInstallDto;
-- manifest: ModManifest;
-- pack: ContentPack;
-- /** Capabilities currently granted (approved and not revoked). */
-- granted: ReadonlySet<string>;
--}
--export declare class ModRegistry {
-- private readonly store;
-- private readonly content;
-- private readonly events;
-- private readonly now;
-- private readonly views;
-- private revision;
-- constructor(store: PersistenceStore, content: ContentRegistry, events?: EventSink, now?: () => number);
-- /** Bumps on every committed change; the runtime reconciles when it moves. */
-- get version(): number;
-- list(): ModView[];
-- get(id: string): ModView | null;
-- isActive(id: string): boolean;
-- /**
-- * The one grant check. True only while the install is enabled and the
-- * capability is in its approved, unrevoked subset. The trust tier is not
-- * consulted — a tier label never grants anything.
-- */
-- isGranted(id: string, capability: string): boolean;
-- auditLog(id: string, limit?: number): ModAuditDto[];
-- /** Appends a runtime audit record (use, deny, retract, …). */
-- audit(id: string, action: string, capability: string | null, detail?: Record<string, unknown>): void;
-- install(req: InstallRequest, actor: ModActor): ModView;
-- enable(id: string, actor: ModActor): ModView;
-- disable(id: string, actor: ModActor): ModView;
-- /** Ends the install for good: every grant is revoked and the mod stops at the next tick. */
-- revoke(id: string, actor: ModActor, reason?: string): ModView;
-- grant(id: string, capability: string, actor: ModActor): ModView;
-- revokeGrant(id: string, capability: string, actor: ModActor): ModView;
-- private setStatus;
-- private assertGrantable;
-- private require;
-- private writeAudit;
-- /** games.moderation.action, unless the publisher is acting on their own mod. */
-- private moderated;
-- private emit;
-- /** Re-reads one install after a committed change. */
-- private refresh;
--}
--//# sourceMappingURL=registry.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/registry.d.ts.map b/apps/server/dist-types/mods/registry.d.ts.map
ddeleted file mode 100644
iindex 5b5852e..0000000
---- a/apps/server/dist-types/mods/registry.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/mods/registry.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/routes.d.ts b/apps/server/dist-types/mods/routes.d.ts
ddeleted file mode 100644
iindex 3c718d6..0000000
---- a/apps/server/dist-types/mods/routes.d.ts
++++ /dev/null
@@@ -1,30 +0,0 @@
--/**
-- * Mod registry HTTP API (JSON; errors are RFC 9457 problem+json with the
-- * legacy `error` field).
-- *
-- * GET /api/v1/mods public: installs, status, grants
-- * GET /api/v1/mods/:id public: one install incl. manifest + pack
-- * GET /api/v1/mods/:id/audit?limit= staff: install/grant/use/deny/revoke log
-- * POST /api/v1/mods staff: { manifest, pack, approve[], trustTier, enable }
-- * POST /api/v1/mods/:id/enable|disable staff
-- * POST /api/v1/mods/:id/revoke staff: { reason? } — terminal
-- * POST /api/v1/mods/:id/grants staff: { capability }
-- * DELETE /api/v1/mods/:id/grants/:capability staff
-- *
-- * Staff = an openvibe.network owner/admin session, or a service principal
-- * whose token (audience openvibe.games) carries `games.mod.manage`.
-- */
--import type { IncomingMessage, ServerResponse } from 'node:http';
--import type { Logger } from '@openvibe/shared';
--import { type ModActor, type ModRegistry, type ModView } from './registry.js';
--export declare const MODS_PREFIX = "/api/v1/mods";
--export interface ModsApi {
-- registry: ModRegistry;
-- /** Resolves the staff actor behind a request, or null. */
-- authorize(req: IncomingMessage): Promise<ModActor | null>;
-- log: Logger;
--}
--export declare function publicView(v: ModView): Record<string, unknown>;
--/** Returns true when the request was a mods API request (handled or answered). */
--export declare function handleModsRequest(req: IncomingMessage, res: ServerResponse, api: ModsApi): boolean;
--//# sourceMappingURL=routes.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/routes.d.ts.map b/apps/server/dist-types/mods/routes.d.ts.map
ddeleted file mode 100644
iindex 2f202b6..0000000
---- a/apps/server/dist-types/mods/routes.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/mods/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAY,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,KAAK,OAAO,EAAE,MAAM,eAAe,CAAA;AAcvF,eAAO,MAAM,WAAW,iBAAiB,CAAA;AAGzC,MAAM,WAAW,OAAO;IACtB,QAAQ,EAAE,WAAW,CAAA;IACrB,0DAA0D;IAC1D,SAAS,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IACzD,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAc9D;AA0DD,kFAAkF;AAClF,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,OAAO,GACX,OAAO,CA+FT"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/routes.test.d.ts b/apps/server/dist-types/mods/routes.test.d.ts
ddeleted file mode 100644
iindex 83da75b..0000000
---- a/apps/server/dist-types/mods/routes.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=routes.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/routes.test.d.ts.map b/apps/server/dist-types/mods/routes.test.d.ts.map
ddeleted file mode 100644
iindex dbf75e2..0000000
---- a/apps/server/dist-types/mods/routes.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"routes.test.d.ts","sourceRoot":"","sources":["../../src/mods/routes.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/runtime.d.ts b/apps/server/dist-types/mods/runtime.d.ts
ddeleted file mode 100644
iindex 593e136..0000000
---- a/apps/server/dist-types/mods/runtime.d.ts
++++ /dev/null
@@@ -1,89 +0,0 @@
--/**
-- * The mod runtime seam.
-- *
-- * A mod never touches the game server, the world or the database. It gets a
-- * `ModApi`: a frozen object of bindings, one per runtime capability, and
-- * every binding checks the registry AT CALL TIME. A capability the install
-- * was not granted — or one revoked a moment ago — throws before anything
-- * happens, so it cannot be reached from the mod side at all.
-- *
-- * `games-content@1` packs are interpreted here through that same API; an
-- * executable runtime (a script sandbox) would receive the same bindings.
-- * Executable mods are not accepted yet: sandboxed execution with enforced
-- * CPU, memory, storage and network budgets belongs to OpenVibe.Host
-- * (Stage C). Until then the manifest's resource budget is metered, not
-- * enforced by a sandbox.
-- *
-- * Each tick the runtime reconciles when the registry changed (and once a
-- * second otherwise): an inactive mod, or one that lost a capability, has
-- * its effects retracted — its props despawned, its announcements stopped —
-- * on that very tick.
-- */
--import type { ContentRegistry } from '@openvibe/content';
--import type { PersistenceStore } from '@openvibe/persistence';
--import type { Logger } from '@openvibe/shared';
--import type { ModRegistry } from './registry.js';
--/** What the game server lends the runtime. Only the runtime holds it. */
--export interface ModHost {
-- announce(text: string): void;
-- /** Spawns an inert prop owned by `owner`; returns its entity id. */
-- placeProp(spec: {
-- item: string;
-- pos: [number, number, number];
-- yaw: number;
-- owner: string;
-- }): string | null;
-- removeEntity(entityId: string): boolean;
-- entityExists(entityId: string): boolean;
--}
--/** The bindings a mod gets. Nothing else is reachable from a mod. */
--export interface ModApi {
-- readonly modId: string;
-- /** games.world.announce */
-- announce(text: string): void;
-- /** games.prop.place: places (or re-places) the prop for `key`. */
-- placeProp(key: string, item: string, pos: [number, number, number], yaw?: number): string | null;
-- /** games.prop.place: removes the prop for `key`. */
-- removeProp(key: string): boolean;
--}
--export declare class ModCapabilityError extends Error {
-- readonly modId: string;
-- readonly capability: string;
-- readonly code = "capability.denied";
-- constructor(modId: string, capability: string);
--}
--export interface ModRuntimeOptions {
-- /** Reconcile at least this often when nothing changed (ticks). */
-- reconcileEveryTicks: number;
-- now?: () => number;
--}
--export declare class ModRuntime {
-- private readonly registry;
-- private readonly store;
-- private readonly content;
-- private readonly log;
-- private readonly opts;
-- private seenVersion;
-- private nextTick;
-- /** modId -> last announcement time per announcement index. */
-- private readonly announced;
-- /** `${modId}|${cap}` denials already audited since the last registry change. */
-- private readonly deniedNoted;
-- private readonly budgetNotedAt;
-- private readonly now;
-- constructor(registry: ModRegistry, store: PersistenceStore, content: ContentRegistry, log: Logger, opts: ModRuntimeOptions);
-- /**
-- * The mod-facing API for one install. Frozen, and closed over nothing a
-- * mod could use to reach the host except through a checked binding.
-- */
-- bindingsFor(modId: string, host: ModHost): ModApi;
-- tick(tick: number, host: ModHost): void;
-- private runContentPack;
-- private attempt;
-- /** Despawns everything the mod placed. */
-- private retractProps;
-- private noteDenied;
-- /** Meters the declared per-tick CPU budget (enforcement: OpenVibe.Host, Stage C). */
-- private meter;
--}
--//# sourceMappingURL=runtime.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/runtime.d.ts.map b/apps/server/dist-types/mods/runtime.d.ts.map
ddeleted file mode 100644
iindex 9dbb5f9..0000000
---- a/apps/server/dist-types/mods/runtime.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/mods/runtime.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/runtime.test.d.ts b/apps/server/dist-types/mods/runtime.test.d.ts
ddeleted file mode 100644
iindex 4724582..0000000
---- a/apps/server/dist-types/mods/runtime.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=runtime.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/runtime.test.d.ts.map b/apps/server/dist-types/mods/runtime.test.d.ts.map
ddeleted file mode 100644
iindex 536b824..0000000
---- a/apps/server/dist-types/mods/runtime.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"runtime.test.d.ts","sourceRoot":"","sources":["../../src/mods/runtime.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/testFixtures.d.ts b/apps/server/dist-types/mods/testFixtures.d.ts
ddeleted file mode 100644
iindex b30703b..0000000
---- a/apps/server/dist-types/mods/testFixtures.d.ts
++++ /dev/null
@@@ -1,5 +0,0 @@
--/** Shared test fixtures for the mod platform (not used at runtime). */
--import type { ModManifest } from './manifest.js';
--export declare const MOD_ID = "mod_01JABCDEFGHJKMNPQRSTVWXYZ0";
--export declare function sampleManifest(overrides?: Partial<ModManifest>): ModManifest;
--//# sourceMappingURL=testFixtures.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/mods/testFixtures.d.ts.map b/apps/server/dist-types/mods/testFixtures.d.ts.map
ddeleted file mode 100644
iindex 88d1bf4..0000000
---- a/apps/server/dist-types/mods/testFixtures.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"testFixtures.d.ts","sourceRoot":"","sources":["../../src/mods/testFixtures.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,eAAO,MAAM,MAAM,mCAAmC,CAAA;AAEtD,wBAAgB,cAAc,CAAC,SAAS,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAchF"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/editorLocks.d.ts b/apps/server/dist-types/net/editorLocks.d.ts
ddeleted file mode 100644
iindex aab0545..0000000
---- a/apps/server/dist-types/net/editorLocks.d.ts
++++ /dev/null
@@@ -1,34 +0,0 @@
--/**
-- * Server-authoritative transient edit locks for the collaborative map
-- * editor. Locks are leases: they expire unless the owner's connection
-- * keeps heartbeating, and they never serialize into the map. Pure class —
-- * unit tested; the WebSocket layer wires it to peers.
-- */
--export interface LockResult {
-- granted: boolean;
-- /** On denial: the object ids that were already held by someone else. */
-- blocked: {
-- id: string;
-- owner: number;
-- }[];
--}
--export declare class LockManager {
-- /** object id -> owning peer id */
-- private readonly locks;
-- /** peer id -> last heartbeat ms */
-- private readonly beats;
-- /**
-- * Atomically acquire ALL ids for a peer (all-or-nothing): a multi-select
-- * transform must never start with half its objects locked.
-- */
-- acquire(peer: number, ids: string[], now: number): LockResult;
-- release(peer: number, ids: string[]): void;
-- releaseAll(peer: number): string[];
-- heartbeat(peer: number, now: number): void;
-- /** Expire leases of peers that stopped heartbeating; returns freed ids. */
-- sweep(now: number): string[];
-- ownerOf(id: string): number | undefined;
-- /** Full lock state for broadcasting: id -> owner peer id. */
-- state(): Record<string, number>;
--}
--//# sourceMappingURL=editorLocks.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/editorLocks.d.ts.map b/apps/server/dist-types/net/editorLocks.d.ts.map
ddeleted file mode 100644
iindex 0e917d0..0000000
---- a/apps/server/dist-types/net/editorLocks.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorLocks.d.ts","sourceRoot":"","sources":["../../src/net/editorLocks.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,wEAAwE;IACxE,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACzC;AAID,qBAAa,WAAW;IACtB,kCAAkC;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4B;IAClD,mCAAmC;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4B;IAElD;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU;IAY7D,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI;IAM1C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAYlC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAI1C,2EAA2E;IAC3E,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE;IAU5B,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIvC,6DAA6D;IAC7D,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;CAGhC"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/editorLocks.test.d.ts b/apps/server/dist-types/net/editorLocks.test.d.ts
ddeleted file mode 100644
iindex f6cdfe2..0000000
---- a/apps/server/dist-types/net/editorLocks.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=editorLocks.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/editorLocks.test.d.ts.map b/apps/server/dist-types/net/editorLocks.test.d.ts.map
ddeleted file mode 100644
iindex bd2f2b9..0000000
---- a/apps/server/dist-types/net/editorLocks.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorLocks.test.d.ts","sourceRoot":"","sources":["../../src/net/editorLocks.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/editorWs.d.ts b/apps/server/dist-types/net/editorWs.d.ts
ddeleted file mode 100644
iindex c1a9bad..0000000
---- a/apps/server/dist-types/net/editorWs.d.ts
++++ /dev/null
@@@ -1,14 +0,0 @@
--import type { Server } from 'node:http';
--import { WebSocketServer } from 'ws';
--import type { Logger } from '@openvibe/shared';
--import type { EditorAuth } from './httpServer.js';
--export interface EditorHub {
-- wss: WebSocketServer;
-- /** Graceful stop: the lease sweeper stops. */
-- stop(): void;
-- broadcastSaved(revision: string): void;
-- /** Live peer count, for tests and diagnostics. */
-- peerCount(): number;
--}
--export declare function attachEditorWs(http: Server, auth: EditorAuth, log: Logger): EditorHub;
--//# sourceMappingURL=editorWs.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/editorWs.d.ts.map b/apps/server/dist-types/net/editorWs.d.ts.map
ddeleted file mode 100644
iindex b3c2e61..0000000
---- a/apps/server/dist-types/net/editorWs.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"editorWs.d.ts","sourceRoot":"","sources":["../../src/net/editorWs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,eAAe,EAAkB,MAAM,IAAI,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAU9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAgCjD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,eAAe,CAAA;IACpB,8CAA8C;IAC9C,IAAI,IAAI,IAAI,CAAA;IACZ,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACtC,kDAAkD;IAClD,SAAS,IAAI,MAAM,CAAA;CACpB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,CAqMrF"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/gracefulStop.d.ts b/apps/server/dist-types/net/gracefulStop.d.ts
ddeleted file mode 100644
iindex 48a913b..0000000
---- a/apps/server/dist-types/net/gracefulStop.d.ts
++++ /dev/null
@@@ -1,38 +0,0 @@
--import type { Server } from 'node:http';
--import type { WebSocketServer } from 'ws';
--/**
-- * Graceful stop (roadmap WS-P lifecycle; openvibe-contracts manifests/services/games.json →
-- * lifecycle.shutdown). main.ts runs the order; these are the network halves of it.
-- *
-- * The unit's TimeoutStopSec is 30: the whole stop is bounded by DEADLINE_MS (the manifest's
-- * deadlineSeconds), well inside it. Requests in flight get DRAIN_MS; sockets that do not answer the
-- * close handshake within SOCKETS_MS are terminated.
-- */
--export declare const DEADLINE_MS = 10000;
--export declare const DRAIN_MS = 5000;
--export declare const SOCKETS_MS = 3000;
--/** WebSocket close code for "service restart" (RFC 6455 registry): the clients reconnect on their own. */
--export declare const CLOSE_RESTART = 1012;
--export interface HttpDrainer {
-- /**
-- * Stop taking connections, close idle keep-alive ones, answer every request in flight with
-- * Connection: close. Resolves 0 once every connection has ended, or after `ms` with how many
-- * requests were still open (their connections are then cut).
-- */
-- close(ms: number): Promise<number>;
-- stopping(): boolean;
--}
--/** Tracks responses in flight on `server`. Attach before the server takes requests. */
--export declare function httpDrainer(server: Server): HttpDrainer;
--/**
-- * Close every client of `wss` with 1012 (service restart) and `reason`; a client that has not
-- * finished the close handshake after `ms` is terminated. Resolves once every one has closed (its
-- * 'close' handlers have run).
-- */
--export declare function closeSockets(wss: WebSocketServer, ms: number, reason?: string): Promise<{
-- closed: number;
-- terminated: number;
--}>;
--/** Await `p`, but no longer than `ms`. */
--export declare function within<T>(ms: number, p: Promise<T> | undefined | null): Promise<T | undefined>;
--//# sourceMappingURL=gracefulStop.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/gracefulStop.d.ts.map b/apps/server/dist-types/net/gracefulStop.d.ts.map
ddeleted file mode 100644
iindex 5ffdc2b..0000000
---- a/apps/server/dist-types/net/gracefulStop.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"gracefulStop.d.ts","sourceRoot":"","sources":["../../src/net/gracefulStop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,WAAW,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,IAAI,CAAA;AAEzC;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,QAAS,CAAA;AACjC,eAAO,MAAM,QAAQ,OAAQ,CAAA;AAC7B,eAAO,MAAM,UAAU,OAAQ,CAAA;AAE/B,0GAA0G;AAC1G,eAAO,MAAM,aAAa,OAAO,CAAA;AAEjC,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAClC,QAAQ,IAAI,OAAO,CAAA;CACpB;AAED,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAiCvD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,eAAe,EACpB,EAAE,EAAE,MAAM,EACV,MAAM,SAAmB,GACxB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CA+BjD;AAED,0CAA0C;AAC1C,wBAAgB,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAS9F"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/httpServer.d.ts b/apps/server/dist-types/net/httpServer.d.ts
ddeleted file mode 100644
iindex 817eda3..0000000
---- a/apps/server/dist-types/net/httpServer.d.ts
++++ /dev/null
@@@ -1,39 +0,0 @@
--import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
--import type { Logger } from '@openvibe/shared';
--import type { ServerMetrics } from '../observability/metrics.js';
--import type { MapFileV2 } from '@openvibe/content';
--export interface EditorAuth {
-- /** Shared-secret fallback. */
-- key: string | null;
-- /** openvibe.network session endpoint; token validated there when configured. */
-- networkAuthUrl: string | null;
--}
--/** openvibe.network OAuth2 client — powers the /auth/login → /auth/callback flow. */
--export interface OAuthConfig {
-- clientId: string;
-- clientSecret: string;
-- /** Public openvibe.network base, e.g. https://openvibe.network */
-- baseUrl: string;
-- /** Our public base, e.g. https://openvibe.games (redirect_uri host). */
-- selfUrl: string;
-- /** Public base of the play vhost, e.g. https://play.openvibe.games. */
-- playUrl: string;
--}
--/** Platform seams (roadmap Wave 12): extra API routes and a stored-asset hook. */
--export interface HttpPlatformHooks {
-- /** Answers a request it owns (returns true), e.g. the mod registry API. */
-- handle?: (req: IncomingMessage, res: ServerResponse) => boolean;
-- /** A map asset was stored (or found already stored) locally. */
-- onAssetStored?: (asset: {
-- hash: string;
-- url: string;
-- bytes: number;
-- mime: string;
-- }) => void;
--}
--export declare function createHttpServer(staticDir: string | null, metrics: ServerMetrics, log: Logger, mapPath?: string, editorAuth?: EditorAuth, onMapSaved?: (next: MapFileV2, revision: string) => void, listCharacters?: (token: string, auth: string | undefined) => Promise<{
-- slot: number;
-- name: string;
-- appearance: unknown;
--}[]>, oauth?: OAuthConfig | null, platform?: HttpPlatformHooks): Server;
--//# sourceMappingURL=httpServer.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/httpServer.d.ts.map b/apps/server/dist-types/net/httpServer.d.ts.map
ddeleted file mode 100644
iindex 4f708ff..0000000
---- a/apps/server/dist-types/net/httpServer.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"httpServer.d.ts","sourceRoot":"","sources":["../../src/net/httpServer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAwBhE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AA2BlD,MAAM,WAAW,UAAU;IACzB,8BAA8B;IAC9B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,gFAAgF;IAChF,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAA;IACf,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAA;CAChB;AAmCD,kFAAkF;AAClF,MAAM,WAAW,iBAAiB;IAChC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAA;IAC/D,gEAAgE;IAChE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CAC5F;AAYD,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,UAAU,EACvB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,EACxD,cAAc,CAAC,EAAE,CACf,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GAAG,SAAS,KACrB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,EAAE,CAAC,EACnE,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,EAC1B,QAAQ,CAAC,EAAE,iBAAiB,GAC3B,MAAM,CAmkBR"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/httpServer.test.d.ts b/apps/server/dist-types/net/httpServer.test.d.ts
ddeleted file mode 100644
iindex df9e18e..0000000
---- a/apps/server/dist-types/net/httpServer.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=httpServer.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/httpServer.test.d.ts.map b/apps/server/dist-types/net/httpServer.test.d.ts.map
ddeleted file mode 100644
iindex bdac944..0000000
---- a/apps/server/dist-types/net/httpServer.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"httpServer.test.d.ts","sourceRoot":"","sources":["../../src/net/httpServer.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/mapAssetStore.d.ts b/apps/server/dist-types/net/mapAssetStore.d.ts
ddeleted file mode 100644
iindex 54b9708..0000000
---- a/apps/server/dist-types/net/mapAssetStore.d.ts
++++ /dev/null
@@@ -1,46 +0,0 @@
--/** Big enough for a detailed 4K texture or a scene model; not unbounded. */
--export declare const MAX_ASSET_BYTES: number;
--export interface AssetKind {
-- mime: string;
-- ext: string;
--}
--/**
-- * What these bytes actually are, or null.
-- *
-- * Deliberately does NOT consult the filename or a client-supplied type: an
-- * uploader that can pick the stored extension can pick what the server will
-- * later serve it as.
-- */
--export declare function sniffAsset(bytes: Buffer): AssetKind | null;
--export interface StoredAsset {
-- /** `sha256-<hex>` — the content identity the map document references. */
-- hash: string;
-- /** `/map-assets/<hash>.<ext>` */
-- url: string;
-- bytes: number;
-- mime: string;
--}
--export type StoreOutcome = {
-- ok: true;
-- asset: StoredAsset;
-- deduplicated: boolean;
--} | {
-- ok: false;
-- status: 400 | 413;
-- error: string;
--};
--export declare const hashOf: (bytes: Buffer) => string;
--/** The stored filename for a hash. Ids are server-generated; never client input. */
--export declare const assetFileName: (hash: string, ext: string) => string;
--/**
-- * Store `bytes` under their content hash. Returns the existing asset without
-- * writing when the content is already present — the dedupe that lets an
-- * unchanged paint mask be re-saved for free.
-- */
--export declare function storeAsset(assetsDir: string, bytes: Buffer): Promise<StoreOutcome>;
--/**
-- * Whether a `/map-assets/<name>` request is for a content-addressed asset,
-- * which is immutable and can therefore be cached forever.
-- */
--export declare const isContentAddressed: (name: string) => boolean;
--//# sourceMappingURL=mapAssetStore.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/mapAssetStore.d.ts.map b/apps/server/dist-types/net/mapAssetStore.d.ts.map
ddeleted file mode 100644
iindex 69f6578..0000000
---- a/apps/server/dist-types/net/mapAssetStore.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapAssetStore.d.ts","sourceRoot":"","sources":["../../src/net/mapAssetStore.ts"],"names":[],"mappings":"AAqBA,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ;AAmCD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAE1D;AAED,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,WAAW,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,GACvD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEnD,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,KAAG,MACuB,CAAA;AAE9D,oFAAoF;AACpF,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,KAAK,MAAM,KAAG,MAA0B,CAAA;AAEpF;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA0BxF;AAYD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,OACH,CAAA"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/mapAssetStore.test.d.ts b/apps/server/dist-types/net/mapAssetStore.test.d.ts
ddeleted file mode 100644
iindex 879ee7d..0000000
---- a/apps/server/dist-types/net/mapAssetStore.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=mapAssetStore.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/mapAssetStore.test.d.ts.map b/apps/server/dist-types/net/mapAssetStore.test.d.ts.map
ddeleted file mode 100644
iindex aaf7013..0000000
---- a/apps/server/dist-types/net/mapAssetStore.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapAssetStore.test.d.ts","sourceRoot":"","sources":["../../src/net/mapAssetStore.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/mapStore.d.ts b/apps/server/dist-types/net/mapStore.d.ts
ddeleted file mode 100644
iindex 339445d..0000000
---- a/apps/server/dist-types/net/mapStore.d.ts
++++ /dev/null
@@@ -1,32 +0,0 @@
--import { type MapFileV2 } from '@openvibe/content';
--export interface MapRecord {
-- map: MapFileV2;
-- /** SHA-256 of the canonical form — the ETag clients send back as If-Match. */
-- revision: string;
-- canonical: string;
--}
--export type SaveOutcome = {
-- status: 200;
-- record: MapRecord;
--} | {
-- status: 400 | 409 | 413 | 422;
-- error: string;
-- issues?: string[];
-- revision?: string;
--};
--/** 24 MB of JSON is far more than any real map; refuse rather than buffer it. */
--export declare const MAX_MAP_BYTES: number;
--export declare function revisionOf(canonical: string): string;
--/**
-- * Load the artifact, migrating v1 on the way in. A missing or unreadable file
-- * yields an empty v2 map rather than throwing — a fresh server should boot to
-- * a blank world, not fail.
-- */
--export declare function loadMap(mapPath: string): Promise<MapRecord>;
--/**
-- * Validate and persist a save. `ifMatch` is the revision the editor last
-- * loaded; a mismatch is a 409 so the client can show a conflict instead of
-- * overwriting someone else's work.
-- */
--export declare function saveMap(mapPath: string, body: string, current: MapRecord, ifMatch: string | undefined): Promise<SaveOutcome>;
--//# sourceMappingURL=mapStore.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/mapStore.d.ts.map b/apps/server/dist-types/net/mapStore.d.ts.map
ddeleted file mode 100644
iindex 630e5fc..0000000
---- a/apps/server/dist-types/net/mapStore.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapStore.d.ts","sourceRoot":"","sources":["../../src/net/mapStore.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAiD,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAEjG,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,SAAS,CAAA;IACd,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAClC;IAAE,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1F,iFAAiF;AACjF,eAAO,MAAM,aAAa,QAAmB,CAAA;AAE7C,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAYD;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAUjE;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,EAClB,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,OAAO,CAAC,WAAW,CAAC,CA8BtB"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/mapStore.test.d.ts b/apps/server/dist-types/net/mapStore.test.d.ts
ddeleted file mode 100644
iindex b4717e4..0000000
---- a/apps/server/dist-types/net/mapStore.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=mapStore.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/mapStore.test.d.ts.map b/apps/server/dist-types/net/mapStore.test.d.ts.map
ddeleted file mode 100644
iindex d17ee97..0000000
---- a/apps/server/dist-types/net/mapStore.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mapStore.test.d.ts","sourceRoot":"","sources":["../../src/net/mapStore.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/networkAuth.d.ts b/apps/server/dist-types/net/networkAuth.d.ts
ddeleted file mode 100644
iindex 644ca5d..0000000
---- a/apps/server/dist-types/net/networkAuth.d.ts
++++ /dev/null
@@@ -1,35 +0,0 @@
--export type OpenVibeRank = 'owner' | 'admin' | 'moderator' | null;
--export interface NetworkUser {
-- /** The Network's own account id (legacy; integer today). */
-- id: string;
-- name: string;
-- rank: OpenVibeRank;
-- /**
-- * Canonical subject (`usr_…`, or `gst_…` for a Network guest) from the
-- * token's `subject_id` claim; null only for tokens older than subjects.
-- */
-- subjectId: string | null;
--}
--export declare function isCanonicalSubject(value: unknown): value is string;
--/** The rank the staff map gives these (accepted) claims. */
--export declare function rankOf(claims: Record<string, unknown>): OpenVibeRank;
--/** The account record as openvibe.network's /api/auth/me returns it. */
--export interface NetworkAccount {
-- id?: string | number;
-- username?: string;
-- role?: string;
-- is_banned?: number;
-- /** Canonical subject (Wave 1); mirrors the token's `subject_id` claim. */
-- subject_id?: string;
-- [key: string]: unknown;
--}
--/**
-- * Fetches the raw account behind a bearer token from openvibe.network, or
-- * null when the token is missing, rejected or the Network is unreachable.
-- * The same-origin /auth/me endpoint hands this object to the shared navbar.
-- */
--export declare function fetchNetworkAccount(url: string | null, auth: string | undefined): Promise<NetworkAccount | null>;
--export declare function resolveNetworkUser(url: string | null, auth: string | undefined): Promise<NetworkUser | null>;
--/** The map editor and mod administration: staff.games.manage (rank owner or admin). */
--export declare function canEditMap(rank: OpenVibeRank): boolean;
--//# sourceMappingURL=networkAuth.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/networkAuth.d.ts.map b/apps/server/dist-types/net/networkAuth.d.ts.map
ddeleted file mode 100644
iindex 1c3a299..0000000
---- a/apps/server/dist-types/net/networkAuth.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"networkAuth.d.ts","sourceRoot":"","sources":["../../src/net/networkAuth.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,IAAI,CAAA;AAEjE,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,YAAY,CAAA;IAClB;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAKD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAElE;AAiBD,4DAA4D;AAC5D,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,YAAY,CAKpE;AAmBD,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAahC;AAED,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,GAAG,IAAI,EAClB,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAgB7B;AAED,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAEtD"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/networkAuth.test.d.ts b/apps/server/dist-types/net/networkAuth.test.d.ts
ddeleted file mode 100644
iindex f1cc915..0000000
---- a/apps/server/dist-types/net/networkAuth.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=networkAuth.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/networkAuth.test.d.ts.map b/apps/server/dist-types/net/networkAuth.test.d.ts.map
ddeleted file mode 100644
iindex e4a9627..0000000
---- a/apps/server/dist-types/net/networkAuth.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"networkAuth.test.d.ts","sourceRoot":"","sources":["../../src/net/networkAuth.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/notFound.d.ts b/apps/server/dist-types/net/notFound.d.ts
ddeleted file mode 100644
iindex 7b2885a..0000000
---- a/apps/server/dist-types/net/notFound.d.ts
++++ /dev/null
@@@ -1,23 +0,0 @@
--import type { IncomingMessage, ServerResponse } from 'node:http';
--/**
-- * The answer for a path no route matches: a real 404, never the landing page
-- * with a 200 (a "soft 404" that search engines index as a duplicate of the
-- * portal and monitors read as up).
-- *
-- * Page requests get a small self-contained HTML page that names the path and
-- * links to the host's real entry points. API paths and clients asking for
-- * JSON get `{"error":"not_found"}` (the shape /auth/* already answers). A
-- * missing asset (any extension but .html) gets plain text.
-- */
--export interface NotFoundLink {
-- href: string;
-- label: string;
--}
--export type NotFoundKind = 'html' | 'json' | 'text';
--export declare function escapeHtml(value: string): string;
--/** Which body a 404 for `path` gets. */
--export declare function notFoundKind(req: IncomingMessage, path: string): NotFoundKind;
--export declare function notFoundPage(path: string, links: readonly NotFoundLink[]): string;
--/** Answers 404 for `path` with the body its client can use; HEAD gets the headers only. */
--export declare function sendNotFound(req: IncomingMessage, res: ServerResponse, path: string, links: readonly NotFoundLink[]): void;
--//# sourceMappingURL=notFound.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/notFound.d.ts.map b/apps/server/dist-types/net/notFound.d.ts.map
ddeleted file mode 100644
iindex 3d17783..0000000
---- a/apps/server/dist-types/net/notFound.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"notFound.d.ts","sourceRoot":"","sources":["../../src/net/notFound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAGhE;;;;;;;;;GASG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;AAUnD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAO7E;AAKD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAuCjF;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CAC1B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,YAAY,EAAE,GAC7B,IAAI,CAmBN"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/sso.d.ts b/apps/server/dist-types/net/sso.d.ts
ddeleted file mode 100644
iindex 37a7141..0000000
---- a/apps/server/dist-types/net/sso.d.ts
++++ /dev/null
@@@ -1,93 +0,0 @@
--/**
-- * openvibe.network SSO helpers for the /auth/* routes: where a sign-in may
-- * send the browser afterwards, the cookies that carry the session and the
-- * cross-site "is anyone signed in here?" hint, and the state cookie that ties
-- * a callback to the login that started it.
-- *
-- * Kept free of http.Server types so the rules are unit-testable on their own.
-- */
--/** Name of the cookie holding the openvibe.network access token. */
--export declare const SESSION_COOKIE = "ovg_sso";
--/**
-- * Cross-network hint read by the shared navbar: `account` means this browser
-- * has signed in somewhere on OpenVibe (so a silent prompt=none sign-in is
-- * worth attempting), `guest` means it signed out. JS-readable by design.
-- */
--export declare const SSO_HINT_COOKIE = "ov_sso_hint";
--/** Short-lived, HttpOnly: pins the OAuth `state` and the post-login `next`. */
--export declare const LOGIN_STATE_COOKIE = "ovg_login";
--export declare function isSilentDenial(error: string | null | undefined): boolean;
--export interface NextOrigins {
-- /** openvibe.network — the sign-in-everywhere / sign-out-everywhere chains hop through it. */
-- network: string;
-- /** Our own public bases (apex + play host). */
-- self: string[];
--}
--/**
-- * Validates a `next` target. Accepts a same-site path (`/play`, never `//evil`)
-- * or an absolute https URL on openvibe.network or one of our own hosts.
-- * Anything else yields null so an open redirect is impossible.
-- */
--export declare function safeNext(raw: string | null | undefined, origins: NextOrigins): string | null;
--/** Appends `sso=none` to a validated `next` so the landing page knows the silent attempt found nobody. */
--export declare function withSsoNone(next: string): string;
--export declare function parseCookies(header: string | undefined): Record<string, string>;
--export interface LoginState {
-- state: string;
-- next: string | null;
-- silent: boolean;
--}
--export declare function encodeLoginState(s: LoginState): string;
--export declare function decodeLoginState(raw: string | undefined): LoginState | null;
--export declare function sessionCookie(token: string, secure: boolean): string;
--export declare function clearSessionCookie(): string;
--export declare function ssoHintCookie(value: 'account' | 'guest', secure: boolean): string;
--export declare function loginStateCookie(s: LoginState, secure: boolean): string;
--export declare function clearLoginStateCookie(): string;
--/**
-- * Decodes a JWT's payload WITHOUT checking its signature. Only for reading
-- * claims we cross-check locally (the FedCM nonce); never for trusting an
-- * identity.
-- */
--export declare function decodeJwtPayload(token: string): Record<string, unknown> | null;
--/** True when the assertion's `nonce` claim is exactly the nonce the page posted. */
--export declare function fedcmNonceMatches(token: string, nonce: string): boolean;
--export interface FedcmBody {
-- token: string;
-- nonce: string;
--}
--/** Max bytes a /auth/fedcm body may carry: an assertion JWT is a few KB at most. */
--export declare const MAX_FEDCM_BODY_BYTES: number;
--/**
-- * Parses and validates the JSON body of POST /auth/fedcm. Returns an error
-- * code (for a 400) instead of the body when it is malformed, incomplete or
-- * its nonce does not match the assertion's.
-- */
--export declare function parseFedcmBody(raw: string): {
-- body: FedcmBody;
--} | {
-- error: string;
--};
--/**
-- * Remembers, briefly, which session tokens openvibe.network last confirmed so
-- * the silent-login shortcut (`/auth/login?silent=1` with an ovg_sso cookie
-- * that already resolves) does not cost a Network lookup on every page. Only
-- * positive answers are kept: a miss just falls through to the normal
-- * prompt=none round trip, which is the right answer for it anyway.
-- */
--export declare class SessionCheckCache {
-- private readonly ttlMs;
-- private readonly maxEntries;
-- private readonly hits;
-- constructor(ttlMs?: number, maxEntries?: number);
-- has(token: string, now?: number): boolean;
-- remember(token: string, now?: number): void;
-- forget(token: string): void;
--}
--/**
-- * The page the callback answers with: it mirrors the session into
-- * localStorage (the game client reads it from there) before moving on, so a
-- * cross-origin `next` still leaves this origin signed in.
-- */
--export declare function sessionHandoffHtml(token: string | null, hint: 'account' | 'guest', dest: string): string;
--//# sourceMappingURL=sso.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/sso.d.ts.map b/apps/server/dist-types/net/sso.d.ts.map
ddeleted file mode 100644
iindex 532bca4..0000000
---- a/apps/server/dist-types/net/sso.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"sso.d.ts","sourceRoot":"","sources":["../../src/net/sso.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/sso.test.d.ts b/apps/server/dist-types/net/sso.test.d.ts
ddeleted file mode 100644
iindex b1baae2..0000000
---- a/apps/server/dist-types/net/sso.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=sso.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/sso.test.d.ts.map b/apps/server/dist-types/net/sso.test.d.ts.map
ddeleted file mode 100644
iindex 81b9cd5..0000000
---- a/apps/server/dist-types/net/sso.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"sso.test.d.ts","sourceRoot":"","sources":["../../src/net/sso.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/wsTransport.d.ts b/apps/server/dist-types/net/wsTransport.d.ts
ddeleted file mode 100644
iindex bc63510..0000000
---- a/apps/server/dist-types/net/wsTransport.d.ts
++++ /dev/null
@@@ -1,6 +0,0 @@
--import type { Server } from 'node:http';
--import { WebSocketServer } from 'ws';
--import type { Logger } from '@openvibe/shared';
--import type { GameServer } from '../game/gameServer.js';
--export declare function attachWebSocket(http: Server, game: GameServer, log: Logger): WebSocketServer;
--//# sourceMappingURL=wsTransport.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/net/wsTransport.d.ts.map b/apps/server/dist-types/net/wsTransport.d.ts.map
ddeleted file mode 100644
iindex f12abf7..0000000
---- a/apps/server/dist-types/net/wsTransport.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"wsTransport.d.ts","sourceRoot":"","sources":["../../src/net/wsTransport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,eAAe,EAAkB,MAAM,IAAI,CAAA;AAEpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,EAAkB,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAWvE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,CAqE5F"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/observability/metrics.d.ts b/apps/server/dist-types/observability/metrics.d.ts
ddeleted file mode 100644
iindex 081b0e0..0000000
---- a/apps/server/dist-types/observability/metrics.d.ts
++++ /dev/null
@@@ -1,45 +0,0 @@
--import type { Logger } from '@openvibe/shared';
--/**
-- * Development observability: cheap counters sampled by the tick loop,
-- * exposed as JSON on /metrics and summarized to the structured log
-- * periodically. A real metrics backend can consume the same object later.
-- */
--export declare class ServerMetrics {
-- tick: number;
-- /** When the last simulation tick completed (epoch ms; 0 = none yet). /api/ready reads it. */
-- lastTickAt: number;
-- tickDurationMs: number;
-- physicsMs: number;
-- sessions: number;
-- entities: number;
-- awakeBodies: number;
-- settledBodies: number;
-- bytesOut: number;
-- messagesOut: number;
-- snapshotBytes: number;
-- dbDirtyQueue: number;
-- /** Live constraints and the multi-prop structures they form. */
-- constraints: number;
-- constraintIslands: number;
-- /** Coarse world regions: active = player ±1 ring; occupied = has players. */
-- activeRegions: number;
-- occupiedRegions: number;
-- /** NPC simulation LOD population. */
-- npcsFull: number;
-- npcsAbstract: number;
-- /**
-- * Live map layers, by stable id. These make "Save is live" checkable from
-- * outside the process: a static added in the editor must show up here on
-- * the next tick, and an identical repeated save must not change the count.
-- */
-- mapStatics: number;
-- mapTerrains: number;
-- mapZones: number;
-- /** Cumulative map-layer body rebuilds; identical saves must not move it. */
-- mapRebuilds: number;
-- private emaAlpha;
-- recordTick(totalMs: number, physicsMs: number): void;
-- snapshot(): Record<string, number>;
-- logSummary(log: Logger): void;
--}
--//# sourceMappingURL=metrics.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/observability/metrics.d.ts.map b/apps/server/dist-types/observability/metrics.d.ts.map
ddeleted file mode 100644
iindex 7525747..0000000
---- a/apps/server/dist-types/observability/metrics.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/observability/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAE9C;;;;GAIG;AACH,qBAAa,aAAa;IACxB,IAAI,SAAI;IACR,6FAA6F;IAC7F,UAAU,SAAI;IACd,cAAc,SAAI;IAClB,SAAS,SAAI;IACb,QAAQ,SAAI;IACZ,QAAQ,SAAI;IACZ,WAAW,SAAI;IACf,aAAa,SAAI;IACjB,QAAQ,SAAI;IACZ,WAAW,SAAI;IACf,aAAa,SAAI;IACjB,YAAY,SAAI;IAChB,gEAAgE;IAChE,WAAW,SAAI;IACf,iBAAiB,SAAI;IACrB,6EAA6E;IAC7E,aAAa,SAAI;IACjB,eAAe,SAAI;IACnB,qCAAqC;IACrC,QAAQ,SAAI;IACZ,YAAY,SAAI;IAChB;;;;OAIG;IACH,UAAU,SAAI;IACd,WAAW,SAAI;IACf,QAAQ,SAAI;IACZ,4EAA4E;IAC5E,WAAW,SAAI;IAEf,OAAO,CAAC,QAAQ,CAAO;IAEvB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKpD,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IA2BlC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAG9B"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/observability/readiness.d.ts b/apps/server/dist-types/observability/readiness.d.ts
ddeleted file mode 100644
iindex fc48c52..0000000
---- a/apps/server/dist-types/observability/readiness.d.ts
++++ /dev/null
@@@ -1,55 +0,0 @@
--import type { IncomingMessage, ServerResponse } from 'node:http';
--/**
-- * GET /api/ready: whether this process can serve the game, in the shape the
-- * other OpenVibe services answer (openvibe-shared/ready): `status`
-- * ready/degraded/not_ready, a `checks` object with one entry per named check,
-- * and the names of the `failed` (required) and `degraded` (optional) ones.
-- * 200 when every required check passes, 503 otherwise; each failed check
-- * carries its reason in `error`.
-- *
-- * Required: `db` (world.db answers a trivial query) and `tick` (the fixed-tick
-- * simulation has advanced recently). /healthz stays liveness only.
-- */
--/**
-- * No completed tick for this long means the simulation is stalled. The loop
-- * runs at `tickRate` (30/s, one tick every ~33 ms) and resyncs itself after
-- * falling more than a second behind, so a healthy server never goes a second
-- * without a tick; 5 s leaves room for a GC pause, a persistence flush or a
-- * live map rebuild, and still flags a frozen world within one deploy poll.
-- */
--export declare const TICK_STALL_MS = 5000;
--export interface ReadinessCheck {
-- status: 'ok' | 'fail';
-- required: boolean;
-- latency_ms: number;
-- checked_at: string;
-- error?: string;
-- detail?: Record<string, unknown>;
--}
--export interface ReadinessBody {
-- ready: boolean;
-- status: 'ready' | 'degraded' | 'not_ready';
-- service: 'games';
-- checked_at: string;
-- failed: string[];
-- degraded: string[];
-- checks: Record<string, ReadinessCheck>;
--}
--export interface ReadinessDeps {
-- /** Runs a trivial query against world.db; throws or returns false when it fails. */
-- pingDb: () => boolean;
-- /** The tick counter and when the last tick completed (epoch ms, 0 = never). */
-- metrics: {
-- readonly tick: number;
-- readonly lastTickAt: number;
-- };
-- /** Players connected now (a non-required `sessions` check; OpenVibe.Host's protected probe reads it). */
-- online?: () => number;
-- tickStallMs?: number;
-- now?: () => number;
--}
--export declare function createReadiness(deps: ReadinessDeps): {
-- check: () => ReadinessBody;
-- handle: (req: IncomingMessage, res: ServerResponse) => boolean;
--};
--//# sourceMappingURL=readiness.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/observability/readiness.d.ts.map b/apps/server/dist-types/observability/readiness.d.ts.map
ddeleted file mode 100644
iindex 7c21e54..0000000
---- a/apps/server/dist-types/observability/readiness.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"readiness.d.ts","sourceRoot":"","sources":["../../src/observability/readiness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE;;;;;;;;;;GAUG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,OAAQ,CAAA;AAElC,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,IAAI,GAAG,MAAM,CAAA;IACrB,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,WAAW,CAAA;IAC1C,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,MAAM,EAAE,MAAM,OAAO,CAAA;IACrB,+EAA+E;IAC/E,OAAO,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/D,yGAAyG;IACzG,MAAM,CAAC,EAAE,MAAM,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AA4BD,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG;IACpD,KAAK,EAAE,MAAM,aAAa,CAAA;IAC1B,MAAM,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAA;CAC/D,CAgEA"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/observability/readiness.test.d.ts b/apps/server/dist-types/observability/readiness.test.d.ts
ddeleted file mode 100644
iindex 69a10c5..0000000
---- a/apps/server/dist-types/observability/readiness.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=readiness.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/observability/readiness.test.d.ts.map b/apps/server/dist-types/observability/readiness.test.d.ts.map
ddeleted file mode 100644
iindex d4319e4..0000000
---- a/apps/server/dist-types/observability/readiness.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"readiness.test.d.ts","sourceRoot":"","sources":["../../src/observability/readiness.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/observability/release.d.ts b/apps/server/dist-types/observability/release.d.ts
ddeleted file mode 100644
iindex b585700..0000000
---- a/apps/server/dist-types/observability/release.d.ts
++++ /dev/null
@@@ -1,41 +0,0 @@
--import type { IncomingMessage, ServerResponse } from 'node:http';
--/**
-- * GET /release.json (ADR-016, roadmap D43): what this deployment is, in the manifest shape the other
-- * OpenVibe services serve through openvibe-shared/release. The release id is RELEASE_SHA when set,
-- * otherwise the checked-out commit read from .git (no child process). Public deployment facts only.
-- */
--export interface ReleaseManifest {
-- service: string;
-- release: string;
-- released_at: string | null;
-- booted_at: string;
-- contracts_version: string | null;
-- packages: Record<string, string>;
-- min_client_release: string | null;
-- mixed_version_window_hours: number;
-- components: Record<string, {
-- kind: string;
-- version: string;
-- }>;
-- schema_generation: null;
-- schema_compatible_from: null;
-- contract_ranges: Record<string, string>;
--}
--/** The commit checked out at `start` or the nearest parent with a .git (HEAD, a loose ref, or packed-refs), or null. */
--export declare function gitCommit(start: string): string | null;
--export declare function buildRelease(root: string, { env, now }?: {
-- env?: NodeJS.ProcessEnv;
-- now?: () => Date;
--}): ReleaseManifest;
--/** Answers GET /release.json (returns true), else false. */
--export declare function releaseHandler(manifest: ReleaseManifest): (req: IncomingMessage, res: ServerResponse) => boolean;
--/** A request from this machine that did not come through the proxy (nginx sets the client-IP headers). */
--export declare function isDirectLoopback(req: IncomingMessage): boolean;
--/**
-- * The metrics snapshot in Prometheus text format (Track O): every finite number becomes a gauge
-- * games_<snake_case key>; nested objects and non-numbers are skipped.
-- */
--export declare function prometheusText(snapshot: Record<string, unknown>): string;
--/** Does this scrape ask for the text format (Prometheus sends text/plain or OpenMetrics in Accept)? */
--export declare function wantsPrometheus(req: IncomingMessage): boolean;
--//# sourceMappingURL=release.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/observability/release.d.ts.map b/apps/server/dist-types/observability/release.d.ts.map
ddeleted file mode 100644
iindex 39ded34..0000000
---- a/apps/server/dist-types/observability/release.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"release.d.ts","sourceRoot":"","sources":["../../src/observability/release.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,0BAA0B,EAAE,MAAM,CAAA;IAClC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC7D,iBAAiB,EAAE,IAAI,CAAA;IACvB,sBAAsB,EAAE,IAAI,CAAA;IAC5B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAED,wHAAwH;AACxH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuBtD;AAUD,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,EAAE,GAAiB,EAAE,GAAsB,EAAE,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,GAChG,eAAe,CAuBjB;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,IAE9C,KAAK,eAAe,EAAE,KAAK,cAAc,KAAG,OAAO,CAM5D;AAED,0GAA0G;AAC1G,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAK9D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQxE;AAED,uGAAuG;AACvG,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAI7D"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/observability/release.test.d.ts b/apps/server/dist-types/observability/release.test.d.ts
ddeleted file mode 100644
iindex b41c7fd..0000000
---- a/apps/server/dist-types/observability/release.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=release.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/observability/release.test.d.ts.map b/apps/server/dist-types/observability/release.test.d.ts.map
ddeleted file mode 100644
iindex 06da06a..0000000
---- a/apps/server/dist-types/observability/release.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"release.test.d.ts","sourceRoot":"","sources":["../../src/observability/release.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/accounts.d.ts b/apps/server/dist-types/platform/accounts.d.ts
ddeleted file mode 100644
iindex b6aba70..0000000
---- a/apps/server/dist-types/platform/accounts.d.ts
++++ /dev/null
@@@ -1,44 +0,0 @@
--/**
-- * Account keys (ADR-0006).
-- *
-- * A signed-in account is keyed by its canonical openvibe.network subject
-- * (`usr_…`, or `gst_…` for a Network guest) taken from the token's
-- * `subject_id` claim. Before subjects existed Games keyed it by
-- * `ovn:<network user id>`; those characters are adopted into the subject key
-- * the first time the account signs in (or by scripts/migrateIdentity.ts), and
-- * the mapping is kept in `identity_legacy_map`.
-- *
-- * Local guests keep their browser token (ADR-0004). A guest token may use
-- * letters, digits, `_` and `-` only (the client mints alphanumerics), and
-- * may not start with a subject prefix, so it can never equal a subject or a
-- * legacy key: a guest presenting `usr_…` or `ovn:…` as its token cannot open
-- * someone else's characters.
-- */
--import type { IdentityRepository, PlayerRepository } from '@openvibe/persistence';
--import type { NetworkUser } from '../net/networkAuth.js';
--/** Prefix of pre-subject account keys: `ovn:<openvibe.network user id>`. */
--export declare const LEGACY_NETWORK_PREFIX = "ovn:";
--export declare function isGuestToken(token: unknown): token is string;
--export declare function legacyNetworkKey(networkUserId: string): string;
--export interface AccountResolution {
-- /** The players.token value the account's characters live under. */
-- key: string;
-- /** Canonical subject, or null for a token older than subjects. */
-- subjectId: string | null;
-- /** Characters moved from the legacy key by this call. */
-- adopted: number;
--}
--export interface AccountStores {
-- identity: IdentityRepository;
-- players: PlayerRepository;
--}
--/**
-- * The account key for a signed-in Network user, adopting any characters
-- * still under its legacy key. Idempotent.
-- */
--export declare function accountForNetworkUser(stores: AccountStores, user: NetworkUser, now: number, onConflict?: (info: {
-- legacyKey: string;
-- subjectId: string;
-- error: string;
--}) => void): AccountResolution;
--//# sourceMappingURL=accounts.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/accounts.d.ts.map b/apps/server/dist-types/platform/accounts.d.ts.map
ddeleted file mode 100644
iindex b486c88..0000000
---- a/apps/server/dist-types/platform/accounts.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"accounts.d.ts","sourceRoot":"","sources":["../../src/platform/accounts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExD,4EAA4E;AAC5E,eAAO,MAAM,qBAAqB,SAAS,CAAA;AAO3C,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE5D;AAED,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,MAAM,WAAW,iBAAiB;IAChC,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,OAAO,EAAE,gBAAgB,CAAA;CAC1B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,MAAM,EACX,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,GACnF,iBAAiB,CAmBnB"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/accounts.test.d.ts b/apps/server/dist-types/platform/accounts.test.d.ts
ddeleted file mode 100644
iindex 020ae4c..0000000
---- a/apps/server/dist-types/platform/accounts.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=accounts.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/accounts.test.d.ts.map b/apps/server/dist-types/platform/accounts.test.d.ts.map
ddeleted file mode 100644
iindex f897ad2..0000000
---- a/apps/server/dist-types/platform/accounts.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"accounts.test.d.ts","sourceRoot":"","sources":["../../src/platform/accounts.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/gameEvents.d.ts b/apps/server/dist-types/platform/gameEvents.d.ts
ddeleted file mode 100644
iindex c6ab5ce..0000000
---- a/apps/server/dist-types/platform/gameEvents.d.ts
++++ /dev/null
@@@ -1,126 +0,0 @@
--/**
-- * Games → OpenVibe.Events: public lifecycle and progression as durable
-- * events (roadmap Wave 12, ADR-004).
-- *
-- * games.player.joined a character entered the world
-- * games.player.left it left (its state was saved in the same transaction)
-- * games.skill.leveled a persisted skill level went up (one event per skill per save)
-- * games.blueprint.unlocked a persisted blueprint recipe unlock
-- * games.world.saved the authoritative world was written (checkpoint / shutdown)
-- * games.mod.* mod install, enable, disable, grant changes and revoke
-- * games.moderation.action staff took down or put back a mod someone else published
-- * (common.moderation-action@1, ADR-022), for Network's moderation
-- * audit log
-- *
-- * Scraplandia has no achievement system, so there is no games.achievement.*
-- * event: skill levels and blueprint unlocks are the real progression
-- * milestones the game keeps.
-- *
-- * Every event is written to Games' own `event_outbox` table inside the SAME
-- * SQLite transaction as the state it describes (progression events are
-- * derived from what the save actually wrote), and a relay publishes them
-- * with the `games` service token (audience openvibe.events, capability
-- * events.event.publish). Events or Network being down never blocks a tick:
-- * rows wait and are retried.
-- */
--import type { EventInput, Outbox } from 'openvibe-sdk/events';
--import type { SubjectRef } from 'openvibe-sdk/core';
--export declare const EVENT_SOURCE = "games";
--/** A character as events describe it. */
--export interface EventPlayer {
-- playerId: string;
-- slot: number;
-- name: string;
-- /** Canonical subject for signed-in accounts; null for local guests. */
-- subjectId: string | null;
--}
--/** The persisted progression a save compares against. */
--export interface ProgressSnapshot {
-- /** Level per skill id. */
-- levels: Record<string, number>;
-- /** Unlocked blueprint recipe ids. */
-- unlocks: string[];
--}
--export declare function actorFor(subjectId: string | null): SubjectRef;
--export declare function playerJoinedEvent(p: EventPlayer, opts: {
-- world: string;
-- restored: boolean;
--}): EventInput;
--export declare function playerLeftEvent(p: EventPlayer, opts: {
-- world: string;
-- sessionSeconds: number;
--}): EventInput;
--/** Progression milestones between two persisted snapshots (never downgrades). */
--export declare function progressEvents(p: EventPlayer, before: ProgressSnapshot, after: ProgressSnapshot, world: string): EventInput[];
--export declare function worldSavedEvent(world: string, save: {
-- reason: 'checkpoint' | 'shutdown';
-- entities: number;
-- players: number;
-- sinceMs: number;
--}): EventInput;
--export type ModLifecycle = 'installed' | 'enabled' | 'disabled' | 'revoked' | 'grants_changed';
--export declare function modEvent(kind: ModLifecycle, mod: {
-- id: string;
-- name: string;
-- version: string;
-- target: string;
-- trustTier: string;
--}, actor: SubjectRef, detail: Record<string, unknown>): EventInput;
--/**
-- * games.moderation.action: one staff action on someone else's content. `actor` is the staff
-- * member (a service when no person is known); the payload never carries the content itself.
-- */
--export declare function moderationEvent(action: string, target: {
-- type: string;
-- id: string;
-- ownerSubject: string | null;
--}, actor: SubjectRef, opts?: {
-- reason?: string | null;
-- details?: Record<string, unknown>;
--}): EventInput;
--/** Where events go. `enqueue` must run inside the transaction making the change. */
--export interface EventSink {
-- readonly enabled: boolean;
-- enqueue(event: EventInput): void;
--}
--export declare const NO_EVENTS: EventSink;
--/** An EventSink over the SDK outbox; wakes the relay right after the write. */
--export declare function outboxSink(outbox: Outbox): EventSink;
--/**
-- * Per-session bookkeeping the game server drives: the progression baseline
-- * each save is compared with, join times, and the world-save accumulator.
-- * All `record*` methods run inside the caller's store transaction and return
-- * a callback to run once it has committed.
-- */
--export declare class GameEventRecorder {
-- private readonly sink;
-- private readonly world;
-- private readonly checkpointEveryMs;
-- private readonly now;
-- private readonly tracked;
-- private sinceMs;
-- private pendingEntities;
-- private pendingPlayers;
-- private lastSavedEventMs;
-- constructor(sink: EventSink, world: string, checkpointEveryMs: number, now?: () => number);
-- get enabled(): boolean;
-- /** A character joined; `progress` is what the database holds for it. */
-- recordJoin(player: EventPlayer, progress: ProgressSnapshot, restored: boolean): void;
-- /**
-- * Players were written: emits the progression they newly persisted.
-- * Returns the commit callback that advances their baselines.
-- */
-- recordPlayersSaved(saved: {
-- player: EventPlayer;
-- progress: ProgressSnapshot;
-- }[]): () => void;
-- /** The character left; its final save is part of the same transaction. */
-- recordLeave(player: EventPlayer): () => void;
-- /**
-- * A flush wrote `entities` world rows and `players` player rows. Emits a
-- * `games.world.saved` checkpoint at most once per interval (and always on
-- * shutdown) with the totals since the previous one.
-- */
-- recordWorldSaved(entities: number, players: number, reason: 'checkpoint' | 'shutdown'): () => void;
--}
--//# sourceMappingURL=gameEvents.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/gameEvents.d.ts.map b/apps/server/dist-types/platform/gameEvents.d.ts.map
ddeleted file mode 100644
iindex 4ea2fb0..0000000
---- a/apps/server/dist-types/platform/gameEvents.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"gameEvents.d.ts","sourceRoot":"","sources":["../../src/platform/gameEvents.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/gameEvents.test.d.ts b/apps/server/dist-types/platform/gameEvents.test.d.ts
ddeleted file mode 100644
iindex ec18d61..0000000
---- a/apps/server/dist-types/platform/gameEvents.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=gameEvents.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/gameEvents.test.d.ts.map b/apps/server/dist-types/platform/gameEvents.test.d.ts.map
ddeleted file mode 100644
iindex 9176827..0000000
---- a/apps/server/dist-types/platform/gameEvents.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"gameEvents.test.d.ts","sourceRoot":"","sources":["../../src/platform/gameEvents.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/identityMigration.d.ts b/apps/server/dist-types/platform/identityMigration.d.ts
ddeleted file mode 100644
iindex e4bd6e6..0000000
---- a/apps/server/dist-types/platform/identityMigration.d.ts
++++ /dev/null
@@@ -1,31 +0,0 @@
--/**
-- * Batch adoption of legacy accounts (ADR-0006): every account still keyed by
-- * `ovn:<network user id>` is resolved to its canonical subject through the
-- * Network identity service (capability identity.subject.resolve, audience
-- * openvibe.network, the `games` service token) and moved over.
-- *
-- * Sign-in adopts an account on its own; this pass covers the accounts that
-- * have not signed in since. Idempotent: adopted keys own no characters any
-- * more, so a second run finds nothing to do.
-- */
--import type { PersistenceStore } from '@openvibe/persistence';
--import type { IdentityClient } from 'openvibe-sdk/identity';
--export interface MigrationReport {
-- /** Legacy account keys that still owned characters. */
-- scanned: number;
-- /** Keys the Network resolved to a subject. */
-- resolved: number;
-- /** Characters moved to their subject key. */
-- moved: number;
-- /** Characters left under the legacy key because the subject already used that slot. */
-- conflicts: number;
-- /** Keys the Network did not know (left untouched). */
-- unresolved: string[];
-- dryRun: boolean;
--}
--export declare function migrateLegacyAccounts(store: PersistenceStore, identity: Pick<IdentityClient, 'resolveBatch'>, opts?: {
-- dryRun?: boolean;
-- batchSize?: number;
-- now?: () => number;
--}): Promise<MigrationReport>;
--//# sourceMappingURL=identityMigration.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/identityMigration.d.ts.map b/apps/server/dist-types/platform/identityMigration.d.ts.map
ddeleted file mode 100644
iindex 2df8a3b..0000000
---- a/apps/server/dist-types/platform/identityMigration.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"identityMigration.d.ts","sourceRoot":"","sources":["../../src/platform/identityMigration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAI3D,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAA;IACf,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAA;IAChB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,gBAAgB,EACvB,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,cAAc,CAAC,EAC9C,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAO,GACtE,OAAO,CAAC,eAAe,CAAC,CAmC1B"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/identityMigration.test.d.ts b/apps/server/dist-types/platform/identityMigration.test.d.ts
ddeleted file mode 100644
iindex 7b70afa..0000000
---- a/apps/server/dist-types/platform/identityMigration.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=identityMigration.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/identityMigration.test.d.ts.map b/apps/server/dist-types/platform/identityMigration.test.d.ts.map
ddeleted file mode 100644
iindex 3925e25..0000000
---- a/apps/server/dist-types/platform/identityMigration.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"identityMigration.test.d.ts","sourceRoot":"","sources":["../../src/platform/identityMigration.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/liveLegacyImport.d.ts b/apps/server/dist-types/platform/liveLegacyImport.d.ts
ddeleted file mode 100644
iindex b094ba4..0000000
---- a/apps/server/dist-types/platform/liveLegacyImport.d.ts
++++ /dev/null
@@@ -1,156 +0,0 @@
--import type Database from 'better-sqlite3';
--import type { IdentityClient } from 'openvibe-sdk/identity';
--/** A Live user's canonical subject: `usr_<ULID>`. Guest subjects never own Live rows. */
--export declare const USER_SUBJECT: RegExp;
--/** The production Live data directory; the importer reads snapshots only. */
--export declare const LIVE_PRODUCTION_DIR = "/opt/openvibe.live/";
--export interface LegacyTableSpec {
-- table: string;
-- decision: 'import' | 'exclude';
-- /** Column naming the owning Live user (for exclude tables: evidence of play only). */
-- userColumn?: string;
-- /** Primary-key columns of the source row: the idempotency key (import tables only). */
-- keyColumns?: string[];
-- /** Why the table is excluded, or what the archived rows are. */
-- reason: string;
-- /** Rows of an import table that are excluded one by one, and why. */
-- excludeRows?: {
-- reason: string;
-- test: (row: Row, ownsOtherRows: boolean) => boolean;
-- };
--}
--type Row = Record<string, unknown>;
--/**
-- * A `game_players` row nobody played: Live's profile lookup
-- * (`game.getPlayer`, still called by Live's chat profile routes) inserts a
-- * default row for every profile it shows. Such a row was never acted on
-- * (`last_action = created_at`), has no XP, counters, equipment, sleeping bag
-- * or cosmetics, and its user owns no row in any other legacy game table.
-- */
--export declare function isPlaceholderPlayer(row: Row, ownsOtherRows: boolean): boolean;
--/** Every legacy game and canvas table in Live, with its decision (docs/legacy-import.md). */
--export declare const LEGACY_TABLES: readonly LegacyTableSpec[];
--export type HoldReason = 'no-subject' | 'subject-unconfirmed' | 'subject-conflict' | 'no-owner' | 'differs-from-earlier-import';
--export declare const HOLD_REASONS: Record<HoldReason, string>;
--export interface HeldRow {
-- table: string;
-- key: string;
-- liveUserId: number | null;
-- reason: HoldReason;
--}
--export interface TableReport {
-- table: string;
-- decision: 'import' | 'exclude';
-- /** The table does not exist in the snapshot (counts are 0). */
-- missing: boolean;
-- read: number;
-- /** Rows in world.db after the run (new plus already imported). */
-- imported: number;
-- /** Rows this run writes (0 on a re-run). */
-- written: number;
-- held: number;
-- excluded: number;
-- balanced: boolean;
--}
--export interface ImportReport {
-- dryRun: boolean;
-- subjectSource: string;
-- tables: TableReport[];
-- totals: Omit<TableReport, 'table' | 'decision' | 'missing'>;
-- /** Live users owning importable rows, and how their subjects resolved. */
-- users: {
-- total: number;
-- mapped: number;
-- held: Record<string, number>;
-- };
-- held: HeldRow[];
-- /** Rows of import tables excluded one by one, per `table: reason`. */
-- excludedRows: Record<string, number>;
-- /** Legacy-looking tables in the snapshot that no decision covers. */
-- unlisted: {
-- table: string;
-- rows: number;
-- }[];
-- /** Every table balances and nothing is unlisted. */
-- reconciled: boolean;
--}
--/** Resolves Live user ids to subjects at the Network; null = the Network does not know the user. */
--export type NetworkSubjectLookup = (liveUserIds: number[]) => Promise<Map<number, string | null>>;
--/** Lookup through the Network identity service: `(system 'live', type 'user', id)` -> subject. */
--export declare function networkSubjectLookup(identity: Pick<IdentityClient, 'resolveBatch'>): NetworkSubjectLookup;
--/** Lookup from a map exported from the Network: `{ "<live user id>": "usr_…" }`. */
--export declare function subjectMapLookup(map: unknown): NetworkSubjectLookup;
--/** A row about to be written. */
--export interface PlannedRow {
-- table: string;
-- key: string;
-- liveUserId: number;
-- subjectId: string;
-- payload: string;
--}
--export interface ImportPlan {
-- report: ImportReport;
-- rows: PlannedRow[];
--}
--/** Deterministic JSON of a source row (sorted keys) so re-runs compare byte for byte. */
--export declare function canonicalRow(row: Row): string;
--export declare function rowKey(row: Row, columns: readonly string[]): string;
--/**
-- * Refuses the production Live database: the importer reads a `.backup`
-- * snapshot, never the file Live has open.
-- */
--export declare function assertSnapshotPath(path: string, productionDir?: string): void;
--export interface RunOptions {
-- livePath?: string;
-- apply: boolean;
-- backupPath?: string;
-- subjects?: string;
-- /** Whether `backupPath` already exists. */
-- backupExists: boolean;
--}
--/**
-- * Why a run must not start, or null. `--apply` needs a fresh `--backup`
-- * path (a stale file is not a backup of now) and a Network subject source.
-- */
--export declare function refusalFor(opts: RunOptions): string | null;
--/** Subjects Live itself recorded for its users (linked_accounts, service 'network'). */
--export declare function liveLinkedSubjects(live: Database.Database): Map<number, string[]>;
--/**
-- * The subject a Live user's rows go to, or why they are held. With a Network
-- * answer the Network is the authority and Live's own record must agree; with
-- * none (an informational dry run) Live's single recorded subject is used.
-- */
--export declare function decideSubject(liveUserId: number, linked: ReadonlyMap<number, string[]>, network: ReadonlyMap<number, string | null> | null): {
-- subjectId: string;
--} | {
-- hold: HoldReason;
--};
--export interface PlanOptions {
-- /** The Network's answer for each Live user id, or null to rely on Live's own record only. */
-- network: NetworkSubjectLookup | null;
-- subjectSource: string;
-- dryRun: boolean;
--}
--/**
-- * Reads the Live snapshot and decides every row. `world` may be null (a
-- * fresh world with nothing imported yet) and is only read here.
-- */
--export declare function planLegacyImport(live: Database.Database, world: Database.Database | null, opts: PlanOptions): Promise<ImportPlan>;
--/**
-- * Writes the planned rows in one transaction. Existing keys are never
-- * touched (ON CONFLICT DO NOTHING); the write count must equal the plan or
-- * the transaction rolls back.
-- */
--export declare function applyLegacyImport(world: Database.Database, plan: ImportPlan, now: number): number;
--/** Archived row counts per source table (post-apply verification). */
--export declare function archivedCounts(world: Database.Database): Map<string, number>;
--/**
-- * Snapshot of world.db to `path` taken before anything is written, then
-- * verified: PRAGMA integrity_check must answer `ok` and the snapshot must
-- * hold the same players and archive rows as the source.
-- */
--export declare function backupAndVerify(world: Database.Database, path: string, open: (path: string) => Database.Database): Promise<void>;
--/** The reconciliation report as text. */
--export declare function formatReport(report: ImportReport): string;
--export {};
--//# sourceMappingURL=liveLegacyImport.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/liveLegacyImport.d.ts.map b/apps/server/dist-types/platform/liveLegacyImport.d.ts.map
ddeleted file mode 100644
iindex 30c7d4a..0000000
---- a/apps/server/dist-types/platform/liveLegacyImport.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"liveLegacyImport.d.ts","sourceRoot":"","sources":["../../src/platform/liveLegacyImport.ts"],"names":[],"mappings":"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"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/liveLegacyImport.test.d.ts b/apps/server/dist-types/platform/liveLegacyImport.test.d.ts
ddeleted file mode 100644
iindex 1837439..0000000
---- a/apps/server/dist-types/platform/liveLegacyImport.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=liveLegacyImport.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/liveLegacyImport.test.d.ts.map b/apps/server/dist-types/platform/liveLegacyImport.test.d.ts.map
ddeleted file mode 100644
iindex a0fa090..0000000
---- a/apps/server/dist-types/platform/liveLegacyImport.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"liveLegacyImport.test.d.ts","sourceRoot":"","sources":["../../src/platform/liveLegacyImport.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/mediaMirror.d.ts b/apps/server/dist-types/platform/mediaMirror.d.ts
ddeleted file mode 100644
iindex 00c8c06..0000000
---- a/apps/server/dist-types/platform/mediaMirror.d.ts
++++ /dev/null
@@@ -1,41 +0,0 @@
--import type { PersistenceStore } from '@openvibe/persistence';
--import type { Logger } from '@openvibe/shared';
--import type { OpenVibeClient } from 'openvibe-sdk/core';
--export interface MediaMirrorOptions {
-- client: OpenVibeClient;
-- store: PersistenceStore;
-- namespace: string;
-- assetsDir: string;
-- log: Logger;
-- now?: () => number;
-- intervalMs?: number;
--}
--export declare class MediaMirror {
-- private readonly opts;
-- private timer;
-- private running;
-- private stopped;
-- private readonly now;
-- constructor(opts: MediaMirrorOptions);
-- /** Queues one stored asset (idempotent) and wakes the worker. */
-- enqueue(asset: {
-- hash: string;
-- url: string;
-- bytes: number;
-- mime: string;
-- }): void;
-- /** Queues every content-addressed asset already on disk (a no-op for known ones). */
-- backfill(): Promise<number>;
-- start(): void;
-- /** No new passes; resolves when the pass in progress (if any) has finished. */
-- stop(): Promise<void>;
-- kick(): void;
-- /** Mirrors every due row once. Concurrent callers share the pass. */
-- runOnce(): Promise<void>;
-- private schedule;
-- private pass;
-- private base;
-- private mirror;
-- private fail;
--}
--//# sourceMappingURL=mediaMirror.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/mediaMirror.d.ts.map b/apps/server/dist-types/platform/mediaMirror.d.ts.map
ddeleted file mode 100644
iindex 17d2c69..0000000
---- a/apps/server/dist-types/platform/mediaMirror.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mediaMirror.d.ts","sourceRoot":"","sources":["../../src/platform/mediaMirror.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAkB,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AA2BvD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,cAAc,CAAA;IACtB,KAAK,EAAE,gBAAgB,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,qBAAa,WAAW;IAMV,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;gBAEL,IAAI,EAAE,kBAAkB;IAIrD,iEAAiE;IACjE,OAAO,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAUhF,qFAAqF;IAC/E,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAqBjC,KAAK,IAAI,IAAI;IAKb,+EAA+E;IAC/E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAOrB,IAAI,IAAI,IAAI;IAIZ,qEAAqE;IACrE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IASxB,OAAO,CAAC,QAAQ;YAUF,IAAI;IAUlB,OAAO,CAAC,IAAI;YAIE,MAAM;IA4DpB,OAAO,CAAC,IAAI;CAoBb"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/mediaMirror.test.d.ts b/apps/server/dist-types/platform/mediaMirror.test.d.ts
ddeleted file mode 100644
iindex 928998d..0000000
---- a/apps/server/dist-types/platform/mediaMirror.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=mediaMirror.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/mediaMirror.test.d.ts.map b/apps/server/dist-types/platform/mediaMirror.test.d.ts.map
ddeleted file mode 100644
iindex 69d813d..0000000
---- a/apps/server/dist-types/platform/mediaMirror.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"mediaMirror.test.d.ts","sourceRoot":"","sources":["../../src/platform/mediaMirror.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/progressSummary.d.ts b/apps/server/dist-types/platform/progressSummary.d.ts
ddeleted file mode 100644
iindex 096b60b..0000000
---- a/apps/server/dist-types/platform/progressSummary.d.ts
++++ /dev/null
@@@ -1,36 +0,0 @@
--import type { OpenVibeClient } from 'openvibe-sdk/core';
--export declare const NAMESPACE = "games.progress.summary";
--export interface ProgressSummary {
-- level?: number;
-- achievements?: number;
-- playtime_hours?: number;
-- last_world?: string;
--}
--export interface SessionOutcome {
-- levels: Record<string, number>;
-- sessionSeconds: number;
-- world: string;
--}
--/** The next record from the one stored and a finished session. */
--export declare function mergeSummary(prev: ProgressSummary | undefined, s: SessionOutcome): ProgressSummary;
--interface Log {
-- warn(msg: string, fields?: Record<string, unknown>): void;
--}
--export declare class ProgressSummaryWriter {
-- private readonly log;
-- private readonly now;
-- private readonly joined;
-- private readonly modules;
-- private inflight;
-- private readonly writes;
-- constructor(client: OpenVibeClient, log: Log, now?: () => number);
-- /** A character entered the world (its session starts counting). */
-- playerJoined(playerId: string): void;
-- /** A character left: fold the session into the person's summary. Resolves when written (or given up). */
-- playerLeft(playerId: string, subjectId: string | null, levels: Record<string, number>, world: string): Promise<void>;
-- pending(): number;
-- /** Graceful stop: resolves when every write in flight has finished (or given up). */
-- settle(): Promise<void>;
--}
--export {};
--//# sourceMappingURL=progressSummary.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/progressSummary.d.ts.map b/apps/server/dist-types/platform/progressSummary.d.ts.map
ddeleted file mode 100644
iindex 145b873..0000000
---- a/apps/server/dist-types/platform/progressSummary.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"progressSummary.d.ts","sourceRoot":"","sources":["../../src/platform/progressSummary.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAEvD,eAAO,MAAM,SAAS,2BAA2B,CAAA;AAGjD,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,cAAc,EAAE,MAAM,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,kEAAkE;AAClE,wBAAgB,YAAY,CAC1B,IAAI,EAAE,eAAe,GAAG,SAAS,EACjC,CAAC,EAAE,cAAc,GAChB,eAAe,CAUjB;AAED,UAAU,GAAG;IACX,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAC1D;AAED,qBAAa,qBAAqB;IAQ9B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;IACnD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsD;IAC9E,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;gBAGhD,MAAM,EAAE,cAAc,EACL,GAAG,EAAE,GAAG,EACR,GAAG,GAAE,MAAM,MAAiB;IAK/C,mEAAmE;IACnE,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIpC,yGAAyG;IACzG,UAAU,CACR,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC;IA6BhB,OAAO,IAAI,MAAM;IAIjB,qFAAqF;IACrF,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAGxB"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/progressSummary.test.d.ts b/apps/server/dist-types/platform/progressSummary.test.d.ts
ddeleted file mode 100644
iindex dd6bdf8..0000000
---- a/apps/server/dist-types/platform/progressSummary.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=progressSummary.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/progressSummary.test.d.ts.map b/apps/server/dist-types/platform/progressSummary.test.d.ts.map
ddeleted file mode 100644
iindex ee0c2d1..0000000
---- a/apps/server/dist-types/platform/progressSummary.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"progressSummary.test.d.ts","sourceRoot":"","sources":["../../src/platform/progressSummary.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/proposals.test.d.ts b/apps/server/dist-types/platform/proposals.test.d.ts
ddeleted file mode 100644
iindex 2cfa218..0000000
---- a/apps/server/dist-types/platform/proposals.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=proposals.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/proposals.test.d.ts.map b/apps/server/dist-types/platform/proposals.test.d.ts.map
ddeleted file mode 100644
iindex 52cae8a..0000000
---- a/apps/server/dist-types/platform/proposals.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"proposals.test.d.ts","sourceRoot":"","sources":["../../src/platform/proposals.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/revocationEvents.d.ts b/apps/server/dist-types/platform/revocationEvents.d.ts
ddeleted file mode 100644
iindex 76d0b35..0000000
---- a/apps/server/dist-types/platform/revocationEvents.d.ts
++++ /dev/null
@@@ -1,59 +0,0 @@
--/**
-- * Sign-out everywhere reaches Games (roadmap WS-B task 4; Contracts 0.39.0
-- * network.user.token_valid_after).
-- *
-- * POST /internal/events is the endpoint of Games' Events subscription to that topic, signed with
-- * GAMES_EVENTS_SECRET (signature v2 only) and loopback only (a request carrying a forwarding header
-- * came through nginx and is refused). A delivery moves the person's cutoff in the SDK's revocation
-- * store (only ever forward; anything not from Network is ignored), then `onRevoked` closes the game
-- * sessions they opened with an older sign-in. New sign-ins already fail: Games checks every token with
-- * the Network's /api/auth/me, which applies the same cutoff.
-- */
--import type { IncomingMessage, ServerResponse } from 'node:http';
--import type { Logger } from '@openvibe/shared';
--interface RevocationStore {
-- apply(event: unknown): string;
-- isRevoked(claims: {
-- iat?: number;
-- subject_id?: string;
-- } | null | undefined): boolean;
-- cutoffFor(subject: string): number;
--}
--export declare const TOPIC = "network.user.token_valid_after";
--export interface RevocationEvents {
-- /** Answers POST /internal/events (returns true when the request was ours). */
-- handle(req: IncomingMessage, res: ServerResponse): boolean;
-- store: RevocationStore;
-- stats: {
-- received: number;
-- revoked: number;
-- closed: number;
-- refused: number;
-- ignored: number;
-- };
--}
--export declare function createRevocationEvents(opts: {
-- db: unknown;
-- secrets: string[];
-- onRevoked: (subjectId: string, validAfterMs: number) => number;
-- log: Logger;
--}): RevocationEvents;
--/**
-- * Create Games' subscription to TOPIC at Events if it is missing (idempotent: an existing one, even
-- * disabled by an operator, is left as it is). Needs the grant games events.subscription.manage.
-- */
--export declare function ensureRevocationSubscription(opts: {
-- eventsUrl: string;
-- endpoint: string;
-- secret: string;
-- tokens: {
-- getToken(ctx?: {
-- audience?: string;
-- scope?: string;
-- }): Promise<string>;
-- };
-- fetchImpl?: typeof fetch;
-- log: Logger;
--}): Promise<'exists' | 'created'>;
--export {};
--//# sourceMappingURL=revocationEvents.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/revocationEvents.d.ts.map b/apps/server/dist-types/platform/revocationEvents.d.ts.map
ddeleted file mode 100644
iindex 0448102..0000000
---- a/apps/server/dist-types/platform/revocationEvents.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"revocationEvents.d.ts","sourceRoot":"","sources":["../../src/platform/revocationEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAI9C,UAAU,eAAe;IACvB,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAA;IAC7B,SAAS,CAAC,MAAM,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAA;IACpF,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;CACnC;AAaD,eAAO,MAAM,KAAK,mCAAmC,CAAA;AAIrD,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,MAAM,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAA;IAC1D,KAAK,EAAE,eAAe,CAAA;IACtB,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAC/F;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9D,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,gBAAgB,CA0DnB;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CAAC,IAAI,EAAE;IACvD,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;IAClF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAkBhC"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/revocationEvents.test.d.ts b/apps/server/dist-types/platform/revocationEvents.test.d.ts
ddeleted file mode 100644
iindex 226b2df..0000000
---- a/apps/server/dist-types/platform/revocationEvents.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=revocationEvents.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/revocationEvents.test.d.ts.map b/apps/server/dist-types/platform/revocationEvents.test.d.ts.map
ddeleted file mode 100644
iindex f9e4b5f..0000000
---- a/apps/server/dist-types/platform/revocationEvents.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"revocationEvents.test.d.ts","sourceRoot":"","sources":["../../src/platform/revocationEvents.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/serviceClient.d.ts b/apps/server/dist-types/platform/serviceClient.d.ts
ddeleted file mode 100644
iindex eb5bc76..0000000
---- a/apps/server/dist-types/platform/serviceClient.d.ts
++++ /dev/null
@@@ -1,16 +0,0 @@
--import { type FetchLike, type OpenVibeClient } from 'openvibe-sdk/core';
--import type { PlatformConfig } from '../config.js';
--export interface PlatformClient {
-- client: OpenVibeClient;
-- clientId: string;
-- /** The `games` principal's tokens, for calls the SDK client has no method for (Events subscriptions). */
-- tokens: {
-- getToken(ctx?: {
-- audience?: string;
-- scope?: string;
-- }): Promise<string>;
-- };
--}
--/** Null when no client secret is configured: Games then makes no service calls. */
--export declare function createPlatformClient(cfg: PlatformConfig, fetchImpl?: FetchLike): PlatformClient | null;
--//# sourceMappingURL=serviceClient.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/serviceClient.d.ts.map b/apps/server/dist-types/platform/serviceClient.d.ts.map
ddeleted file mode 100644
iindex 4c65a55..0000000
---- a/apps/server/dist-types/platform/serviceClient.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"serviceClient.d.ts","sourceRoot":"","sources":["../../src/platform/serviceClient.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,yGAAyG;IACzG,MAAM,EAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;KAAE,CAAA;CACnF;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,cAAc,EACnB,SAAS,CAAC,EAAE,SAAS,GACpB,cAAc,GAAG,IAAI,CAqBvB"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/staffAuth.d.ts b/apps/server/dist-types/platform/staffAuth.d.ts
ddeleted file mode 100644
iindex e4233d3..0000000
---- a/apps/server/dist-types/platform/staffAuth.d.ts
++++ /dev/null
@@@ -1,16 +0,0 @@
--import type { IncomingMessage } from 'node:http';
--import { type UserTokenClaims } from 'openvibe-sdk/auth';
--import type { ModActor } from '../mods/registry.js';
--export declare const CAP_MOD_MANAGE = "games.mod.manage";
--export declare const GAMES_AUDIENCE = "openvibe.games";
--export interface StaffAuthOptions {
-- /** openvibe.network /api/auth/me, or null in local development. */
-- networkAuthUrl: string | null;
-- /** Network base for the JWKS (`/api/.well-known/jwks`). */
-- networkUrl: string;
-- editorKey: string | null;
-- /** Test seam: verifies a principal token. */
-- verifyPrincipal?: (token: string) => Promise<UserTokenClaims>;
--}
--export declare function createStaffAuthorizer(opts: StaffAuthOptions): (req: IncomingMessage) => Promise<ModActor | null>;
--//# sourceMappingURL=staffAuth.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/platform/staffAuth.d.ts.map b/apps/server/dist-types/platform/staffAuth.d.ts.map
ddeleted file mode 100644
iindex 76ed44c..0000000
---- a/apps/server/dist-types/platform/staffAuth.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"staffAuth.d.ts","sourceRoot":"","sources":["../../src/platform/staffAuth.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAInD,eAAO,MAAM,cAAc,qBAAqB,CAAA;AAChD,eAAO,MAAM,cAAc,mBAAmB,CAAA;AAO9C,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,6CAA6C;IAC7C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;CAC9D;AA0BD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,gBAAgB,GACrB,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA4CpD"}
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/shutdown.test.d.ts b/apps/server/dist-types/shutdown.test.d.ts
ddeleted file mode 100644
iindex 740d8bb..0000000
---- a/apps/server/dist-types/shutdown.test.d.ts
++++ /dev/null
@@@ -1,2 +0,0 @@
--export {};
--//# sourceMappingURL=shutdown.test.d.ts.map
\\ No newline at end of file
ddiff --git a/apps/server/dist-types/shutdown.test.d.ts.map b/apps/server/dist-types/shutdown.test.d.ts.map
ddeleted file mode 100644
iindex 6c82766..0000000
---- a/apps/server/dist-types/shutdown.test.d.ts.map
++++ /dev/null
@@@ -1 +0,0 @@
--{"version":3,"file":"shutdown.test.d.ts","sourceRoot":"","sources":["../src/shutdown.test.ts"],"names":[],"mappings":""}
\\ No newline at end of file
ddiff --git a/apps/server/scripts/importLiveLegacy.ts b/apps/server/scripts/importLiveLegacy.ts
ddeleted file mode 100644
iindex cf86d74..0000000
---- a/apps/server/scripts/importLiveLegacy.ts
++++ /dev/null
@@@ -1,171 +0,0 @@
--/**
-- * Imports OpenVibe.Live's legacy HoboQuest rows into world.db
-- * (`legacy_live_rows`) and reconciles every legacy game and canvas table:
-- * read = imported + held + excluded. See docs/legacy-import.md.
-- *
-- * # dry run (default): prints the reconciliation, writes nothing
-- * DB_PATH=/opt/openvibe.games/data/world.db \
-- * OV_OAUTH_CLIENT_SECRET=… OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000 \
-- * node --import tsx apps/server/scripts/importLiveLegacy.ts \
-- * --live /path/to/live-snapshot.db --subjects network
-- *
-- * # apply: snapshot world.db to --backup, verify it, then import in one transaction
-- * … importLiveLegacy.ts --live … --subjects network --apply --backup /path/world-before.db
-- *
-- * Options:
-- * --live <path> a `.backup` snapshot of Live's live.db (never the production file)
-- * --world <path> world.db (default DB_PATH, else data/world.db)
-- * --subjects network resolve Live user ids at the Network identity service
-- * (games principal, grant identity.subject.resolve)
-- * --subjects <file> or a JSON object { "<live user id>": "usr_…" } exported from it
-- * (omitted: Live's own linked_accounts record only; dry run only)
-- * --apply write; requires --backup and --subjects
-- * --backup <path> where world.db is copied first (must not exist); verified with integrity_check
-- * --json print the report as JSON (includes every held row)
-- *
-- * Exit: 0 reconciled, 1 error, 2 refused (usage), 3 not reconciled.
-- * Idempotent: a re-run writes nothing (written = 0) and reports the same counts.
-- */
--import { existsSync, readFileSync } from 'node:fs'
--import Database from 'better-sqlite3'
--import { openSqliteStore } from '@openvibe/persistence/sqlite'
--import { createIdentityClient } from 'openvibe-sdk/identity'
--import { loadConfig } from '../src/config.js'
--import {
-- applyLegacyImport,
-- archivedCounts,
-- assertSnapshotPath,
-- backupAndVerify,
-- formatReport,
-- networkSubjectLookup,
-- planLegacyImport,
-- refusalFor,
-- subjectMapLookup,
-- type NetworkSubjectLookup,
--} from '../src/platform/liveLegacyImport.js'
--import { createPlatformClient } from '../src/platform/serviceClient.js'
--
--class Refusal extends Error {}
--
--function arg(name: string): string | undefined {
-- const i = process.argv.indexOf(name)
-- if (i < 0) return undefined
-- const v = process.argv[i + 1]
-- if (!v || v.startsWith('--')) throw new Refusal(`${name} needs a value`)
-- return v
--}
--
--function subjectLookup(
-- source: string | undefined,
-- config: ReturnType<typeof loadConfig>,
--): NetworkSubjectLookup | null {
-- if (!source) return null
-- if (source === 'network') {
-- const platform = createPlatformClient(config.platform)
-- if (!platform) throw new Refusal('--subjects network needs OV_OAUTH_CLIENT_SECRET')
-- return networkSubjectLookup(createIdentityClient(platform.client))
-- }
-- try {
-- return subjectMapLookup(JSON.parse(readFileSync(source, 'utf8')))
-- } catch (err) {
-- throw new Refusal(`${source}: ${err instanceof Error ? err.message : String(err)}`)
-- }
--}
--
--async function main(): Promise<number> {
-- const config = loadConfig(process.env)
-- const livePath = arg('--live')
-- const worldPath = arg('--world') ?? config.dbPath
-- const subjects = arg('--subjects')
-- const backupPath = arg('--backup')
-- const apply = process.argv.includes('--apply')
-- const json = process.argv.includes('--json')
--
-- const refusal = refusalFor({
-- livePath,
-- apply,
-- backupPath,
-- subjects,
-- backupExists: backupPath !== undefined && existsSync(backupPath),
-- })
-- if (refusal || !livePath) throw new Refusal(refusal ?? '--live is required')
-- try {
-- assertSnapshotPath(livePath)
-- } catch (err) {
-- throw new Refusal(err instanceof Error ? err.message : String(err))
-- }
-- if (!existsSync(worldPath)) throw new Refusal(`world database ${worldPath} does not exist`)
--
-- const lookup = subjectLookup(subjects, config)
-- const subjectSource = !subjects
-- ? "Live's linked_accounts only"
-- : subjects === 'network'
-- ? 'the Network identity service'
-- : `subject map ${subjects}`
-- const live = new Database(livePath, { readonly: true, fileMustExist: true })
-- try {
-- if (!apply) {
-- const world = new Database(worldPath, { readonly: true, fileMustExist: true })
-- try {
-- const plan = await planLegacyImport(live, world, {
-- network: lookup,
-- subjectSource,
-- dryRun: true,
-- })
-- console.log(json ? JSON.stringify(plan.report, null, 2) : formatReport(plan.report))
-- return plan.report.reconciled ? 0 : 3
-- } finally {
-- world.close()
-- }
-- }
--
-- // Apply: back up world.db before anything (including the schema upgrade) writes to it.
-- const raw = new Database(worldPath, { fileMustExist: true })
-- try {
-- await backupAndVerify(raw, backupPath as string, (p) => new Database(p, { readonly: true }))
-- } finally {
-- raw.close()
-- }
-- console.error(`backup written and verified (integrity_check ok): ${backupPath}`)
--
-- const store = openSqliteStore(worldPath) // upgrades to the schema with legacy_live_rows
-- try {
-- const plan = await planLegacyImport(live, store.db, {
-- network: lookup,
-- subjectSource,
-- dryRun: false,
-- })
-- if (!plan.report.reconciled) {
-- console.log(json ? JSON.stringify(plan.report, null, 2) : formatReport(plan.report))
-- console.error('not reconciled: nothing written')
-- return 3
-- }
-- const written = applyLegacyImport(store.db, plan, Date.now())
-- const after = archivedCounts(store.db)
-- for (const t of plan.report.tables) {
-- if (t.decision === 'import' && (after.get(t.table) ?? 0) < t.imported) {
-- throw new Error(`post-apply check: ${t.table} holds fewer rows than imported`)
-- }
-- }
-- console.log(json ? JSON.stringify(plan.report, null, 2) : formatReport(plan.report))
-- console.error(`wrote ${written} rows to legacy_live_rows`)
-- return 0
-- } finally {
-- store.close()
-- }
-- } finally {
-- live.close()
-- }
--}
--
--main().then(
-- (code) => process.exit(code),
-- (err: unknown) => {
-- if (err instanceof Refusal) {
-- console.error(`refused: ${err.message}`)
-- process.exit(2)
-- }
-- console.error('live legacy import failed:', err)
-- process.exit(1)
-- },
--)
ddiff --git a/apps/server/scripts/migrateIdentity.ts b/apps/server/scripts/migrateIdentity.ts
ddeleted file mode 100644
iindex 1b1ef0e..0000000
---- a/apps/server/scripts/migrateIdentity.ts
++++ /dev/null
@@@ -1,40 +0,0 @@
--/**
-- * Adopts every account still keyed by `ovn:<network user id>` into its
-- * canonical openvibe.network subject (ADR-0006). Sign-in does this for each
-- * account on its own; this covers the ones that have not signed in since.
-- *
-- * DB_PATH=/opt/openvibe.games/data/world.db \
-- * OV_OAUTH_CLIENT_SECRET=… OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000 \
-- * node --import tsx apps/server/scripts/migrateIdentity.ts [--dry-run]
-- *
-- * Needs the grant [games, identity.subject.resolve, openvibe.network].
-- * Idempotent; safe to run while the game server is up (SQLite WAL).
-- */
--import { openSqliteStore } from '@openvibe/persistence/sqlite'
--import { createIdentityClient } from 'openvibe-sdk/identity'
--import { loadConfig } from '../src/config.js'
--import { migrateLegacyAccounts } from '../src/platform/identityMigration.js'
--import { createPlatformClient } from '../src/platform/serviceClient.js'
--
--async function main(): Promise<void> {
-- const config = loadConfig(process.env)
-- const platform = createPlatformClient(config.platform)
-- if (!platform) {
-- console.error('OV_OAUTH_CLIENT_SECRET is not set: cannot call the Network identity service')
-- process.exit(2)
-- }
-- const store = openSqliteStore(config.dbPath)
-- try {
-- const report = await migrateLegacyAccounts(store, createIdentityClient(platform.client), {
-- dryRun: process.argv.includes('--dry-run'),
-- })
-- console.log(JSON.stringify(report, null, 2))
-- } finally {
-- store.close()
-- }
--}
--
--main().catch((err: unknown) => {
-- console.error('identity migration failed:', err)
-- process.exit(1)
--})
ddiff --git a/apps/server/scripts/sliceTest.ts b/apps/server/scripts/sliceTest.ts
iindex 295ef16..1c410db 100644
---- a/apps/server/scripts/sliceTest.ts
++++ b/apps/server/scripts/sliceTest.ts
@@@ -77,7 +77,6 @@ function startServer(): Promise<void> {
...process.env,
PORT: String(PORT),
DB_PATH: dbPath,
-- GUEST_IP_BINDING: 'off',
EVENT_INTERVAL_SCALE: '0.05',
MAP_PATH: mapPath,
},
ddiff --git a/apps/server/src/config.ts b/apps/server/src/config.ts
iindex d337bcb..06d2de2 100644
---- a/apps/server/src/config.ts
++++ b/apps/server/src/config.ts
@@@ -7,8 +7,6 @@ export interface ServerConfig {
mapPath: string
editorKey: string | null
networkAuthUrl: string | null
-- /** Bind fresh guest tokens to client IPs (off in tests: peers share an IP). */
-- guestIpBinding: boolean
/** openvibe.network OAuth client (SSO); null until the secret is configured. */
oauth: {
clientId: string
@@@ -101,7 +99,6 @@ export function loadConfig(env: NodeJS.ProcessEnv): ServerConfig {
(env.OV_OAUTH_CLIENT_SECRET
? `${env.OV_NETWORK_URL ?? 'https://openvibe.network'}/api/auth/me`
: null),
-- guestIpBinding: env.GUEST_IP_BINDING !== 'off',
oauth: env.OV_OAUTH_CLIENT_SECRET
? {
clientId: env.OV_OAUTH_CLIENT_ID ?? 'games',
ddiff --git a/apps/server/src/game/gameServer.ts b/apps/server/src/game/gameServer.ts
iindex 8fd2082..8c4cd79 100644
---- a/apps/server/src/game/gameServer.ts
++++ b/apps/server/src/game/gameServer.ts
@@@ -68,7 +68,7 @@ import {
} from '@openvibe/shared'
import type { ServerConfig } from '../config.js'
import { canEditMap, resolveNetworkUser } from '../net/networkAuth.js'
--import { accountForNetworkUser, isGuestToken } from '../platform/accounts.js'
++import { isGuestToken } from '../platform/accounts.js'
import type { EventPlayer, GameEventRecorder, ProgressSnapshot } from '../platform/gameEvents.js'
import type { ProgressSummaryWriter } from '../platform/progressSummary.js'
import type { ModHost, ModRuntime } from '../mods/runtime.js'
@@@ -101,8 +101,6 @@ import { EventManager } from './eventManager.js'
/** A network connection as the game sees it — transport-agnostic. */
export interface GameConnection {
-- /** Real client IP (Cloudflare-aware) — guest identity hangs off this. */
-- ip: string
send(text: string): void
close(code: number, reason: string): void
}
@@@ -1233,39 +1231,29 @@ export class GameServer {
}
const slot = msg.slot ?? 0
-- // Account resolution. openvibe.network sign-in keys the account on the SSO
-- // identity (3 slots, follows you across devices). Guests get ONE
-- // character bound to their connection: browser token first, IP as the
-- // recovery path when the token is gone.
++ // Account resolution. openvibe.network sign-in keys the account on the
++ // canonical subject (3 slots, follows you across devices). Guests get ONE
++ // character bound to their connection's browser token.
let token = msg.token
let subjectId: string | null = null
let authIat: number | null = null
let rank: 'owner' | 'admin' | 'moderator' | null = null
if (msg.auth) {
const user = await resolveNetworkUser(this.config.networkAuthUrl, msg.auth)
-- if (!user) {
++ // A signed-in account is keyed by its canonical subject (ADR-0006); a
++ // token that carries none cannot name an account.
++ if (!user || !user.subjectId) {
conn.send(encodeServerMessage({ t: 'reject', reason: 'auth_failed' }))
conn.close(4009, 'auth_failed')
return
}
-- // Canonical subject key (ADR-0006); pre-subject characters are adopted
-- // from their legacy `ovn:<id>` key on the way in.
-- const account = accountForNetworkUser(this.store, user, Date.now(), (conflict) =>
-- this.log.warn('legacy identity conflict', conflict),
-- )
-- if (account.adopted > 0) {
-- this.log.info('legacy characters adopted', {
-- subject: account.subjectId ?? '',
-- moved: account.adopted,
-- })
-- }
-- token = account.key
-- subjectId = account.subjectId
++ token = user.subjectId
++ subjectId = user.subjectId
authIat = tokenIat(msg.auth)
rank = user.rank
// Guest conversion (WS-B task 8): the character this browser played as a guest joins the
// account in its first free slot, once.
-- if (subjectId && isGuestToken(msg.token)) {
++ if (isGuestToken(msg.token)) {
const g = this.store.identity.adoptGuestCharacter(msg.token, subjectId, Date.now())
if (g.moved > 0)
this.log.info('guest character adopted', { subject: subjectId, slot: g.slot ?? -1 })
@@@ -1273,9 +1261,9 @@ export class GameServer {
this.log.info('guest character kept: account slots full', { subject: subjectId })
}
} else {
-- // A guest token may never look like an account key (`usr_…`,
-- // `ovn:…`): that would open someone else's characters. The client
-- // mints plain alphanumerics; anything key-shaped is refused.
++ // A guest token may never look like an account key (`usr_…`): that
++ // would open someone else's characters. The client mints plain
++ // alphanumerics; anything key-shaped is refused.
if (!isGuestToken(msg.token)) {
conn.send(encodeServerMessage({ t: 'reject', reason: 'invalid_hello' }))
conn.close(4007, 'invalid_guest_token')
@@@ -1286,9 +1274,6 @@ export class GameServer {
conn.close(4010, 'guest_one_character')
return
}
-- if (this.config.guestIpBinding && conn.ip !== 'unknown') {
-- token = this.store.guests.resolve(conn.ip, msg.token)
-- }
}
const existing = this.store.players.findByTokenSlot(token, slot)
// One live session per CHARACTER; other characters of the same account
ddiff --git a/apps/server/src/game/platformIntegration.test.ts b/apps/server/src/game/platformIntegration.test.ts
iindex f786e15..96fa9e8 100644
---- a/apps/server/src/game/platformIntegration.test.ts
++++ b/apps/server/src/game/platformIntegration.test.ts
@@@ -3,8 +3,7 @@
* (headless Havok, real SQLite file, protocol-level connections), across a
* restart:
*
-- * - a signed-in player is keyed by the canonical subject, and a character
-- * from before subjects (`ovn:<id>`) is adopted with the same player id;
++ * - a signed-in player is keyed by the canonical subject;
* - a guest presenting an account key as its token is refused;
* - a mod's props enter the world through the runtime seam, a denied
* capability never takes effect, and a revoked mod's props are gone on
@@@ -58,7 +57,6 @@ const dbPath = join(dir, 'world.db')
const config = loadConfig({
DB_PATH: dbPath,
MAP_PATH: join(dir, 'map.json'),
-- GUEST_IP_BINDING: 'off',
OV_NETWORK_AUTH_URL: 'http://network.test/api/auth/me',
})
let havok: unknown
@@@ -130,7 +128,6 @@ function connect(game: GameServer) {
const inbox: ServerMessage[] = []
let closed: { code: number; reason: string } | null = null
const conn: GameConnection = {
-- ip: '127.0.0.1',
send: (text) => inbox.push(JSON.parse(text) as ServerMessage),
close: (code, reason) => {
closed = { code, reason }
@@@ -167,51 +164,28 @@ describe('Games on platform identity, events and mods (real server, restart)', (
},
},
})
-- let legacyPlayerId = ''
++ let signedInPlayerId = ''
let timeBefore = ''
it('runs identity, mods and events against the live world', async () => {
-- // A character from before canonical subjects, keyed by the Network user id.
-- {
-- const seed = openSqliteStore(dbPath)
-- seed.players.upsert({
-- id: 'pl_legacy_57',
-- token: 'ovn:57',
-- name: 'Ana',
-- pos: [3, 2, 3],
-- yaw: 0,
-- inventory: { size: 24, hotbar: 6, slots: [] },
-- skills: { mining: 40 },
-- friends: [],
-- appearance: null,
-- charSlot: 0,
-- armor: null,
-- reputation: {},
-- unlocks: [],
-- activeJob: null,
-- stats: null,
-- updatedAt: 1,
-- })
-- seed.close()
-- legacyPlayerId = 'pl_legacy_57'
-- }
const a = boot(platform)
// A guest cannot claim an account key as its token.
const intruder = connect(a.game)
-- const refused = await intruder.hello('ovn:57')
++ const refused = await intruder.hello(SUBJECT)
expect(refused.welcome).toBeUndefined()
expect(refused.closed).toEqual({ code: 4007, reason: 'invalid_guest_token' })
-- // The signed-in player lands on the adopted legacy character.
++ // The signed-in player is keyed by the canonical subject.
const ana = connect(a.game)
const joined = await ana.hello('abcdefgh12', 'tok-ana')
-- expect(joined.welcome).toMatchObject({ t: 'welcome', playerId: legacyPlayerId })
-- expect(a.store.players.findById(legacyPlayerId)).toMatchObject({
++ expect(joined.welcome).toBeDefined()
++ signedInPlayerId = (joined.welcome as { playerId: string }).playerId
++ a.game.flush()
++ expect(a.store.players.findById(signedInPlayerId)).toMatchObject({
token: SUBJECT,
subjectId: SUBJECT,
})
-- expect(a.store.identity.subjectForLegacy('ovn:57')).toBe(SUBJECT)
// Mod A: props granted, announcements requested but denied.
a.registry.install(
@@@ -292,9 +266,8 @@ describe('Games on platform identity, events and mods (real server, restart)', (
// The account comes back to the same character, by subject.
const ana = connect(b.game)
const again = await ana.hello('zyxwvut98', 'tok-ana')
-- expect(again.welcome).toMatchObject({ playerId: legacyPlayerId })
-- expect(b.store.players.listByToken(SUBJECT).map((p) => p.id)).toEqual([legacyPlayerId])
-- expect(b.store.players.listByToken('ovn:57')).toEqual([])
++ expect(again.welcome).toMatchObject({ playerId: signedInPlayerId })
++ expect(b.store.players.listByToken(SUBJECT).map((p) => p.id)).toEqual([signedInPlayerId])
b.game.onDisconnect(ana.conn)
b.game.shutdown()
await b.outbox.stop()
ddiff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
iindex e1daaef..8e29f3e 100644
---- a/apps/server/src/main.ts
++++ b/apps/server/src/main.ts
@@@ -35,7 +35,7 @@ import { createNetworkModGrants } from './mods/networkGrants.js'
import { createGamesActorLimits } from './net/actorLimits.js'
import { handleModsRequest } from './mods/routes.js'
import { ModRuntime } from './mods/runtime.js'
--import { accountForNetworkUser, isGuestToken } from './platform/accounts.js'
++import { isGuestToken } from './platform/accounts.js'
import { ProgressSummaryWriter } from './platform/progressSummary.js'
import {
EVENT_SOURCE,
@@@ -73,8 +73,8 @@ async function main(): Promise<void> {
// same file over /map.json).
if (existsSync(config.mapPath)) {
try {
-- // v1 artifacts still load — parseMapFile migrates them — but the
-- // RUNTIME representation is always v2 compiled straight through.
++ // The document is v2; the RUNTIME representation is v2 compiled straight
++ // through.
const parsed = parseMapFile(JSON.parse(readFileSync(config.mapPath, 'utf8')))
if (parsed.ok) {
// The map's statics live in the override, NOT merged into
@@@ -85,7 +85,6 @@ async function main(): Promise<void> {
log.info('edited map loaded', {
statics: parsed.map.statics.length,
terrains: parsed.map.terrains.length,
-- migrated: parsed.migrated,
})
} else {
log.warn('edited map invalid — ignoring', { issues: parsed.issues.slice(0, 5).join('; ') })
@@@ -306,13 +305,12 @@ async function main(): Promise<void> {
let account = token
if (auth) {
const user = await resolveNetworkUser(config.networkAuthUrl, auth)
-- if (!user) return []
-- const resolved = accountForNetworkUser(store, user, Date.now())
-- account = resolved.key
++ if (!user?.subjectId) return []
++ account = user.subjectId
// Guest conversion (WS-B task 8): this browser's guest character shows up in the account's
// list (and moves there) the moment it signs in, before any slot is picked.
-- if (resolved.subjectId && isGuestToken(token))
-- store.identity.adoptGuestCharacter(token, resolved.subjectId, Date.now())
++ if (isGuestToken(token))
++ store.identity.adoptGuestCharacter(token, user.subjectId, Date.now())
} else if (!isGuestToken(token)) {
// Never list an account key's characters for a guest query.
return []
ddiff --git a/apps/server/src/mods/registry.ts b/apps/server/src/mods/registry.ts
iindex 6683a62..15fbb8e 100644
---- a/apps/server/src/mods/registry.ts
++++ b/apps/server/src/mods/registry.ts
@@@ -53,7 +53,7 @@ export const TRUST_TIERS: readonly ModTrustTier[] = ['unreviewed', 'reviewed', '
/** Who performed a registry change: an audit string and an event actor. */
export interface ModActor {
-- /** `usr_…`, `ovn:<id>` for a pre-subject staff token, or `svc:<client>`. */
++ /** `usr_…`, a Network user id, or `svc:<client>`. */
audit: string
subject: SubjectRef
}
ddiff --git a/apps/server/src/net/httpServer.test.ts b/apps/server/src/net/httpServer.test.ts
iindex 07d547c..312895f 100644
---- a/apps/server/src/net/httpServer.test.ts
++++ b/apps/server/src/net/httpServer.test.ts
@@@ -128,15 +128,6 @@ describe('real routes on the apex', () => {
expect(r.body).toContain(marker)
})
-- it.each([
-- ['/game', '/play'],
-- ['/canvas', '/'],
-- ])('%s (the old OpenVibe.Live URL) redirects to %s', async (path, location) => {
-- const r = await send(path)
-- expect(r.status).toBe(301)
-- expect(r.headers.location).toBe(location)
-- })
--
it('serves hashed build assets as immutable, and public files', async () => {
const asset = await send(ASSET)
expect(asset.status).toBe(200)
@@@ -258,14 +249,11 @@ describe('play.openvibe.games', () => {
expect(r.body).toContain('game page')
})
-- it.each(['/play', '/play/', '/play.html', '/game', '/canvas'])(
-- '%s redirects to /',
-- async (path) => {
-- const r = await send(path, { host: PLAY })
-- expect(r.status).toBe(301)
-- expect(r.headers.location).toBe('/')
-- },
-- )
++ it.each(['/play', '/play/', '/play.html'])('%s redirects to /', async (path) => {
++ const r = await send(path, { host: PLAY })
++ expect(r.status).toBe(301)
++ expect(r.headers.location).toBe('/')
++ })
it('serves the editor, assets, map and health like the apex', async () => {
expect((await send('/editor', { host: PLAY })).body).toContain('editor page')
ddiff --git a/apps/server/src/net/httpServer.ts b/apps/server/src/net/httpServer.ts
iindex 298cac8..333813f 100644
---- a/apps/server/src/net/httpServer.ts
++++ b/apps/server/src/net/httpServer.ts
@@@ -167,7 +167,7 @@ export function createHttpServer(
if (platform?.handle?.(req, res)) return
if (url === '/map.json' && mapPath) {
// The canonical v2 document, with its revision as an ETag. This IS the
-- // wire format: there is no v1 projection any more.
++ // wire format.
void loadMap(mapPath).then((rec) => {
res.writeHead(200, {
'content-type': 'application/json',
@@@ -246,18 +246,11 @@ export function createHttpServer(
})
return
}
-- // ── Custom texture assets: uploaded once, served to every player. ──
++ // ── Map assets: uploaded once, served to every player. ──
// Big source files (4k photo textures) live on disk next to the map
// artifact instead of being base64-embedded into map.json.
-- // Generic content-addressed asset upload (textures, paint masks, models).
-- // `/api/texture` is the same handler under its old name so older editor
-- // builds keep working; there is ONE store behind both.
-- if (
-- (url === '/api/map-assets' || url === '/api/texture') &&
-- req.method === 'POST' &&
-- mapPath &&
-- editorAuth
-- ) {
++ // Generic content-addressed upload (textures, paint masks, models).
++ if (url === '/api/map-assets' && req.method === 'POST' && mapPath && editorAuth) {
const token = (req.headers['x-editor-key'] as string | undefined) ?? undefined
void editorAuthorized(editorAuth, token).then(async (authed) => {
if (!authed) {
@@@ -673,14 +666,6 @@ export function createHttpServer(
res.end()
return
}
-- // The game's URLs from when it lived on OpenVibe.Live, which still links
-- // and 301s to them: the game is /play now, and the pixel canvas is gone
-- // (its nearest page is the portal).
-- if (url === '/game' || url === '/canvas') {
-- res.writeHead(301, { location: url === '/game' && !playHost ? '/play' : '/' })
-- res.end()
-- return
-- }
const pageAlias =
url === '/'
? playHost
ddiff --git a/apps/server/src/net/mapAssetStore.ts b/apps/server/src/net/mapAssetStore.ts
iindex bdcb60d..9af9286 100644
---- a/apps/server/src/net/mapAssetStore.ts
++++ b/apps/server/src/net/mapAssetStore.ts
@@@ -1,16 +1,8 @@
/**
* Content-addressed store for map assets (textures, paint masks, models).
*
-- * What it replaces: `POST /api/texture` named uploads
-- * `tex-<timestamp>-<random>.<ext>`, took the extension from a query
-- * parameter, and wrote them with `writeFileSync` on the request thread.
-- * So the same texture uploaded twice cost twice the disk and produced two
-- * unrelated URLs that no map could ever be shown to share; a client could
-- * claim `ext=png` for arbitrary bytes; and a slow disk blocked the event loop
-- * for every other player on the server.
-- *
-- * Here the NAME IS THE CONTENT: sha256 of the actual bytes, with the
-- * extension decided by sniffing those bytes rather than believing the client.
++ * The NAME IS THE CONTENT: sha256 of the actual bytes, with the extension
++ * decided by sniffing those bytes rather than believing the client.
* Identical uploads collapse to one file and one URL for free, which is what
* makes "save the map again without touching the paint mask" cost nothing.
* Files are immutable, so they can be served with a permanent cache header.
ddiff --git a/apps/server/src/net/mapStore.test.ts b/apps/server/src/net/mapStore.test.ts
iindex 530a477..1ab6124 100644
---- a/apps/server/src/net/mapStore.test.ts
++++ b/apps/server/src/net/mapStore.test.ts
@@@ -25,20 +25,14 @@ describe('loadMap', () => {
expect(r.map.terrains).toEqual([])
})
-- it('migrates a v1 artifact on the way in', async () => {
++ it('ignores an invalid artifact rather than throwing', async () => {
writeFileSync(
mapPath,
-- JSON.stringify({
-- v: 1,
-- halfExtent: 100,
-- sub: 8,
-- heights: blankHeights(8),
-- statics: [],
-- }),
++ JSON.stringify({ v: 1, halfExtent: 100, sub: 8, heights: '', statics: [] }),
)
const r = await loadMap(mapPath)
expect(r.map.v).toBe(2)
-- expect(r.map.terrains).toHaveLength(1)
++ expect(r.map.terrains).toEqual([])
})
it('falls back to an empty map rather than throwing on garbage', async () => {
@@@ -129,21 +123,10 @@ describe('save pipeline', () => {
expect((await saveMap(mapPath, huge, await loadMap(mapPath), undefined)).status).toBe(413)
})
-- it('accepts and migrates a v1 payload', async () => {
-- const v1 = {
-- v: 1,
-- halfExtent: 100,
-- sub: 8,
-- heights: blankHeights(8),
-- statics: [],
-- mix: 'data:image/png;base64,AAA',
-- }
++ it('rejects a v1 payload as an invalid map', async () => {
++ const v1 = { v: 1, halfExtent: 100, sub: 8, heights: '', statics: [] }
const r = await saveMap(mapPath, JSON.stringify(v1), await loadMap(mapPath), undefined)
-- expect(r.status).toBe(200)
-- // It is persisted as v2, with the splat migrated to paint layers.
-- const onDisk = JSON.parse(readFileSync(mapPath, 'utf8'))
-- expect(onDisk.v).toBe(2)
-- expect(onDisk.terrains[0].surface.paint.layers).toHaveLength(3)
++ expect(r.status).toBe(422)
})
it('creates the artifact on a first save even when the map is unchanged', async () => {
ddiff --git a/apps/server/src/net/mapStore.ts b/apps/server/src/net/mapStore.ts
iindex b01f70e..962aa78 100644
---- a/apps/server/src/net/mapStore.ts
++++ b/apps/server/src/net/mapStore.ts
@@@ -7,7 +7,7 @@
* live world, and two admins saving at once silently clobbered each other —
* "revisions" were a string of array lengths compared on the client.
*
-- * Now every save is: authenticate → size-limit → parse → migrate → validate →
++ * Now every save is: authenticate → size-limit → parse → validate →
* revision check → canonicalise → atomic write → apply live → broadcast.
*/
import { createHash } from 'node:crypto'
@@@ -45,9 +45,9 @@ function record(map: MapFileV2): MapRecord {
}
/**
-- * Load the artifact, migrating v1 on the way in. A missing or unreadable file
-- * yields an empty v2 map rather than throwing — a fresh server should boot to
-- * a blank world, not fail.
++ * Load the artifact. A missing, unreadable or invalid file yields an empty v2
++ * map rather than throwing — a fresh server should boot to a blank world, not
++ * fail.
*/
export async function loadMap(mapPath: string): Promise<MapRecord> {
if (!existsSync(mapPath)) return record(emptyMapV2())
ddiff --git a/apps/server/src/net/wsTransport.ts b/apps/server/src/net/wsTransport.ts
iindex ffdc4d1..979a00a 100644
---- a/apps/server/src/net/wsTransport.ts
++++ b/apps/server/src/net/wsTransport.ts
@@@ -20,8 +20,8 @@ export function attachWebSocket(http: Server, game: GameServer, log: Logger): We
void http
wss.on('connection', (ws: WebSocket, req) => {
-- // Real client IP: Cloudflare/nginx headers first (we sit behind both in
-- // prod), socket address in dev. Guest identity hangs off this.
++ // Real client IP for logs: Cloudflare/nginx headers first (we sit behind
++ // both in prod), socket address in dev.
const fwd = req.headers['x-forwarded-for']
const remote =
(typeof req.headers['cf-connecting-ip'] === 'string'
@@@ -34,7 +34,6 @@ export function attachWebSocket(http: Server, game: GameServer, log: Logger): We
let windowStart = Date.now()
const conn: GameConnection = {
-- ip: remote,
send: (text) => {
if (ws.readyState === ws.OPEN) ws.send(text)
},
ddiff --git a/apps/server/src/platform/accountData.test.ts b/apps/server/src/platform/accountData.test.ts
iindex 3800e29..851dc25 100644
---- a/apps/server/src/platform/accountData.test.ts
++++ b/apps/server/src/platform/accountData.test.ts
@@@ -12,22 +12,50 @@ import { createAccountData, type AccountEvent } from './accountData.js'
* twice; foreign or malformed events are ignored.
*/
const req = createRequire(import.meta.url)
--const { validate } = req('openvibe-contracts') as { validate(id: string, v: unknown): { valid: boolean; errors?: unknown } }
++const { validate } = req('openvibe-contracts') as {
++ validate(id: string, v: unknown): { valid: boolean; errors?: unknown }
++}
const log = createConsoleLogger({ app: 'test' }, 'error')
const DANA = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3'
const OLD = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2B4'
const OTTO = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2C5'
const char = (id: string, token: string) => ({
-- id, token, name: `char-${id}`, pos: [0, 1, 0] as [number, number, number], yaw: 0, inventory: { size: 24, hotbar: 6, slots: [] }, skills: { mining: 3 },
-- friends: [], appearance: null, charSlot: 0, armor: null, reputation: {}, unlocks: [], activeJob: null, stats: null, updatedAt: 1,
++ id,
++ token,
++ name: `char-${id}`,
++ pos: [0, 1, 0] as [number, number, number],
++ yaw: 0,
++ inventory: { size: 24, hotbar: 6, slots: [] },
++ skills: { mining: 3 },
++ friends: [],
++ appearance: null,
++ charSlot: 0,
++ armor: null,
++ reputation: {},
++ unlocks: [],
++ activeJob: null,
++ stats: null,
++ updatedAt: 1,
++})
++const ev = (type: string, payload: Record<string, unknown>, source = 'network'): AccountEvent => ({
++ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2D6',
++ event_type: type,
++ source,
++ payload,
})
--const ev = (type: string, payload: Record<string, unknown>, source = 'network'): AccountEvent => ({ event_id: 'evt_01J8Z3Q4R5S6T7V8W9X0Y1Z2D6', event_type: type, source, payload })
describe('account export and deletion (ADR-033)', () => {
it('exports, then closes sessions, erases and confirms once', async () => {
const store = openSqliteStore(':memory:')
store.players.upsertMany([char('d0', DANA), char('d1', OLD), char('o0', OTTO)])
-- const sent: { path: string; body: { files?: { name: string; content: unknown }[]; erased?: Record<string, number>; retained?: Record<string, number> } }[] = []
++ const sent: {
++ path: string
++ body: {
++ files?: { name: string; content: unknown }[]
++ erased?: Record<string, number>
++ retained?: Record<string, number>
++ }
++ }[] = []
let failNext = false
const closed: string[][] = []
const data = createAccountData({
@@@ -46,19 +74,32 @@ describe('account export and deletion (ADR-033)', () => {
},
log,
})
-- const exp = ev('network.account.export_requested', { export_id: 'exp_01J8Z3Q4R5S6T7V8W9X0Y1Z2E7', subject: DANA })
++ const exp = ev('network.account.export_requested', {
++ export_id: 'exp_01J8Z3Q4R5S6T7V8W9X0Y1Z2E7',
++ subject: DANA,
++ })
expect(await data.apply({ ...exp, source: 'live' })).toBe('ignored:source')
-- expect(await data.apply(ev('network.account.export_requested', { export_id: 'nope', subject: DANA }))).toBe('ignored:payload')
++ expect(
++ await data.apply(
++ ev('network.account.export_requested', { export_id: 'nope', subject: DANA }),
++ ),
++ ).toBe('ignored:payload')
expect(await data.apply(exp)).toBe('exported')
const part = sent[0]!.body
-- expect(validate('network.account-export-part@1', { subject: DANA, files: part.files }).valid).toBe(true)
++ expect(
++ validate('network.account-export-part@1', { subject: DANA, files: part.files }).valid,
++ ).toBe(true)
const chars = part.files![0]!.content as Record<string, unknown>[]
expect(chars.map((c) => c.id)).toEqual(['d0'])
expect(chars[0]).not.toHaveProperty('token')
expect(await data.apply(exp)).toBe('unchanged')
expect(sent.length).toBe(1)
-- const del = ev('network.account.deleted', { deletion_id: 'del_01J8Z3Q4R5S6T7V8W9X0Y1Z2F8', subject: DANA, aliases: [OLD] })
++ const del = ev('network.account.deleted', {
++ deletion_id: 'del_01J8Z3Q4R5S6T7V8W9X0Y1Z2F8',
++ subject: DANA,
++ aliases: [OLD],
++ })
failNext = true
await expect(data.apply(del)).rejects.toThrow(/confirmation refused: 503/)
expect(closed).toEqual([[DANA, OLD]])
@@@ -67,9 +108,11 @@ describe('account export and deletion (ADR-033)', () => {
expect(await data.apply(del)).toBe('confirmed')
expect(closed.length).toBe(1)
const conf = sent[1]!
-- expect(conf.path).toBe('/internal/account-deletions/del_01J8Z3Q4R5S6T7V8W9X0Y1Z2F8/confirmations')
++ expect(conf.path).toBe(
++ '/internal/account-deletions/del_01J8Z3Q4R5S6T7V8W9X0Y1Z2F8/confirmations',
++ )
expect(validate('network.account-deletion-confirmation@1', conf.body).valid).toBe(true)
-- expect(conf.body.erased).toEqual({ characters: 2, legacy_rows: 0, identity_rows: 0 })
++ expect(conf.body.erased).toEqual({ characters: 2, identity_rows: 0 })
expect(conf.body.retained).toEqual({ structures_unowned: 0 })
expect(await data.apply(del)).toBe('unchanged')
expect(sent.length).toBe(2)
ddiff --git a/apps/server/src/platform/accountData.ts b/apps/server/src/platform/accountData.ts
iindex ffebcce..59bd109 100644
---- a/apps/server/src/platform/accountData.ts
++++ b/apps/server/src/platform/accountData.ts
@@@ -5,18 +5,20 @@
* failure is redelivered without erasing twice.
*
* network.account.export_requested Games' part (POST /internal/account-exports/:id/parts, service token):
-- * characters.json (without the sign-in token), structures.json and
-- * live_game_rows.json (rows imported from Live's old game).
++ * characters.json (without the sign-in token) and structures.json.
* network.account.deleted the person's game sessions are closed first (each close saves its character),
-- * then store.identity.eraseSubjects removes their characters, imported rows and
-- * identity-map rows; their structures stay in the world without an owner. Games
-- * then confirms with counts.
++ * then store.identity.eraseSubjects removes their characters and adoption rows;
++ * their structures stay in the world without an owner. Games then confirms
++ * with counts.
*/
import type Database from 'better-sqlite3'
import type { Logger } from '@openvibe/shared'
import type { IdentityRepository } from '@openvibe/persistence'
--export const ACCOUNT_TOPICS = ['network.account.export_requested', 'network.account.deleted'] as const
++export const ACCOUNT_TOPICS = [
++ 'network.account.export_requested',
++ 'network.account.deleted',
++] as const
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/
const EXPORT_RE = /^exp_[0-9A-HJKMNP-TV-Z]{26}$/
const DELETION_RE = /^del_[0-9A-HJKMNP-TV-Z]{26}$/
@@@ -52,11 +54,14 @@ export function createAccountData(opts: {
sent_at TEXT,
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
)`)
-- const get = opts.db.prepare<[string], Record_>('SELECT id, outcome, sent_at, applied_at FROM account_data_events WHERE id = ?')
++ const get = opts.db.prepare<[string], Record_>(
++ 'SELECT id, outcome, sent_at, applied_at FROM account_data_events WHERE id = ?',
++ )
const markSent = opts.db.prepare('UPDATE account_data_events SET sent_at = ? WHERE id = ?')
async function apply(event: AccountEvent): Promise<string> {
-- if (!event || !ACCOUNT_TOPICS.includes(event.event_type as (typeof ACCOUNT_TOPICS)[number])) return 'ignored:type'
++ if (!event || !ACCOUNT_TOPICS.includes(event.event_type as (typeof ACCOUNT_TOPICS)[number]))
++ return 'ignored:type'
if (event.source !== 'network') return 'ignored:source'
const p = event.payload ?? {}
const subject = String(p.subject ?? '')
@@@ -69,14 +74,25 @@ export function createAccountData(opts: {
const files = [
{ name: 'characters.json', content: data.characters },
...(data.structures.length ? [{ name: 'structures.json', content: data.structures }] : []),
-- ...(data.legacyLiveRows.length ? [{ name: 'live_game_rows.json', content: data.legacyLiveRows }] : []),
]
const res = await opts.send(`/internal/account-exports/${id}/parts`, { subject, files })
-- const outcome = res.ok ? 'exported' : res.status === 409 || res.status === 404 ? 'closed' : null
++ const outcome = res.ok
++ ? 'exported'
++ : res.status === 409 || res.status === 404
++ ? 'closed'
++ : null
if (!outcome) throw new Error(`export part refused: ${res.status}`)
opts.db
-- .prepare('INSERT OR REPLACE INTO account_data_events (id, kind, subject, outcome, sent_at) VALUES (?, ?, ?, ?, ?)')
-- .run(id, 'export', subject, JSON.stringify({ result: outcome, characters: data.characters.length }), new Date().toISOString())
++ .prepare(
++ 'INSERT OR REPLACE INTO account_data_events (id, kind, subject, outcome, sent_at) VALUES (?, ?, ?, ?, ?)',
++ )
++ .run(
++ id,
++ 'export',
++ subject,
++ JSON.stringify({ result: outcome, characters: data.characters.length }),
++ new Date().toISOString(),
++ )
return outcome
}
const id = String(p.deletion_id ?? '')
@@@ -84,7 +100,9 @@ export function createAccountData(opts: {
let rec = get.get(id)
let result = 'confirmed'
if (!rec) {
-- const aliases = Array.isArray(p.aliases) ? p.aliases.map(String).filter((s) => SUBJECT_RE.test(s)) : []
++ const aliases = Array.isArray(p.aliases)
++ ? p.aliases.map(String).filter((s) => SUBJECT_RE.test(s))
++ : []
const subjects = [subject, ...aliases]
await opts.closeSessions(subjects)
const erased = opts.identity.eraseSubjects(subjects)
@@@ -97,9 +115,16 @@ export function createAccountData(opts: {
}
if (rec.sent_at) return 'unchanged'
const o = JSON.parse(rec.outcome ?? '{}') as { erased?: Record<string, number> }
-- const erased = Object.fromEntries(Object.entries(o.erased ?? {}).filter(([k]) => k !== 'structures_unowned'))
++ const erased = Object.fromEntries(
++ Object.entries(o.erased ?? {}).filter(([k]) => k !== 'structures_unowned'),
++ )
const retained = { structures_unowned: o.erased?.structures_unowned ?? 0 }
-- const res = await opts.send(`/internal/account-deletions/${id}/confirmations`, { subject, completed_at: rec.applied_at, erased, retained })
++ const res = await opts.send(`/internal/account-deletions/${id}/confirmations`, {
++ subject,
++ completed_at: rec.applied_at,
++ erased,
++ retained,
++ })
if (!res.ok && res.status !== 404) throw new Error(`confirmation refused: ${res.status}`)
markSent.run(new Date().toISOString(), id)
return result
ddiff --git a/apps/server/src/platform/accounts.test.ts b/apps/server/src/platform/accounts.test.ts
iindex aca8ea2..4e0def1 100644
---- a/apps/server/src/platform/accounts.test.ts
++++ b/apps/server/src/platform/accounts.test.ts
@@@ -1,31 +1,8 @@
--import { openSqliteStore } from '@openvibe/persistence/sqlite'
--import type { PlayerDto } from '@openvibe/persistence'
import { describe, expect, it } from 'vitest'
--import { accountForNetworkUser, isGuestToken, legacyNetworkKey } from './accounts.js'
++import { isGuestToken } from './accounts.js'
const SUBJECT = 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0'
--function legacyChar(id: string, token: string, slot: number): PlayerDto {
-- return {
-- id,
-- token,
-- name: id,
-- pos: [4, 5, 6],
-- yaw: 0,
-- inventory: { size: 24, hotbar: 6, slots: [] },
-- skills: {},
-- friends: [],
-- appearance: null,
-- charSlot: slot,
-- armor: null,
-- reputation: {},
-- unlocks: [],
-- activeJob: null,
-- stats: null,
-- updatedAt: 1,
-- }
--}
--
describe('guest tokens (hostile input)', () => {
it('accepts what the client mints', () => {
expect(isGuestToken('a1B2c3D4')).toBe(true)
@@@ -39,14 +16,11 @@ describe('guest tokens (hostile input)', () => {
for (const bad of [
SUBJECT,
'gst_01JABCDEFGHJKMNPQRSTVWXYZ0',
-- 'ovn:57',
-- 'ovn:1',
'short',
'x'.repeat(65),
'has space1',
'usr_whatever123',
'GST_01JABCDEFGHJKMNPQRSTVWXYZ0',
-- 'ovn:57:x',
'',
null,
42,
@@@ -55,53 +29,3 @@ describe('guest tokens (hostile input)', () => {
}
})
})
--
--describe('accountForNetworkUser', () => {
-- it('keys a signed-in account by its subject and adopts legacy characters once', () => {
-- const store = openSqliteStore(':memory:')
-- store.players.upsertMany([legacyChar('p1', 'ovn:57', 0), legacyChar('p2', 'ovn:57', 1)])
-- const user = { id: '57', name: 'ana', rank: null, subjectId: SUBJECT }
--
-- const first = accountForNetworkUser(store, user, 1)
-- expect(first).toEqual({ key: SUBJECT, subjectId: SUBJECT, adopted: 2 })
-- expect(store.players.listByToken(SUBJECT).map((p) => p.id)).toEqual(['p1', 'p2'])
-- // Same player ids: friends lists, prop ownership and trust still point at them.
-- expect(store.players.findById('p1')?.pos).toEqual([4, 5, 6])
--
-- const again = accountForNetworkUser(store, user, 2)
-- expect(again).toEqual({ key: SUBJECT, subjectId: SUBJECT, adopted: 0 })
-- store.close()
-- })
--
-- it('keeps the legacy key for a token issued before subjects', () => {
-- const store = openSqliteStore(':memory:')
-- store.players.upsert(legacyChar('p1', 'ovn:8', 0))
-- const res = accountForNetworkUser(
-- store,
-- { id: '8', name: 'old', rank: null, subjectId: null },
-- 1,
-- )
-- expect(res).toEqual({ key: legacyNetworkKey('8'), subjectId: null, adopted: 0 })
-- expect(store.players.findByTokenSlot('ovn:8', 0)?.id).toBe('p1')
-- store.close()
-- })
--
-- it('reports a conflicting legacy map instead of re-homing characters', () => {
-- const store = openSqliteStore(':memory:')
-- store.players.upsert(legacyChar('p1', 'ovn:3', 0))
-- store.identity.adoptLegacyAccount('ovn:3', SUBJECT, 'network', 1)
-- store.players.upsert(legacyChar('p2', 'ovn:3', 1))
-- const conflicts: unknown[] = []
-- const other = 'usr_01JZZZZZZZZZZZZZZZZZZZZZZZ'
-- const res = accountForNetworkUser(
-- store,
-- { id: '3', name: 'x', rank: null, subjectId: other },
-- 2,
-- (c) => conflicts.push(c),
-- )
-- expect(res.key).toBe(other)
-- expect(conflicts).toHaveLength(1)
-- expect(store.players.findById('p2')?.token).toBe('ovn:3')
-- store.close()
-- })
--})
ddiff --git a/apps/server/src/platform/accounts.ts b/apps/server/src/platform/accounts.ts
iindex bdffdba..53164ed 100644
---- a/apps/server/src/platform/accounts.ts
++++ b/apps/server/src/platform/accounts.ts
@@@ -3,22 +3,14 @@
*
* A signed-in account is keyed by its canonical openvibe.network subject
* (`usr_…`, or `gst_…` for a Network guest) taken from the token's
-- * `subject_id` claim. Before subjects existed Games keyed it by
-- * `ovn:<network user id>`; those characters are adopted into the subject key
-- * the first time the account signs in (or by scripts/migrateIdentity.ts), and
-- * the mapping is kept in `identity_legacy_map`.
++ * `subject_id` claim.
*
* Local guests keep their browser token (ADR-0004). A guest token may use
* letters, digits, `_` and `-` only (the client mints alphanumerics), and
-- * may not start with a subject prefix, so it can never equal a subject or a
-- * legacy key: a guest presenting `usr_…` or `ovn:…` as its token cannot open
-- * someone else's characters.
++ * may not start with a subject prefix, so it can never equal a subject:
++ * a guest presenting `usr_…` as its token cannot open someone else's
++ * characters.
*/
--import type { IdentityRepository, PlayerRepository } from '@openvibe/persistence'
--import type { NetworkUser } from '../net/networkAuth.js'
--
--/** Prefix of pre-subject account keys: `ovn:<openvibe.network user id>`. */
--export const LEGACY_NETWORK_PREFIX = 'ovn:'
/** The client mints `[A-Za-z0-9]{32}` (apps/client/src/net/connection.ts getIdentity). */
const GUEST_TOKEN = /^[A-Za-z0-9_-]{8,64}$/
@@@ -28,51 +20,3 @@ const SUBJECT_PREFIX = /^(usr|gst|app|mod|svc)_/i
export function isGuestToken(token: unknown): token is string {
return typeof token === 'string' && GUEST_TOKEN.test(token) && !SUBJECT_PREFIX.test(token)
}
--
--export function legacyNetworkKey(networkUserId: string): string {
-- return `${LEGACY_NETWORK_PREFIX}${networkUserId}`.slice(0, 64)
--}
--
--export interface AccountResolution {
-- /** The players.token value the account's characters live under. */
-- key: string
-- /** Canonical subject, or null for a token older than subjects. */
-- subjectId: string | null
-- /** Characters moved from the legacy key by this call. */
-- adopted: number
--}
--
--export interface AccountStores {
-- identity: IdentityRepository
-- players: PlayerRepository
--}
--
--/**
-- * The account key for a signed-in Network user, adopting any characters
-- * still under its legacy key. Idempotent.
-- */
--export function accountForNetworkUser(
-- stores: AccountStores,
-- user: NetworkUser,
-- now: number,
-- onConflict?: (info: { legacyKey: string; subjectId: string; error: string }) => void,
--): AccountResolution {
-- const legacyKey = legacyNetworkKey(user.id)
-- if (!user.subjectId) {
-- // A token from before subjects: nothing canonical to key by yet. The
-- // characters stay reachable under the legacy key until the next sign-in
-- // with a current token adopts them.
-- return { key: legacyKey, subjectId: null, adopted: 0 }
-- }
-- const pending = stores.players.listByToken(legacyKey).length > 0
-- if (!pending && stores.identity.subjectForLegacy(legacyKey) !== null) {
-- return { key: user.subjectId, subjectId: user.subjectId, adopted: 0 }
-- }
-- try {
-- const { moved } = stores.identity.adoptLegacyAccount(legacyKey, user.subjectId, 'network', now)
-- return { key: user.subjectId, subjectId: user.subjectId, adopted: moved }
-- } catch (err) {
-- onConflict?.({ legacyKey, subjectId: user.subjectId, error: String(err) })
-- return { key: user.subjectId, subjectId: user.subjectId, adopted: 0 }
-- }
--}
ddiff --git a/apps/server/src/platform/identityMigration.test.ts b/apps/server/src/platform/identityMigration.test.ts
ddeleted file mode 100644
iindex d7f5756..0000000
---- a/apps/server/src/platform/identityMigration.test.ts
++++ /dev/null
@@@ -1,88 +0,0 @@
--import { openSqliteStore } from '@openvibe/persistence/sqlite'
--import type { PlayerDto } from '@openvibe/persistence'
--import { createIdentityClient } from 'openvibe-sdk/identity'
--import { createMockPlatform } from 'openvibe-sdk/testing'
--import { describe, expect, it } from 'vitest'
--import { loadPlatformConfig } from '../config.js'
--import { migrateLegacyAccounts } from './identityMigration.js'
--import { createPlatformClient } from './serviceClient.js'
--
--function char(id: string, token: string, slot = 0): PlayerDto {
-- return {
-- id,
-- token,
-- name: id,
-- pos: [0, 0, 0],
-- yaw: 0,
-- inventory: { size: 24, hotbar: 6, slots: [] },
-- skills: {},
-- friends: [],
-- appearance: null,
-- charSlot: slot,
-- armor: null,
-- reputation: {},
-- unlocks: [],
-- activeJob: null,
-- stats: null,
-- updatedAt: 1,
-- }
--}
--
--describe('batch identity migration (games service token -> Network identity)', () => {
-- it('adopts every resolvable legacy account, leaves unknown ones, and is idempotent', async () => {
-- const platform = createMockPlatform({
-- clients: {
-- games: {
-- secret: 's3cret',
-- grants: [{ capability: 'identity.subject.resolve', audience: 'openvibe.network' }],
-- },
-- },
-- users: [
-- { id: 57, username: 'ana' },
-- { id: 58, username: 'bo' },
-- ],
-- })
-- const [ana, bo] = [...platform.state.users.values()]
-- const pc = createPlatformClient(
-- { ...loadPlatformConfig({}), clientSecret: 's3cret', networkUrl: platform.origins.network },
-- platform.fetch,
-- )!
-- const identity = createIdentityClient(pc.client)
-- const store = openSqliteStore(':memory:')
-- store.players.upsertMany([
-- char('a0', 'ovn:57', 0),
-- char('a1', 'ovn:57', 1),
-- char('b0', 'ovn:58', 0),
-- char('x0', 'ovn:999', 0),
-- char('g0', 'guesttoken1', 0),
-- ])
--
-- const dry = await migrateLegacyAccounts(store, identity, { dryRun: true })
-- expect(dry).toMatchObject({ scanned: 3, resolved: 2, moved: 3, unresolved: ['ovn:999'] })
-- expect(store.players.findById('a0')?.token).toBe('ovn:57')
--
-- const report = await migrateLegacyAccounts(store, identity)
-- expect(report).toMatchObject({ scanned: 3, resolved: 2, moved: 3, conflicts: 0 })
-- expect(store.players.listByToken(ana!.subject_id).map((p) => p.id)).toEqual(['a0', 'a1'])
-- expect(store.players.listByToken(bo!.subject_id).map((p) => p.id)).toEqual(['b0'])
-- expect(store.players.findById('x0')?.token).toBe('ovn:999')
-- expect(store.players.findById('g0')?.token).toBe('guesttoken1')
--
-- const again = await migrateLegacyAccounts(store, identity)
-- expect(again).toMatchObject({ scanned: 1, resolved: 0, moved: 0, unresolved: ['ovn:999'] })
-- store.close()
-- })
--
-- it('fails loudly without the identity grant (nothing is moved)', async () => {
-- const platform = createMockPlatform({ clients: { games: { secret: 's3cret', grants: [] } } })
-- const pc = createPlatformClient(
-- { ...loadPlatformConfig({}), clientSecret: 's3cret', networkUrl: platform.origins.network },
-- platform.fetch,
-- )!
-- const store = openSqliteStore(':memory:')
-- store.players.upsert(char('a0', 'ovn:57'))
-- await expect(migrateLegacyAccounts(store, createIdentityClient(pc.client))).rejects.toThrow()
-- expect(store.players.findById('a0')?.token).toBe('ovn:57')
-- store.close()
-- })
--})
ddiff --git a/apps/server/src/platform/identityMigration.ts b/apps/server/src/platform/identityMigration.ts
ddeleted file mode 100644
iindex d76558a..0000000
---- a/apps/server/src/platform/identityMigration.ts
++++ /dev/null
@@@ -1,69 +0,0 @@
--/**
-- * Batch adoption of legacy accounts (ADR-0006): every account still keyed by
-- * `ovn:<network user id>` is resolved to its canonical subject through the
-- * Network identity service (capability identity.subject.resolve, audience
-- * openvibe.network, the `games` service token) and moved over.
-- *
-- * Sign-in adopts an account on its own; this pass covers the accounts that
-- * have not signed in since. Idempotent: adopted keys own no characters any
-- * more, so a second run finds nothing to do.
-- */
--import type { PersistenceStore } from '@openvibe/persistence'
--import type { IdentityClient } from 'openvibe-sdk/identity'
--import { isCanonicalSubject } from '../net/networkAuth.js'
--import { LEGACY_NETWORK_PREFIX } from './accounts.js'
--
--export interface MigrationReport {
-- /** Legacy account keys that still owned characters. */
-- scanned: number
-- /** Keys the Network resolved to a subject. */
-- resolved: number
-- /** Characters moved to their subject key. */
-- moved: number
-- /** Characters left under the legacy key because the subject already used that slot. */
-- conflicts: number
-- /** Keys the Network did not know (left untouched). */
-- unresolved: string[]
-- dryRun: boolean
--}
--
--export async function migrateLegacyAccounts(
-- store: PersistenceStore,
-- identity: Pick<IdentityClient, 'resolveBatch'>,
-- opts: { dryRun?: boolean; batchSize?: number; now?: () => number } = {},
--): Promise<MigrationReport> {
-- const dryRun = opts.dryRun ?? false
-- const batchSize = Math.min(Math.max(opts.batchSize ?? 200, 1), 500)
-- const now = opts.now ?? Date.now
-- const keys = store.identity.legacyAccountKeys(LEGACY_NETWORK_PREFIX)
-- const report: MigrationReport = {
-- scanned: keys.length,
-- resolved: 0,
-- moved: 0,
-- conflicts: 0,
-- unresolved: [],
-- dryRun,
-- }
-- for (let i = 0; i < keys.length; i += batchSize) {
-- const batch = keys.slice(i, i + batchSize)
-- const ids = batch.map((k) => k.slice(LEGACY_NETWORK_PREFIX.length))
-- const results = await identity.resolveBatch({ system: 'network', type: 'user', ids })
-- for (const [n, key] of batch.entries()) {
-- const projection = results[ids[n] ?? '']
-- const subjectId = projection?.subject?.id
-- if (!isCanonicalSubject(subjectId)) {
-- report.unresolved.push(key)
-- continue
-- }
-- report.resolved++
-- if (dryRun) {
-- report.moved += store.players.listByToken(key).length
-- continue
-- }
-- const r = store.identity.adoptLegacyAccount(key, subjectId, 'network-batch', now())
-- report.moved += r.moved
-- report.conflicts += r.conflicts
-- }
-- }
-- return report
--}
ddiff --git a/apps/server/src/platform/liveLegacyImport.test.ts b/apps/server/src/platform/liveLegacyImport.test.ts
ddeleted file mode 100644
iindex d8fe145..0000000
---- a/apps/server/src/platform/liveLegacyImport.test.ts
++++ /dev/null
@@@ -1,578 +0,0 @@
--import { spawnSync } from 'node:child_process'
--import { mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
--import { tmpdir } from 'node:os'
--import { dirname, join } from 'node:path'
--import { fileURLToPath } from 'node:url'
--import Database from 'better-sqlite3'
--import { openSqliteStore } from '@openvibe/persistence/sqlite'
--import { createIdentityClient } from 'openvibe-sdk/identity'
--import { createMockPlatform } from 'openvibe-sdk/testing'
--import { afterEach, beforeEach, describe, expect, it } from 'vitest'
--import { loadPlatformConfig } from '../config.js'
--import {
-- applyLegacyImport,
-- archivedCounts,
-- assertSnapshotPath,
-- backupAndVerify,
-- decideSubject,
-- formatReport,
-- isPlaceholderPlayer,
-- networkSubjectLookup,
-- planLegacyImport,
-- refusalFor,
-- subjectMapLookup,
-- type ImportReport,
-- type TableReport,
--} from './liveLegacyImport.js'
--import { createPlatformClient } from './serviceClient.js'
--
--// Canonical subjects (Crockford base32 ULIDs).
--const S1 = 'usr_01KKT9AC60KM7CRTB3WN1Z8P56'
--const S2 = 'usr_01KKBC57W8GDSNVJJDQ15PQCDG'
--const S4A = 'usr_01KKBC57W8GDSNVJJDQ15PQCD4'
--const S4B = 'usr_01KKBC57W8GDSNVJJDQ15PQCD5'
--const S5 = 'usr_01KKBC57W8GDSNVJJDQ15PQCD6'
--const S6 = 'usr_01KKBC57W8GDSNVJJDQ15PQCD8'
--const S7 = 'usr_01KKBC57W8GDSNVJJDQ15PQCD9'
--
--/**
-- * A Live snapshot with Live's real column layout for the tables that matter:
-- * user 1 — Live and the Network agree (S1)
-- * user 2 — no Live record, the Network knows it (S2)
-- * user 3 — nobody knows it (held: no-subject)
-- * user 4 — Live says S4A, the Network S4B (held: subject-conflict)
-- * user 5 — Live says S5, the Network does not know it (held: subject-unconfirmed)
-- * user 6 — a placeholder game_players row Live's profile lookup made (excluded)
-- * user 7 — never acted and has no XP, but has a daily-quest row: played (S7)
-- */
--function makeLive(path: string): void {
-- const db = new Database(path)
-- db.pragma('synchronous = OFF')
-- db.exec(`
-- BEGIN;
-- CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT NOT NULL);
-- CREATE TABLE linked_accounts (
-- id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, service TEXT NOT NULL,
-- service_user_id TEXT NOT NULL, service_username TEXT, linked_at DATETIME, subject_id TEXT,
-- UNIQUE(service, service_user_id)
-- );
-- CREATE TABLE game_players (
-- user_id INTEGER PRIMARY KEY, display_name TEXT, x REAL DEFAULT 4096, y REAL DEFAULT 4096,
-- mining_xp INTEGER DEFAULT 0, woodcut_xp INTEGER DEFAULT 0, hp INTEGER DEFAULT 100,
-- equip_pickaxe TEXT, sleeping_bag_x REAL, created_at DATETIME, last_action DATETIME
-- );
-- CREATE TABLE game_inventory (
-- id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, item_id TEXT NOT NULL,
-- quantity INTEGER DEFAULT 1, UNIQUE(user_id, item_id)
-- );
-- CREATE TABLE game_bank (
-- id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, item_id TEXT NOT NULL,
-- quantity INTEGER DEFAULT 1, UNIQUE(user_id, item_id)
-- );
-- CREATE TABLE game_achievements (
-- user_id INTEGER NOT NULL, achievement_id TEXT NOT NULL, completed_at DATETIME,
-- PRIMARY KEY (user_id, achievement_id)
-- );
-- CREATE TABLE game_recipes (
-- id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, recipe_id TEXT NOT NULL,
-- unlocked_at DATETIME, UNIQUE(user_id, recipe_id)
-- );
-- CREATE TABLE game_structures (
-- id INTEGER PRIMARY KEY AUTOINCREMENT, owner_id INTEGER, type TEXT NOT NULL,
-- tile_x INTEGER NOT NULL, tile_y INTEGER NOT NULL
-- );
-- CREATE TABLE game_world_state (key TEXT PRIMARY KEY, value TEXT);
-- CREATE TABLE game_daily_quest_progress (
-- user_id INTEGER NOT NULL, quest_date TEXT NOT NULL, stat_key TEXT NOT NULL, value REAL,
-- PRIMARY KEY (user_id, quest_date, stat_key)
-- );
-- CREATE TABLE canvas_tiles (
-- x INTEGER NOT NULL, y INTEGER NOT NULL, color_index INTEGER NOT NULL, user_id INTEGER,
-- ip_address TEXT, PRIMARY KEY (x, y)
-- );
-- CREATE TABLE canvas_actions (
-- id INTEGER PRIMARY KEY AUTOINCREMENT, action_type TEXT NOT NULL, x INTEGER, y INTEGER,
-- user_id INTEGER, ip_address TEXT
-- );
-- INSERT INTO users (id, username) VALUES
-- (1, 'ana'), (2, 'bo'), (3, 'cy'), (4, 'di'), (5, 'ed'), (6, 'fi'), (7, 'gu');
-- INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES
-- (1, 'network', '101', '${S1}'),
-- (2, 'network', '102', NULL),
-- (4, 'network', '104', '${S4A}'),
-- (5, 'network', '105', '${S5}'),
-- (3, 'google', 'g-3', NULL);
-- INSERT INTO game_players (user_id, display_name, mining_xp, woodcut_xp, equip_pickaxe, created_at, last_action) VALUES
-- (1, 'Ana', 1200, 30, 'pick_iron', '2026-03-01 10:00:00', '2026-09-01 10:00:00'),
-- (2, 'Bo', 0, 0, NULL, '2026-03-01 10:00:00', '2026-09-02 10:00:00'),
-- (3, 'Cy', 5, 5, NULL, '2026-03-01 10:00:00', '2026-09-03 10:00:00'),
-- (4, 'Di', 7, 0, NULL, '2026-03-01 10:00:00', '2026-09-04 10:00:00'),
-- (5, 'Ed', 9, 0, NULL, '2026-03-01 10:00:00', '2026-09-05 10:00:00'),
-- (6, NULL, 0, 0, NULL, '2026-09-19 17:52:12', '2026-09-19 17:52:12'),
-- (7, NULL, 0, 0, NULL, '2026-03-10 09:00:00', '2026-03-10 09:00:00');
-- INSERT INTO game_inventory (user_id, item_id, quantity) VALUES
-- (1, 'ore_copper', 40), (1, 'pick_iron', 1), (2, 'fish_bass', 3), (3, 'loot_lint', 2),
-- (4, 'gem_ruby', 1);
-- INSERT INTO game_bank (user_id, item_id, quantity) VALUES (1, 'raw_stone', 900), (3, 'raw_stick', 12);
-- INSERT INTO game_achievements (user_id, achievement_id, completed_at) VALUES
-- (1, 'first_gather', '2026-03-10'), (1, 'level_10_any', '2026-03-11'), (5, 'first_blood', '2026-03-12');
-- INSERT INTO game_recipes (user_id, recipe_id) VALUES (2, 'recipe_plank'), (3, 'recipe_iron_bar');
-- INSERT INTO game_structures (owner_id, type, tile_x, tile_y) VALUES (1, 'furnace', 4, 190), (NULL, 'ruin', 5, 5);
-- INSERT INTO game_world_state VALUES ('world_seed', '1510266867');
-- INSERT INTO game_daily_quest_progress VALUES
-- (1, '2026-03-10', 'gathered', 5), (2, '2026-03-10', 'caught', 1), (7, '2026-03-10', 'walked', 3);
-- INSERT INTO canvas_tiles VALUES (0, 0, 3, 1, '203.0.113.9'), (0, 1, 4, 2, '203.0.113.9');
-- INSERT INTO canvas_actions (action_type, x, y, user_id, ip_address) VALUES
-- ('place', 0, 0, 1, '203.0.113.9'), ('blocked', 0, 1, 2, '203.0.113.9'), ('place', 0, 1, 2, '203.0.113.9');
-- COMMIT;
-- `)
-- db.close()
--}
--
--/** The Network's answers for the fixture users. */
--const NETWORK: Record<number, string> = { 1: S1, 2: S2, 4: S4B, 6: S6, 7: S7 }
--const network = subjectMapLookup(NETWORK)
--
--let dir: string
--let livePath: string
--let worldPath: string
--
--beforeEach(() => {
-- dir = mkdtempSync(join(tmpdir(), 'live-legacy-'))
-- livePath = join(dir, 'live-snapshot.db')
-- worldPath = join(dir, 'world.db')
-- makeLive(livePath)
-- openSqliteStore(worldPath).close()
--})
--
--afterEach(() => {
-- rmSync(dir, { recursive: true, force: true })
--})
--
--const table = (r: ImportReport, name: string): TableReport => {
-- const t = r.tables.find((x) => x.table === name)
-- if (!t) throw new Error(`no ${name} in report`)
-- return t
--}
--
--async function plan(
-- opts: { network?: typeof network | null; world?: Database.Database | null } = {},
--) {
-- const live = new Database(livePath, { readonly: true })
-- const world = opts.world === undefined ? new Database(worldPath, { readonly: true }) : opts.world
-- try {
-- return await planLegacyImport(live, world, {
-- network: opts.network === undefined ? network : opts.network,
-- subjectSource: 'test',
-- dryRun: true,
-- })
-- } finally {
-- live.close()
-- if (opts.world === undefined) world?.close()
-- }
--}
--
--function dumpArchive(): unknown[] {
-- const db = new Database(worldPath, { readonly: true })
-- try {
-- return db.prepare('SELECT * FROM legacy_live_rows ORDER BY source_table, source_key').all()
-- } finally {
-- db.close()
-- }
--}
--
--async function applyOnce(): Promise<number> {
-- const live = new Database(livePath, { readonly: true })
-- const world = new Database(worldPath)
-- try {
-- const p = await planLegacyImport(live, world, {
-- network,
-- subjectSource: 'test',
-- dryRun: false,
-- })
-- return applyLegacyImport(world, p, 1_790_000_000_000)
-- } finally {
-- live.close()
-- world.close()
-- }
--}
--
--describe('subject mapping', () => {
-- it('uses the Network as the authority and never guesses', () => {
-- const linked = new Map<number, string[]>([
-- [1, [S1]],
-- [4, [S4A]],
-- [5, [S5]],
-- [6, [S1, S2]],
-- ])
-- const net = new Map<number, string | null>([
-- [1, S1],
-- [2, S2],
-- [3, null],
-- [4, S4B],
-- [5, null],
-- [7, 'usr_not-a-ulid'],
-- ])
-- expect(decideSubject(1, linked, net)).toEqual({ subjectId: S1 })
-- expect(decideSubject(2, linked, net)).toEqual({ subjectId: S2 })
-- expect(decideSubject(3, linked, net)).toEqual({ hold: 'no-subject' })
-- expect(decideSubject(4, linked, net)).toEqual({ hold: 'subject-conflict' })
-- expect(decideSubject(5, linked, net)).toEqual({ hold: 'subject-unconfirmed' })
-- expect(decideSubject(6, linked, net)).toEqual({ hold: 'subject-conflict' })
-- expect(decideSubject(7, linked, net)).toEqual({ hold: 'no-subject' })
-- // Without a Network answer only Live's single recorded subject counts.
-- expect(decideSubject(1, linked, null)).toEqual({ subjectId: S1 })
-- expect(decideSubject(2, linked, null)).toEqual({ hold: 'no-subject' })
-- expect(decideSubject(6, linked, null)).toEqual({ hold: 'subject-conflict' })
-- })
--
-- it('keys every imported row by the mapped subject and holds the rest', async () => {
-- const { report, rows } = await plan()
-- expect(report.users).toEqual({
-- total: 6,
-- mapped: 3,
-- held: { 'no-subject': 1, 'subject-conflict': 1, 'subject-unconfirmed': 1 },
-- })
-- const players = rows.filter((r) => r.table === 'game_players')
-- expect(players.map((r) => [r.liveUserId, r.subjectId])).toEqual([
-- [1, S1],
-- [2, S2],
-- [7, S7],
-- ])
-- expect(new Set(rows.map((r) => r.liveUserId))).toEqual(new Set([1, 2, 7]))
-- const heldUsers = new Set(report.held.map((h) => h.liveUserId))
-- expect(heldUsers).toEqual(new Set([3, 4, 5, null]))
-- expect(report.held).toContainEqual({
-- table: 'game_structures',
-- key: '2',
-- liveUserId: null,
-- reason: 'no-owner',
-- })
-- expect(report.held).toContainEqual({
-- table: 'game_achievements',
-- key: '5/first_blood',
-- liveUserId: 5,
-- reason: 'subject-unconfirmed',
-- })
-- // The payload is the whole source row, canonical JSON.
-- const ana = players.find((r) => r.liveUserId === 1)
-- expect(JSON.parse(ana?.payload ?? '{}')).toEqual({
-- created_at: '2026-03-01 10:00:00',
-- display_name: 'Ana',
-- equip_pickaxe: 'pick_iron',
-- hp: 100,
-- last_action: '2026-09-01 10:00:00',
-- mining_xp: 1200,
-- sleeping_bag_x: null,
-- user_id: 1,
-- woodcut_xp: 30,
-- x: 4096,
-- y: 4096,
-- })
-- })
--
-- it('resolves through the Network identity service with the games principal', async () => {
-- const platform = createMockPlatform({
-- clients: {
-- games: {
-- secret: 's3cret',
-- grants: [{ capability: 'identity.subject.resolve', audience: 'openvibe.network' }],
-- },
-- },
-- users: [
-- { id: 101, username: 'ana', subject_id: S1, legacy: [{ system: 'live', id: 1 }] },
-- { id: 102, username: 'bo', subject_id: S2, legacy: [{ system: 'live', id: 2 }] },
-- ],
-- })
-- const pc = createPlatformClient(
-- { ...loadPlatformConfig({}), clientSecret: 's3cret', networkUrl: platform.origins.network },
-- platform.fetch,
-- )
-- if (!pc) throw new Error('no platform client')
-- const lookup = networkSubjectLookup(createIdentityClient(pc.client))
-- const answers = await lookup([1, 2, 3])
-- expect(answers).toEqual(
-- new Map([
-- [1, S1],
-- [2, S2],
-- [3, null],
-- ]),
-- )
-- const { report } = await plan({ network: lookup })
-- expect(report.users.mapped).toBe(2)
-- })
--
-- it('without a Network source relies on Live’s own record only', async () => {
-- const { report } = await plan({ network: null })
-- // user 1 (S1), user 4 (S4A) and user 5 (S5) have a Live record; 2, 3 and 7 do not.
-- expect(report.users).toEqual({ total: 6, mapped: 3, held: { 'no-subject': 3 } })
-- })
--})
--
--describe('placeholder players', () => {
-- const base = {
-- user_id: 9,
-- mining_xp: 0,
-- total_deaths: 0,
-- battle_wins: 0,
-- equip_axe: null,
-- sleeping_bag_x: null,
-- sprite_skin: 0,
-- hp: 100,
-- chat_color: '#e8e6e3',
-- created_at: '2026-09-19 17:52:12',
-- last_action: '2026-09-19 17:52:12',
-- }
-- it('excludes only rows nobody played', () => {
-- expect(isPlaceholderPlayer(base, false)).toBe(true)
-- expect(isPlaceholderPlayer(base, true)).toBe(false) // owns other legacy rows
-- expect(isPlaceholderPlayer({ ...base, last_action: '2026-09-20 00:00:00' }, false)).toBe(false)
-- expect(isPlaceholderPlayer({ ...base, mining_xp: 1 }, false)).toBe(false)
-- expect(isPlaceholderPlayer({ ...base, total_deaths: 1 }, false)).toBe(false)
-- expect(isPlaceholderPlayer({ ...base, battle_wins: 2 }, false)).toBe(false)
-- expect(isPlaceholderPlayer({ ...base, equip_axe: 'axe_stone' }, false)).toBe(false)
-- expect(isPlaceholderPlayer({ ...base, sleeping_bag_x: 12 }, false)).toBe(false)
-- expect(isPlaceholderPlayer({ ...base, sprite_skin: 3 }, false)).toBe(false)
-- expect(isPlaceholderPlayer({ ...base, created_at: null }, false)).toBe(false)
-- })
--})
--
--describe('reconciliation', () => {
-- it('accounts for every source row: read = imported + held + excluded', async () => {
-- const { report } = await plan()
-- for (const t of report.tables) {
-- expect(t.read, t.table).toBe(t.imported + t.held + t.excluded)
-- expect(t.balanced).toBe(true)
-- }
-- expect(table(report, 'game_players')).toMatchObject({
-- read: 7,
-- imported: 3,
-- written: 3,
-- held: 3,
-- excluded: 1,
-- })
-- expect(report.excludedRows).toEqual({
-- "game_players: placeholder rows Live's profile lookup created for people who never played": 1,
-- })
-- expect(table(report, 'game_inventory')).toMatchObject({ read: 5, imported: 3, held: 2 })
-- expect(table(report, 'game_bank')).toMatchObject({ read: 2, imported: 1, held: 1 })
-- expect(table(report, 'game_achievements')).toMatchObject({ read: 3, imported: 2, held: 1 })
-- expect(table(report, 'game_recipes')).toMatchObject({ read: 2, imported: 1, held: 1 })
-- expect(table(report, 'game_structures')).toMatchObject({ read: 2, imported: 1, held: 1 })
-- expect(table(report, 'game_world_state')).toMatchObject({ read: 1, excluded: 1, imported: 0 })
-- expect(table(report, 'game_daily_quest_progress')).toMatchObject({ read: 3, excluded: 3 })
-- expect(table(report, 'canvas_tiles')).toMatchObject({ read: 2, excluded: 2 })
-- expect(table(report, 'canvas_actions')).toMatchObject({ read: 3, excluded: 3 })
-- // Tables the snapshot does not have are reported, with zero rows.
-- expect(table(report, 'game_fish_collection')).toMatchObject({ missing: true, read: 0 })
-- expect(report.totals).toEqual({
-- read: 30,
-- imported: 11,
-- written: 11,
-- held: 9,
-- excluded: 10,
-- balanced: true,
-- })
-- expect(report.unlisted).toEqual([])
-- expect(report.reconciled).toBe(true)
-- const text = formatReport(report)
-- expect(text).toContain('read 30 = imported 11 + held 9 + excluded 10: balanced')
-- expect(text).toContain(
-- 'live user 3 (no-subject): game_players[3] game_inventory[4] game_bank[2]',
-- )
-- expect(text).toContain('RECONCILED')
-- })
--
-- it('refuses to apply when a legacy table has no recorded decision', async () => {
-- const db = new Database(livePath)
-- db.exec(
-- "CREATE TABLE game_pets (user_id INTEGER, pet TEXT); INSERT INTO game_pets VALUES (1, 'rat');",
-- )
-- db.close()
-- const p = await plan()
-- expect(p.report.unlisted).toEqual([{ table: 'game_pets', rows: 1 }])
-- expect(p.report.reconciled).toBe(false)
-- expect(formatReport(p.report)).toContain('NOT RECONCILED')
-- const world = new Database(worldPath)
-- try {
-- expect(() => applyLegacyImport(world, p, 1)).toThrow(/does not reconcile/)
-- } finally {
-- world.close()
-- }
-- expect(dumpArchive()).toEqual([])
-- })
--})
--
--describe('apply and idempotency', () => {
-- it('writes the plan in one transaction and a re-run changes nothing', async () => {
-- expect(await applyOnce()).toBe(11)
-- const first = dumpArchive()
-- expect(first).toHaveLength(11)
-- const world = new Database(worldPath, { readonly: true })
-- try {
-- expect(Object.fromEntries(archivedCounts(world))).toEqual({
-- game_achievements: 2,
-- game_bank: 1,
-- game_inventory: 3,
-- game_players: 3,
-- game_recipes: 1,
-- game_structures: 1,
-- })
-- } finally {
-- world.close()
-- }
--
-- // Re-run: same counts, nothing written, archive byte-identical.
-- const again = await plan()
-- expect(again.rows).toEqual([])
-- expect(again.report.totals).toEqual({
-- read: 30,
-- imported: 11,
-- written: 0,
-- held: 9,
-- excluded: 10,
-- balanced: true,
-- })
-- expect(await applyOnce()).toBe(0)
-- expect(dumpArchive()).toEqual(first)
-- })
--
-- it('picks up rows whose subject appears later, without touching earlier ones', async () => {
-- await applyOnce()
-- const first = dumpArchive()
-- const later = subjectMapLookup({ ...NETWORK, 3: 'usr_01KKBC57W8GDSNVJJDQ15PQCD7' })
-- const p = await plan({ network: later })
-- expect(p.report.totals.written).toBe(4) // user 3: player, inventory, bank and recipe rows
-- expect(p.rows.every((r) => r.liveUserId === 3)).toBe(true)
-- const world = new Database(worldPath)
-- try {
-- expect(applyLegacyImport(world, p, 2)).toBe(4)
-- } finally {
-- world.close()
-- }
-- const after = dumpArchive()
-- expect(after).toHaveLength(15)
-- for (const row of first) expect(after).toContainEqual(row)
-- })
--
-- it('holds a row that changed since it was imported and never overwrites it', async () => {
-- await applyOnce()
-- const before = dumpArchive()
-- const db = new Database(livePath)
-- db.exec("UPDATE game_inventory SET quantity = 41 WHERE user_id = 1 AND item_id = 'ore_copper'")
-- db.close()
-- const p = await plan()
-- expect(p.report.held).toContainEqual({
-- table: 'game_inventory',
-- key: '1',
-- liveUserId: 1,
-- reason: 'differs-from-earlier-import',
-- })
-- expect(table(p.report, 'game_inventory')).toMatchObject({ read: 5, imported: 2, held: 3 })
-- expect(p.report.reconciled).toBe(true)
-- const world = new Database(worldPath)
-- try {
-- expect(applyLegacyImport(world, p, 3)).toBe(0)
-- } finally {
-- world.close()
-- }
-- expect(dumpArchive()).toEqual(before)
-- })
--
-- it('plans against a world that has no archive table yet (schema 12)', async () => {
-- const legacyWorld = new Database(':memory:')
-- const p = await plan({ world: legacyWorld })
-- expect(p.report.totals.written).toBe(11)
-- legacyWorld.close()
-- })
--})
--
--describe('safety rails', () => {
-- it('refuses --apply without --backup, a reused backup path and a missing subject source', () => {
-- const base = { livePath: 'x.db', apply: true, backupExists: false }
-- expect(refusalFor({ ...base, subjects: 'network' })).toBe('--apply requires --backup <path>')
-- expect(refusalFor({ ...base, backupPath: 'b.db' })).toMatch(/requires --subjects/)
-- expect(
-- refusalFor({ ...base, backupPath: 'b.db', subjects: 'network', backupExists: true }),
-- ).toMatch(/already exists/)
-- expect(refusalFor({ ...base, backupPath: 'b.db', subjects: 'network' })).toBeNull()
-- expect(refusalFor({ apply: false, backupExists: false })).toMatch(/--live/)
-- expect(refusalFor({ livePath: 'x.db', apply: false, backupExists: false })).toBeNull()
-- })
--
-- it('the script exits 2 on --apply without --backup and writes nothing', () => {
-- const script = join(
-- dirname(fileURLToPath(import.meta.url)),
-- '../../scripts/importLiveLegacy.ts',
-- )
-- const before = readFileSync(worldPath)
-- const run = spawnSync(
-- process.execPath,
-- [
-- '--import',
-- 'tsx',
-- script,
-- '--live',
-- livePath,
-- '--world',
-- worldPath,
-- '--apply',
-- '--subjects',
-- 'network',
-- ],
-- { encoding: 'utf8', env: { ...process.env, OV_OAUTH_CLIENT_SECRET: '' } },
-- )
-- expect(run.status).toBe(2)
-- expect(run.stderr).toContain('refused: --apply requires --backup <path>')
-- expect(readFileSync(worldPath).equals(before)).toBe(true)
-- })
--
-- it('the script dry-runs with an exported subject map and prints the reconciliation', () => {
-- const script = join(
-- dirname(fileURLToPath(import.meta.url)),
-- '../../scripts/importLiveLegacy.ts',
-- )
-- const mapPath = join(dir, 'subjects.json')
-- writeFileSync(mapPath, JSON.stringify(NETWORK))
-- const run = spawnSync(
-- process.execPath,
-- ['--import', 'tsx', script, '--live', livePath, '--world', worldPath, '--subjects', mapPath],
-- { encoding: 'utf8' },
-- )
-- expect(run.stderr).toBe('')
-- expect(run.status).toBe(0)
-- expect(run.stdout).toContain('DRY RUN')
-- expect(run.stdout).toContain('read 30 = imported 11 + held 9 + excluded 10: balanced')
-- expect(dumpArchive()).toEqual([])
-- })
--
-- it('backs up world.db and verifies the copy with integrity_check', async () => {
-- const world = new Database(worldPath)
-- const backup = join(dir, 'world-before.db')
-- try {
-- await backupAndVerify(world, backup, (p) => new Database(p, { readonly: true }))
-- expect(existsSync(backup)).toBe(true)
-- // A copy that does not verify is refused.
-- const bad = join(dir, 'world-bad.db')
-- await expect(
-- backupAndVerify(world, bad, (p) => {
-- const buf = readFileSync(p)
-- buf.fill(0x5a, 100, buf.length) // garble every page after the header
-- writeFileSync(p, buf)
-- return new Database(p, { readonly: true })
-- }),
-- ).rejects.toThrow()
-- } finally {
-- world.close()
-- }
-- })
--
-- it('refuses to read the production Live database', () => {
-- const prod = join(dir, 'opt-live')
-- mkdirSync(prod)
-- const file = join(prod, 'live.db')
-- writeFileSync(file, '')
-- expect(() => assertSnapshotPath(file, `${prod}/`)).toThrow(/production database/)
-- expect(() => assertSnapshotPath(livePath, `${prod}/`)).not.toThrow()
-- })
--})
ddiff --git a/apps/server/src/platform/liveLegacyImport.ts b/apps/server/src/platform/liveLegacyImport.ts
ddeleted file mode 100644
iindex b4bc965..0000000
---- a/apps/server/src/platform/liveLegacyImport.ts
++++ /dev/null
@@@ -1,746 +0,0 @@
--/**
-- * Import of OpenVibe.Live's legacy game data (roadmap Wave 12 migration gate,
-- * docs/legacy-import.md).
-- *
-- * Live ran HoboQuest (a 2D tile RPG: `game_*` tables) and a pixel canvas
-- * (`canvas_*`) until both were retired. Scraplandia has no equivalent for
-- * HoboQuest's items, recipes, achievements or skills, so the per-player rows
-- * are kept verbatim in `legacy_live_rows`, keyed by the owner's canonical
-- * Network subject; world-level, expired and derived rows and the canvas are
-- * excluded with a recorded reason. Nothing here touches players or the world.
-- *
-- * Every source row ends in exactly one bucket — imported, held or excluded —
-- * and the report checks `read = imported + held + excluded` per table.
-- * Rows whose Live user has no Network subject (or an ambiguous one) are held
-- * and listed, never guessed. The import is idempotent: rows are keyed by the
-- * source primary key and never overwritten.
-- */
--import { realpathSync } from 'node:fs'
--import type Database from 'better-sqlite3'
--import type { IdentityClient } from 'openvibe-sdk/identity'
--
--/** A Live user's canonical subject: `usr_<ULID>`. Guest subjects never own Live rows. */
--export const USER_SUBJECT = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/
--
--/** The production Live data directory; the importer reads snapshots only. */
--export const LIVE_PRODUCTION_DIR = '/opt/openvibe.live/'
--
--export interface LegacyTableSpec {
-- table: string
-- decision: 'import' | 'exclude'
-- /** Column naming the owning Live user (for exclude tables: evidence of play only). */
-- userColumn?: string
-- /** Primary-key columns of the source row: the idempotency key (import tables only). */
-- keyColumns?: string[]
-- /** Why the table is excluded, or what the archived rows are. */
-- reason: string
-- /** Rows of an import table that are excluded one by one, and why. */
-- excludeRows?: {
-- reason: string
-- test: (row: Row, ownsOtherRows: boolean) => boolean
-- }
--}
--
--type Row = Record<string, unknown>
--
--/**
-- * A `game_players` row nobody played: Live's profile lookup
-- * (`game.getPlayer`, still called by Live's chat profile routes) inserts a
-- * default row for every profile it shows. Such a row was never acted on
-- * (`last_action = created_at`), has no XP, counters, equipment, sleeping bag
-- * or cosmetics, and its user owns no row in any other legacy game table.
-- */
--export function isPlaceholderPlayer(row: Row, ownsOtherRows: boolean): boolean {
-- if (ownsOtherRows) return false
-- if (row.created_at == null || row.last_action !== row.created_at) return false
-- for (const [col, v] of Object.entries(row)) {
-- const zero = v === null || v === 0
-- if (/_xp$|^total_|^battle_(wins|losses)$|^structures_built$|^resources_gathered$/.test(col)) {
-- if (!zero) return false
-- } else if (/^equip_|^sleeping_bag_|^name_effect$|^particle_effect$/.test(col)) {
-- if (v !== null) return false
-- } else if (col === 'sprite_skin' && !zero) {
-- return false
-- }
-- }
-- return true
--}
--
--/** Every legacy game and canvas table in Live, with its decision (docs/legacy-import.md). */
--export const LEGACY_TABLES: readonly LegacyTableSpec[] = [
-- {
-- table: 'game_players',
-- decision: 'import',
-- userColumn: 'user_id',
-- keyColumns: ['user_id'],
-- reason: 'HoboQuest profile: skill XP, combat stats, equipment, cosmetics, lifetime counters',
-- excludeRows: {
-- reason: "placeholder rows Live's profile lookup created for people who never played",
-- test: isPlaceholderPlayer,
-- },
-- },
-- {
-- table: 'game_inventory',
-- decision: 'import',
-- userColumn: 'user_id',
-- keyColumns: ['id'],
-- reason: 'HoboQuest item stacks carried by the player',
-- },
-- {
-- table: 'game_bank',
-- decision: 'import',
-- userColumn: 'user_id',
-- keyColumns: ['id'],
-- reason: 'HoboQuest item stacks in the bank',
-- },
-- {
-- table: 'game_achievements',
-- decision: 'import',
-- userColumn: 'user_id',
-- keyColumns: ['user_id', 'achievement_id'],
-- reason: 'HoboQuest achievements completed',
-- },
-- {
-- table: 'game_recipes',
-- decision: 'import',
-- userColumn: 'user_id',
-- keyColumns: ['id'],
-- reason: 'HoboQuest recipes unlocked',
-- },
-- {
-- table: 'game_fish_collection',
-- decision: 'import',
-- userColumn: 'user_id',
-- keyColumns: ['user_id', 'fish_id'],
-- reason: 'HoboQuest fish album',
-- },
-- {
-- table: 'game_battle_stats',
-- decision: 'import',
-- userColumn: 'user_id',
-- keyColumns: ['user_id'],
-- reason: 'HoboQuest PvP record',
-- },
-- {
-- table: 'game_structures',
-- decision: 'import',
-- userColumn: 'owner_id',
-- keyColumns: ['id'],
-- reason: 'HoboQuest buildings owned (a record of ownership; the tile world is gone)',
-- },
-- {
-- table: 'game_farm_plots',
-- decision: 'import',
-- userColumn: 'user_id',
-- keyColumns: ['id'],
-- reason: 'HoboQuest farm plots',
-- },
-- {
-- table: 'game_world_state',
-- decision: 'exclude',
-- reason:
-- "the seed of HoboQuest's procedural 2D world; Scraplandia's world is its own definition",
-- },
-- {
-- table: 'game_effects',
-- decision: 'exclude',
-- userColumn: 'user_id',
-- reason: 'timed buffs; every one has expired with the game',
-- },
-- {
-- table: 'game_dungeon_runs',
-- decision: 'exclude',
-- userColumn: 'leader_id',
-- reason: 'in-progress dungeon sessions; there is no dungeon to resume',
-- },
-- {
-- table: 'game_leaderboard',
-- decision: 'exclude',
-- userColumn: 'user_id',
-- reason: 'a cache derived from game_players',
-- },
-- {
-- table: 'game_daily_quest_progress',
-- decision: 'exclude',
-- userColumn: 'user_id',
-- reason: 'progress on daily quests of past days (they reset daily and can no longer be claimed)',
-- },
-- {
-- table: 'game_daily_quest_claims',
-- decision: 'exclude',
-- userColumn: 'user_id',
-- reason: 'claims of past daily quests',
-- },
-- {
-- table: 'canvas_tiles',
-- decision: 'exclude',
-- reason: "Live's pixel canvas is not a Games feature; its rows carry IP addresses",
-- },
-- {
-- table: 'canvas_actions',
-- decision: 'exclude',
-- reason: 'the pixel canvas placement and moderation log (with IP addresses)',
-- },
-- { table: 'canvas_settings', decision: 'exclude', reason: 'pixel canvas configuration' },
-- { table: 'canvas_snapshots', decision: 'exclude', reason: 'pixel canvas snapshots' },
-- { table: 'canvas_bans', decision: 'exclude', reason: 'pixel canvas moderation' },
-- { table: 'canvas_region_locks', decision: 'exclude', reason: 'pixel canvas moderation' },
-- { table: 'canvas_user_overrides', decision: 'exclude', reason: 'pixel canvas rate limits' },
--]
--
--/** Tables that belong to the legacy game or canvas: every one must be listed above. */
--const LEGACY_TABLE_PATTERN = /^(game|canvas)_/
--
--export type HoldReason =
-- | 'no-subject'
-- | 'subject-unconfirmed'
-- | 'subject-conflict'
-- | 'no-owner'
-- | 'differs-from-earlier-import'
--
--export const HOLD_REASONS: Record<HoldReason, string> = {
-- 'no-subject': 'the Live user has no Network subject',
-- 'subject-unconfirmed': 'Live records a subject but the Network does not confirm it',
-- 'subject-conflict': 'Live and the Network name different subjects',
-- 'no-owner': 'the row names no Live user',
-- 'differs-from-earlier-import': 'already imported with different content (never overwritten)',
--}
--
--export interface HeldRow {
-- table: string
-- key: string
-- liveUserId: number | null
-- reason: HoldReason
--}
--
--export interface TableReport {
-- table: string
-- decision: 'import' | 'exclude'
-- /** The table does not exist in the snapshot (counts are 0). */
-- missing: boolean
-- read: number
-- /** Rows in world.db after the run (new plus already imported). */
-- imported: number
-- /** Rows this run writes (0 on a re-run). */
-- written: number
-- held: number
-- excluded: number
-- balanced: boolean
--}
--
--export interface ImportReport {
-- dryRun: boolean
-- subjectSource: string
-- tables: TableReport[]
-- totals: Omit<TableReport, 'table' | 'decision' | 'missing'>
-- /** Live users owning importable rows, and how their subjects resolved. */
-- users: { total: number; mapped: number; held: Record<string, number> }
-- held: HeldRow[]
-- /** Rows of import tables excluded one by one, per `table: reason`. */
-- excludedRows: Record<string, number>
-- /** Legacy-looking tables in the snapshot that no decision covers. */
-- unlisted: { table: string; rows: number }[]
-- /** Every table balances and nothing is unlisted. */
-- reconciled: boolean
--}
--
--/** Resolves Live user ids to subjects at the Network; null = the Network does not know the user. */
--export type NetworkSubjectLookup = (liveUserIds: number[]) => Promise<Map<number, string | null>>
--
--/** Lookup through the Network identity service: `(system 'live', type 'user', id)` -> subject. */
--export function networkSubjectLookup(
-- identity: Pick<IdentityClient, 'resolveBatch'>,
--): NetworkSubjectLookup {
-- return async (ids) => {
-- const out = new Map<number, string | null>()
-- if (!ids.length) return out
-- const results = await identity.resolveBatch({ system: 'live', type: 'user', ids })
-- for (const id of ids) out.set(id, results[String(id)]?.subject?.id ?? null)
-- return out
-- }
--}
--
--/** Lookup from a map exported from the Network: `{ "<live user id>": "usr_…" }`. */
--export function subjectMapLookup(map: unknown): NetworkSubjectLookup {
-- if (!map || typeof map !== 'object' || Array.isArray(map)) {
-- throw new Error('expected a JSON object { "<live user id>": "usr_…" }')
-- }
-- const entries = map as Record<string, unknown>
-- return async (ids) =>
-- new Map(
-- ids.map((id) => {
-- const v = entries[String(id)]
-- return [id, typeof v === 'string' ? v : null]
-- }),
-- )
--}
--
--/** A row about to be written. */
--export interface PlannedRow {
-- table: string
-- key: string
-- liveUserId: number
-- subjectId: string
-- payload: string
--}
--
--export interface ImportPlan {
-- report: ImportReport
-- rows: PlannedRow[]
--}
--
--const quoteIdent = (name: string): string => `"${name.replace(/"/g, '""')}"`
--
--function tableExists(db: Database.Database, table: string): boolean {
-- return (
-- db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) !==
-- undefined
-- )
--}
--
--/** Deterministic JSON of a source row (sorted keys) so re-runs compare byte for byte. */
--export function canonicalRow(row: Row): string {
-- const out: Row = {}
-- for (const key of Object.keys(row).sort()) {
-- const v = row[key]
-- if (typeof v === 'bigint') out[key] = Number(v)
-- else if (v === null || typeof v === 'number' || typeof v === 'string') out[key] = v
-- else throw new Error(`unsupported value in column ${key}: ${typeof v}`)
-- }
-- return JSON.stringify(out)
--}
--
--export function rowKey(row: Row, columns: readonly string[]): string {
-- return columns.map((c) => String(row[c])).join('/')
--}
--
--/**
-- * Refuses the production Live database: the importer reads a `.backup`
-- * snapshot, never the file Live has open.
-- */
--export function assertSnapshotPath(path: string, productionDir = LIVE_PRODUCTION_DIR): void {
-- const real = realpathSync(path)
-- if (real.startsWith(productionDir)) {
-- throw new Error(
-- `${path} is Live's production database; take a snapshot first (sqlite3 <db> ".backup <copy>") and pass the copy`,
-- )
-- }
--}
--
--export interface RunOptions {
-- livePath?: string
-- apply: boolean
-- backupPath?: string
-- subjects?: string
-- /** Whether `backupPath` already exists. */
-- backupExists: boolean
--}
--
--/**
-- * Why a run must not start, or null. `--apply` needs a fresh `--backup`
-- * path (a stale file is not a backup of now) and a Network subject source.
-- */
--export function refusalFor(opts: RunOptions): string | null {
-- if (!opts.livePath) return '--live <snapshot of live.db> is required'
-- if (opts.apply && !opts.backupPath) return '--apply requires --backup <path>'
-- if (opts.apply && !opts.subjects) {
-- return '--apply requires --subjects network (or an exported subject map)'
-- }
-- if (opts.backupPath && opts.backupExists) {
-- return `--backup ${opts.backupPath} already exists; choose a new path`
-- }
-- return null
--}
--
--/** Subjects Live itself recorded for its users (linked_accounts, service 'network'). */
--export function liveLinkedSubjects(live: Database.Database): Map<number, string[]> {
-- const out = new Map<number, string[]>()
-- if (!tableExists(live, 'linked_accounts')) return out
-- const cols = live.prepare('PRAGMA table_info(linked_accounts)').all() as { name: string }[]
-- if (!cols.some((c) => c.name === 'subject_id')) return out
-- const rows = live
-- .prepare(
-- "SELECT user_id, subject_id FROM linked_accounts WHERE service = 'network' AND subject_id IS NOT NULL",
-- )
-- .all() as { user_id: number; subject_id: string }[]
-- for (const r of rows) {
-- if (!USER_SUBJECT.test(r.subject_id)) continue
-- const list = out.get(r.user_id) ?? []
-- if (!list.includes(r.subject_id)) list.push(r.subject_id)
-- out.set(r.user_id, list)
-- }
-- return out
--}
--
--/**
-- * The subject a Live user's rows go to, or why they are held. With a Network
-- * answer the Network is the authority and Live's own record must agree; with
-- * none (an informational dry run) Live's single recorded subject is used.
-- */
--export function decideSubject(
-- liveUserId: number,
-- linked: ReadonlyMap<number, string[]>,
-- network: ReadonlyMap<number, string | null> | null,
--): { subjectId: string } | { hold: HoldReason } {
-- const own = linked.get(liveUserId) ?? []
-- if (own.length > 1) return { hold: 'subject-conflict' }
-- if (network) {
-- const answer = network.get(liveUserId) ?? null
-- const n = answer !== null && USER_SUBJECT.test(answer) ? answer : null
-- if (!n) return { hold: own.length ? 'subject-unconfirmed' : 'no-subject' }
-- if (own.length && own[0] !== n) return { hold: 'subject-conflict' }
-- return { subjectId: n }
-- }
-- return own[0] ? { subjectId: own[0] } : { hold: 'no-subject' }
--}
--
--/** Rows already in world.db's archive, keyed `table\u0000key` -> payload. */
--function existingArchive(world: Database.Database | null): Map<string, string> {
-- const out = new Map<string, string>()
-- if (!world || !tableExists(world, 'legacy_live_rows')) return out
-- const rows = world
-- .prepare('SELECT source_table, source_key, payload FROM legacy_live_rows')
-- .all() as { source_table: string; source_key: string; payload: string }[]
-- for (const r of rows) out.set(`${r.source_table}\u0000${r.source_key}`, r.payload)
-- return out
--}
--
--export interface PlanOptions {
-- /** The Network's answer for each Live user id, or null to rely on Live's own record only. */
-- network: NetworkSubjectLookup | null
-- subjectSource: string
-- dryRun: boolean
--}
--
--/**
-- * Reads the Live snapshot and decides every row. `world` may be null (a
-- * fresh world with nothing imported yet) and is only read here.
-- */
--export async function planLegacyImport(
-- live: Database.Database,
-- world: Database.Database | null,
-- opts: PlanOptions,
--): Promise<ImportPlan> {
-- const sourceRows = new Map<string, Row[]>()
-- const excludedCounts = new Map<string, number>()
-- const missing = new Set<string>()
-- /** Live user id -> the legacy tables that hold a row of theirs. */
-- const activity = new Map<number, Set<string>>()
-- const owner = (row: Row, spec: LegacyTableSpec): number | null => {
-- const uid = spec.userColumn ? row[spec.userColumn] : null
-- return typeof uid === 'number' && Number.isInteger(uid) ? uid : null
-- }
--
-- for (const spec of LEGACY_TABLES) {
-- if (!tableExists(live, spec.table)) {
-- missing.add(spec.table)
-- continue
-- }
-- if (spec.userColumn) {
-- const col = quoteIdent(spec.userColumn)
-- const owners = live
-- .prepare(
-- `SELECT DISTINCT ${col} AS uid FROM ${quoteIdent(spec.table)} WHERE ${col} IS NOT NULL`,
-- )
-- .all() as { uid: unknown }[]
-- for (const { uid } of owners) {
-- if (typeof uid !== 'number') continue
-- activity.set(uid, (activity.get(uid) ?? new Set()).add(spec.table))
-- }
-- }
-- if (spec.decision === 'exclude') {
-- const n = live.prepare(`SELECT count(*) AS n FROM ${quoteIdent(spec.table)}`).get() as {
-- n: number
-- }
-- excludedCounts.set(spec.table, n.n)
-- continue
-- }
-- const order = (spec.keyColumns ?? []).map(quoteIdent).join(', ')
-- const rows = live
-- .prepare(`SELECT * FROM ${quoteIdent(spec.table)} ORDER BY ${order}`)
-- .all() as Row[]
-- sourceRows.set(spec.table, rows)
-- }
--
-- // Row-level exclusions, then the users whose rows are left to import.
-- const excludedRow = new Set<Row>()
-- const excludedRows: Record<string, number> = {}
-- const userIds = new Set<number>()
-- for (const spec of LEGACY_TABLES) {
-- for (const row of sourceRows.get(spec.table) ?? []) {
-- const uid = owner(row, spec)
-- const tables = uid === null ? undefined : activity.get(uid)
-- const ownsOtherRows = [...(tables ?? [])].some((t) => t !== spec.table)
-- if (spec.excludeRows?.test(row, ownsOtherRows)) {
-- excludedRow.add(row)
-- const k = `${spec.table}: ${spec.excludeRows.reason}`
-- excludedRows[k] = (excludedRows[k] ?? 0) + 1
-- continue
-- }
-- if (uid !== null) userIds.add(uid)
-- }
-- }
--
-- const listed = new Set(LEGACY_TABLES.map((t) => t.table))
-- const unlisted = (
-- live.prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all() as {
-- name: string
-- }[]
-- )
-- .map((r) => r.name)
-- .filter((name) => LEGACY_TABLE_PATTERN.test(name) && !listed.has(name))
-- .map((table) => ({
-- table,
-- rows: (live.prepare(`SELECT count(*) AS n FROM ${quoteIdent(table)}`).get() as { n: number })
-- .n,
-- }))
--
-- const ids = [...userIds].sort((a, b) => a - b)
-- const linked = liveLinkedSubjects(live)
-- const network = opts.network ? await opts.network(ids) : null
-- const verdicts = new Map(ids.map((id) => [id, decideSubject(id, linked, network)]))
-- const usersHeld: Record<string, number> = {}
-- let mapped = 0
-- for (const v of verdicts.values()) {
-- if ('subjectId' in v) mapped++
-- else usersHeld[v.hold] = (usersHeld[v.hold] ?? 0) + 1
-- }
--
-- const archive = existingArchive(world)
-- const held: HeldRow[] = []
-- const planned: PlannedRow[] = []
-- const tables: TableReport[] = []
--
-- for (const spec of LEGACY_TABLES) {
-- const t: TableReport = {
-- table: spec.table,
-- decision: spec.decision,
-- missing: missing.has(spec.table),
-- read: 0,
-- imported: 0,
-- written: 0,
-- held: 0,
-- excluded: 0,
-- balanced: true,
-- }
-- if (spec.decision === 'exclude') {
-- t.read = excludedCounts.get(spec.table) ?? 0
-- t.excluded = t.read
-- } else {
-- for (const row of sourceRows.get(spec.table) ?? []) {
-- t.read++
-- if (excludedRow.has(row)) {
-- t.excluded++
-- continue
-- }
-- const key = rowKey(row, spec.keyColumns ?? [])
-- const liveUserId = owner(row, spec)
-- const verdict = liveUserId === null ? null : verdicts.get(liveUserId)
-- if (!verdict || !('subjectId' in verdict)) {
-- held.push({
-- table: spec.table,
-- key,
-- liveUserId,
-- reason: verdict ? verdict.hold : 'no-owner',
-- })
-- t.held++
-- continue
-- }
-- const payload = canonicalRow(row)
-- const earlier = archive.get(`${spec.table}\u0000${key}`)
-- if (earlier !== undefined && earlier !== payload) {
-- held.push({ table: spec.table, key, liveUserId, reason: 'differs-from-earlier-import' })
-- t.held++
-- continue
-- }
-- t.imported++
-- if (earlier === undefined) {
-- t.written++
-- planned.push({
-- table: spec.table,
-- key,
-- liveUserId: liveUserId as number,
-- subjectId: verdict.subjectId,
-- payload,
-- })
-- }
-- }
-- }
-- t.balanced = t.read === t.imported + t.held + t.excluded
-- tables.push(t)
-- }
--
-- const totals = tables.reduce(
-- (acc, t) => ({
-- read: acc.read + t.read,
-- imported: acc.imported + t.imported,
-- written: acc.written + t.written,
-- held: acc.held + t.held,
-- excluded: acc.excluded + t.excluded,
-- balanced: acc.balanced && t.balanced,
-- }),
-- { read: 0, imported: 0, written: 0, held: 0, excluded: 0, balanced: true },
-- )
-- totals.balanced =
-- totals.balanced && totals.read === totals.imported + totals.held + totals.excluded
--
-- return {
-- rows: planned,
-- report: {
-- dryRun: opts.dryRun,
-- subjectSource: opts.subjectSource,
-- tables,
-- totals,
-- users: { total: ids.length, mapped, held: usersHeld },
-- held,
-- excludedRows,
-- unlisted,
-- reconciled: totals.balanced && unlisted.length === 0,
-- },
-- }
--}
--
--/**
-- * Writes the planned rows in one transaction. Existing keys are never
-- * touched (ON CONFLICT DO NOTHING); the write count must equal the plan or
-- * the transaction rolls back.
-- */
--export function applyLegacyImport(world: Database.Database, plan: ImportPlan, now: number): number {
-- if (!plan.report.reconciled) throw new Error('refusing to apply: the plan does not reconcile')
-- const insert = world.prepare(`
-- INSERT INTO legacy_live_rows (source_table, source_key, live_user_id, subject_id, payload, imported_at)
-- VALUES (?, ?, ?, ?, ?, ?)
-- ON CONFLICT (source_table, source_key) DO NOTHING
-- `)
-- return world.transaction(() => {
-- let written = 0
-- for (const r of plan.rows) {
-- written += insert.run(r.table, r.key, r.liveUserId, r.subjectId, r.payload, now).changes
-- }
-- if (written !== plan.rows.length) {
-- throw new Error(`wrote ${written} rows, planned ${plan.rows.length}: rolled back`)
-- }
-- return written
-- })()
--}
--
--/** Archived row counts per source table (post-apply verification). */
--export function archivedCounts(world: Database.Database): Map<string, number> {
-- const out = new Map<string, number>()
-- if (!tableExists(world, 'legacy_live_rows')) return out
-- const rows = world
-- .prepare('SELECT source_table, count(*) AS n FROM legacy_live_rows GROUP BY source_table')
-- .all() as { source_table: string; n: number }[]
-- for (const r of rows) out.set(r.source_table, r.n)
-- return out
--}
--
--/**
-- * Snapshot of world.db to `path` taken before anything is written, then
-- * verified: PRAGMA integrity_check must answer `ok` and the snapshot must
-- * hold the same players and archive rows as the source.
-- */
--export async function backupAndVerify(
-- world: Database.Database,
-- path: string,
-- open: (path: string) => Database.Database,
--): Promise<void> {
-- await world.backup(path)
-- const copy = open(path)
-- try {
-- const check = copy.pragma('integrity_check') as { integrity_check: string }[]
-- if (check.length !== 1 || check[0]?.integrity_check !== 'ok') {
-- throw new Error(`backup ${path} failed integrity_check: ${JSON.stringify(check)}`)
-- }
-- const count = (db: Database.Database, table: string): number =>
-- tableExists(db, table)
-- ? (db.prepare(`SELECT count(*) AS n FROM ${quoteIdent(table)}`).get() as { n: number }).n
-- : 0
-- for (const table of ['players', 'world_entities', 'legacy_live_rows']) {
-- if (count(copy, table) !== count(world, table)) {
-- throw new Error(`backup ${path} does not match world.db (${table} row count)`)
-- }
-- }
-- } finally {
-- copy.close()
-- }
--}
--
--/** The reconciliation report as text. */
--export function formatReport(report: ImportReport): string {
-- const lines: string[] = []
-- const mode = report.dryRun ? 'DRY RUN (nothing written)' : 'APPLIED'
-- lines.push(`Live legacy import: ${mode}; subjects from ${report.subjectSource}`)
-- lines.push('')
-- const head = ['table', 'decision', 'read', 'imported', 'written', 'held', 'excluded', 'check']
-- const body = report.tables.map((t) => [
-- t.table + (t.missing ? ' (absent)' : ''),
-- t.decision,
-- String(t.read),
-- String(t.imported),
-- String(t.written),
-- String(t.held),
-- String(t.excluded),
-- t.balanced ? 'ok' : 'MISMATCH',
-- ])
-- const tot = report.totals
-- body.push([
-- 'TOTAL',
-- '',
-- String(tot.read),
-- String(tot.imported),
-- String(tot.written),
-- String(tot.held),
-- String(tot.excluded),
-- tot.balanced ? 'ok' : 'MISMATCH',
-- ])
-- const widths = head.map((h, i) => Math.max(h.length, ...body.map((r) => (r[i] ?? '').length)))
-- const fmt = (r: string[]): string =>
-- r.map((c, i) => (i < 2 ? c.padEnd(widths[i] ?? 0) : c.padStart(widths[i] ?? 0))).join(' ')
-- lines.push(fmt(head))
-- for (const r of body) lines.push(fmt(r))
-- lines.push('')
-- lines.push(
-- `read ${tot.read} = imported ${tot.imported} + held ${tot.held} + excluded ${tot.excluded}: ${
-- tot.balanced ? 'balanced' : 'NOT BALANCED'
-- }`,
-- )
-- const heldUsers = Object.entries(report.users.held)
-- .map(([k, v]) => `${v} ${k}`)
-- .join(', ')
-- lines.push(
-- `Live users owning importable rows: ${report.users.total}; mapped to a subject: ${report.users.mapped}; held: ${heldUsers || 'none'}`,
-- )
-- for (const [what, n] of Object.entries(report.excludedRows)) {
-- lines.push(`Excluded rows of import tables: ${what}: ${n}`)
-- }
-- if (report.unlisted.length) {
-- lines.push('')
-- lines.push('UNLISTED legacy tables (no decision recorded; apply is refused):')
-- for (const u of report.unlisted) lines.push(` ${u.table}: ${u.rows} rows`)
-- }
-- if (report.held.length) {
-- lines.push('')
-- lines.push('Held rows (by Live user; table[source keys]):')
-- const byUser = new Map<string, HeldRow[]>()
-- for (const h of report.held) {
-- const k = `${h.liveUserId ?? 'none'}\u0000${h.reason}`
-- byUser.set(k, [...(byUser.get(k) ?? []), h])
-- }
-- const sorted = [...byUser.entries()].sort(
-- ([a], [b]) => Number(a.split('\u0000')[0]) - Number(b.split('\u0000')[0]),
-- )
-- for (const [k, rows] of sorted) {
-- const [uid, reason] = k.split('\u0000')
-- const perTable = new Map<string, string[]>()
-- for (const r of rows) perTable.set(r.table, [...(perTable.get(r.table) ?? []), r.key])
-- const detail = [...perTable.entries()].map(([t, keys]) => `${t}[${keys.join(',')}]`).join(' ')
-- lines.push(` live user ${uid} (${reason}): ${detail}`)
-- }
-- const reasons = [...new Set(report.held.map((h) => h.reason))]
-- for (const r of reasons) lines.push(` ${r}: ${HOLD_REASONS[r]}`)
-- }
-- lines.push('')
-- lines.push(report.reconciled ? 'RECONCILED' : 'NOT RECONCILED')
-- return lines.join('\n')
--}
ddiff --git a/apps/server/src/platform/staffAuth.ts b/apps/server/src/platform/staffAuth.ts
iindex 3fba55f..9c1875e 100644
---- a/apps/server/src/platform/staffAuth.ts
++++ b/apps/server/src/platform/staffAuth.ts
@@@ -12,7 +12,6 @@ import { createRequire } from 'node:module'
import { verifyUserToken, type UserTokenClaims } from 'openvibe-sdk/auth'
import type { ModActor } from '../mods/registry.js'
import { canEditMap, resolveNetworkUser } from '../net/networkAuth.js'
--import { legacyNetworkKey } from './accounts.js'
export const CAP_MOD_MANAGE = 'games.mod.manage'
export const GAMES_AUDIENCE = 'openvibe.games'
@@@ -95,7 +94,12 @@ export function createStaffAuthorizer(
if (user.subjectId?.startsWith('usr_')) {
return { audit: user.subjectId, subject: { type: 'user', id: user.subjectId } }
}
-- return { audit: legacyNetworkKey(user.id), subject: { type: 'service', id: 'games' } }
++ // A staff session with no canonical user subject (a Network guest, or a
++ // token older than subjects) acts as the games service.
++ return {
++ audit: user.subjectId ?? `network-user:${user.id}`,
++ subject: { type: 'service', id: 'games' },
++ }
}
if (opts.editorKey && sameSecret(token, opts.editorKey)) {
return { audit: 'editor-key', subject: { type: 'service', id: 'games' } }
ddiff --git a/apps/server/src/shutdown.test.ts b/apps/server/src/shutdown.test.ts
iindex 222e226..6040e0b 100644
---- a/apps/server/src/shutdown.test.ts
++++ b/apps/server/src/shutdown.test.ts
@@@ -74,7 +74,6 @@ describe('SIGTERM', () => {
DB_PATH: join(dir, 'world.db'),
MAP_PATH: join(dir, 'map.json'),
STATIC_DIR: '',
-- GUEST_IP_BINDING: 'off',
// The platform side on, pointed at nothing: the outbox exists and has to be stopped.
OV_OAUTH_CLIENT_SECRET: 'test-secret-not-real',
OV_NETWORK_INTERNAL_URL: 'http://127.0.0.1:9',
ddiff --git a/deploy/scripts/deploy.sh b/deploy/scripts/deploy.sh
iindex e47c468..b21f120 100755
---- a/deploy/scripts/deploy.sh
++++ b/deploy/scripts/deploy.sh
@@@ -9,9 +9,9 @@
# DRY_RUN=1 /opt/openvibe.games/deploy/scripts/deploy.sh ovhost plan games
#
# ovhost does the production procedure (pull, pnpm install --frozen-lockfile, pnpm build, restart
--# openvibe-games) with git as the checkout owner instead of sudo git; the tracked dist-types/ a previous build
--# rewrote restored from git before the pull; every workspace package's dependencies checked and better-sqlite3
--# loaded under this Node before the restart; /api/ready polled; on failure the checkout restored, reinstalled
++# openvibe-games) with git as the checkout owner instead of sudo git; every workspace package's
++# dependencies checked and better-sqlite3 loaded under this Node before the restart; /api/ready polled;
++# on failure the checkout restored, reinstalled
# and rebuilt (the build writes the served client) and the unit restarted again (exit 3). Players online are
# reported and reconnect (drain policy report); --wait-idle holds the restart until nobody plays. Do not pull
# by hand first: ovhost would find nothing new (pass --restart if you did).
ddiff --git a/docs/GAMEPLAY_DEVELOPMENT_STATUS.md b/docs/GAMEPLAY_DEVELOPMENT_STATUS.md
ddeleted file mode 100644
iindex e5b9e89..0000000
---- a/docs/GAMEPLAY_DEVELOPMENT_STATUS.md
++++ /dev/null
@@@ -1,418 +0,0 @@
--# Gameplay Development Status
--
--Truthful gap analysis of the ACTUAL implementation, maintained as durable
--state across development sessions. Updated at every milestone.
--
--**Program start:** 2026-08-12, branch `gameplay-next` from `main` @ `2bcd139`.
--
--## Session state (update at every milestone)
--
--```
--Current HEAD: gameplay-next (Stages 1-11 complete; see git log)
--Completed: Stage 1 audit + truthful roadmap + baseline gates
-- Stage 2 constraint toolset (weld/rope/hinge/axis/slider/
-- spring/motor), prop health/repair/destruction,
-- placement ghost, islands + stress smoke
-- Stage 3 container domain, authoritative weather,
-- body temperature + status effects
-- Stage 4 data-driven crops (5), water/fertilizer/
-- sprinklers/tanks, machines (sawmill/mill),
-- generator power, fuel items
-- Stage 5 typed damage pipeline, scrap pistol (hitscan,
-- server-rolled), armor slot + durability, bandage
-- Stage 6 shared SpatialHash + RegionTracker activation
-- Stage 7 NPCs: 4 archetypes, perception/behavior domain,
-- NavigationService seam, full/abstract LOD,
-- persistence, loot, aggro
-- Stage 8 reputation (v10), data-driven markets with
-- restocking stock, tonic production chain
-- Stage 9 world event engine, supply drops migrated,
-- extraction + secured-vs-at-risk loot semantics
-- Stage 10 blueprint discovery (v11), jobs/contracts
-- (deliver + kill) at markets
-- Stage 11 scrap cart vehicle: parts + rigged wheel
-- assembly recognition, force-driven server
-- control, trunk fuel/cargo, free persistence
--Tests: 707 unit tests across 55 files; physics smoke +
-- constraintSmoke (incl. 200-prop stress) OK; sliceTest
-- (17 phases incl. NPC fight, market, extraction,
-- blueprint, contracts, vehicle drive, restart
-- persistence) OK; audit:editor 20 OK
--Schema: sqlite v11 (params v8, armor v9, reputation v10,
-- unlocks+active_job v11); protocol v19
--Remaining: Stage 12-13 items that need real-world profiling first:
-- client world streaming, binary snapshot codec, 200-bot
-- synthetic load harness, richer content web; plus the
-- per-system "Not yet" notes below
--Next exact step: build the synthetic bot harness (protocol clients) and
-- measure tick/bandwidth under load before any codec work
--```
--
--## Architecture facts (verified, load-bearing)
--
--- Server: fixed 30 Hz tick (`config.tickRate`, hardcoded), snapshots every
-- 2nd tick. One `GameServer.step()` drives movement → physgun → physics →
-- sync/settle → crafting → survival (1 Hz) → replication → flush.
--- `@openvibe/gameplay` is engine-free (inventory, crafting, skills, movement,
-- survival, zones, entity store). Verified: no Babylon/DOM/network imports.
--- Physics: `PhysicsWorld` facade (`@openvibe/physics`), Havok adapter via
-- Babylon Physics V2, headless NullEngine on server. Constraint support
-- today: `{type:'weld'}` only → Havok `LOCK` (havokWorld.ts:230).
-- Settle detection is velocity-threshold based (`isSettled`), NOT engine
-- sleep state; settled bodies drop out of snapshots and flush once.
--- Persistence: better-sqlite3 WAL, SCHEMA_VERSION 7, forward-only
-- migrations keyed on `meta.schema_version`. Tables: world_entities,
-- players, constraints, guest_ips, meta. World map itself is a JSON file
-- (MAP_PATH) + map-assets dir, authored by the editor; the world def
-- `openvibeville_v2` ships empty (everything map-authored).
--- Protocol: JSON, zod-validated inbound, versioned. Wire rate limits:
-- 4 KB/message, 120 msgs/s per connection.
--- Entities: `GameEntity` records with optional components (prop, resource,
-- owner, mapSourceId); kinds player/prop/resource in `EntityStore` with
-- kind indexes, plus ONE shared SpatialHash (16 m) for all proximity
-- queries and a RegionTracker (32 m) for activation.
--- Interest management: per-session known-sets, spawn/despawn diffs,
-- snapshots carry only awake relevant bodies. Sleep networking works.
--- Client: prediction + reconciliation for movement; entity interpolation
-- (~130 ms buffer); HUD tabs inventory/crafting/equipment/skills/players;
-- container + merchant panels; contextual E-prompts computed in
-- main.ts `promptFor()`. Weapon modules registry exists but only physgun.
--
--## System-by-system audit
--
--### Physics sandbox (physgun, props)
--
--- **Implemented.** Grab/drag/rotate (view-relative, GMod-feel)/snap/grid/
-- freeze/unfreeze/throw-cap; server-authoritative velocity drive; prop
-- protection (owner + trust, offline TTL cache); zone gating.
--- Files: `apps/server/src/game/physgun.ts`, `gameServer.ts` (handlePhysgun),
-- client `weapons/physgunModule.ts`, `game/interactionController.ts`.
--- Gaps: no precision translation snap while held beyond grid-lock; no
-- player-facing constraint tools (see Constraints).
--
--### Constraints
--
--- **Implemented (Stage 2).** Player-facing rigging tool: weld, rope
-- (anchored max-distance tether, consumes rope item), hinge (limits +
-- friction), axis (free bearing), slider (travel limits), spring
-- (stiffness/damping, consumes scrap), motor (driven hinge, consumes
-- salvaged_motor; construction 5). Two-click UX with selection outline,
-- equipment-panel settings, rope/spring tube visuals with sag; RMB cuts.
--- Server validation: tool, ownership/trust, reach, anchor plausibility
-- (clamped local anchors from world click points), gap, zone build rule,
-- per-entity cap (12), same-type dedup, skill gates, material costs, param
-- space (CONSTRAINT_LIMITS). XP on create.
--- Persistence: ConstraintDto {type, params JSON} (migration v8), restore
-- with unknown-type/dangling pruning, verified across restart in sliceTest.
--- Islands: ConstraintIslands union-find (metrics constraints/
-- constraintIslands; group semantics for later machine/vehicle assembly).
-- Havok sleeps settled structures: 200-prop/370-weld wall settles, idle
-- steps ~1.6 ms, impact wakes ~15 bodies, re-settles (constraintSmoke.ts).
--- Remaining: motor retune UI (setConstraintMotor exists for machines/
-- vehicles later); per-constraint break force.
--
--### Building
--
--- **Implemented (physical philosophy).** Craft -> drop (drop IS placement,
-- clearance raycast) OR ghost placement: translucent preview with zone
-- validity tint, wheel rotation, Shift snapStep grid; `place` message
-- spawns DYNAMIC at pose (server validates slot/capability/range/zone/
-- bounds; physics resolves overlap lies). Physgun position + freeze as
-- before.
--- Free placement always available (drop/toss unchanged).
--
--### Items
--
--- **Implemented, unified ItemDef with capability blocks:** world, food,
-- seed, planter, shop, door, container, weapon (melee), health (max/
-- resistance/repair/destroyLoot — Stage 2), placeable, workstation, tool
-- (physgun/axe/pickaxe/rigging enum). 30 items.
--- Missing capabilities (add with their gameplay): durability, armor, ammo,
-- rangedWeapon, fuel, power*, fluidContainer, machine, processor,
-- growable/harvestable (crop model richer than `seed`), vehiclePart,
-- blueprint, valuable. `ItemStack.meta` exists (merge rules respect it)
-- but nothing writes meta yet.
--
--### Inventory
--
--- **Implemented.** 24 slots (6 hotbar), stacks, move/split/merge/swap,
-- atomic consume, canFit, DTO round-trip. Holster toggle. Server-
-- authoritative with full-inventory resends on change.
--- Missing: equipment slots (armor/clothing), durability/unique-instance
-- metadata use, quick-transfer/sort UX.
--
--### Containers
--
--- **Implemented (Stage 3).** Shared container domain in @openvibe/gameplay
-- (containerAdd/Take/Move/Sort/CanFit/Count/Transfer on the slot-array
-- shape) used by storage boxes, supply crates and — coming — machine
-- inputs/outputs, vehicle trunks, merchant stock. Server ops: open/move
-- (with split count)/sort, range + trust validated, live-pushed to every
-- viewer. Client: click to move, right-click for half, Sort button.
--- container-to-container transfer exists in the domain (containerTransfer)
-- and gets its first server consumer with machines (Stage 4).
--
--### Crafting / workstations
--
--- **Implemented.** 19 recipes, categories, craft-time queue (4 jobs),
-- workstation gating (workbench, burn barrel — id `campfire`), skill gating, output-space
-- aware, XP on completion. Nearby-workstation scan is O(props) per craft.
--
--### Gathering
--
--- **Implemented.** 6 node types, tool gating with bare-hand fallback,
-- yield/power, respawn timers, felled-tree physical trunk spawn, XP.
--
--### Survival
--
--- **Implemented (Stage 3 depth).** Health/hunger/thirst/stamina + body
-- temperature with legible bands: EFFECTIVE temp = ambient (day curve +
-- weather) + burn-barrel warmth − wetness; below 2°C effective the body
-- chills → cold (half stamina regen) → freezing (hp drain); above 32°C it
-- overheats (thirst ×2.5). Timed status framework (wet/cold/freezing/
-- overheated/well_fed/bleeding) with per-status dps + structural rules;
-- big meals apply well_fed (regen boost). Old saves normalized.
--- Death → city respawn keeping inventory (extraction stage will formalize
-- at-risk vs secured semantics). No rest mechanic yet; no clothing
-- insulation yet (armor/equipment arrives Stage 5).
--
--### Farming
--
--- **Implemented (Stage 4).** Data-driven CropDefs (5 materially different
-- crops: regrowing berries, drought-proof mudroot, thirsty mill-bound
-- ration wheat, fiber wirevine for rope, warmth-hungry high-value ember
-- pepper) with growSeconds/stages/waterUse/temperature band/yield/regrow/
-- farming-level gates. PlantState is timestamp-lazy (progress/water/boost/
-- updatedAt advanced in closed form on touch + a 15 s sweep) — plants
-- never tick. Rain waters everything; watering cans (fluidContainer meta)
-- fill from world water or tanks; fertilizer boosts once per growth;
-- sprinklers water planters from nearby tanks; tanks catch rain.
-- Legacy {seedId, plantedAt} plants migrate on restore.
--- Wire: plant {crop, t, water}; client renders stage scale + crop color +
-- thirsty tint; prompts cover water/fertilize/harvest/thirsty.
--
--### Machines / utilities
--
--- **Implemented (Stage 4).** Machine item capability (kind, input/output
-- zones, needsPower) over the shared container; machine recipes
-- (recipe.machine) auto-process unattended with timestamp jobs (park when
-- output is full — never destroy). Sawmill (powered, logs→lumber) and
-- hand grain mill (wheat→flour→burn-barrel flatbread). Power: scrap
-- generator burns fuel items (wood) from its hopper and powers a radius;
-- proximity IS the connection (explicit cables can come with vehicles).
-- Water: tanks + sprinklers as above. Machines/tanks with content refuse
-- pickup. All persisted (machine job, burnUntil, waterAmount).
--- Not yet: machine job progress UI; conveyors; power storage.
--
--### Combat
--
--- **Implemented (Stage 5 foundation).** Generic damage pipeline: typed
-- DamageEvents (blunt/cutting/projectile/explosive/environment/fall) flow
-- through one damagePlayer sink — armor mitigation with durability wear,
-- bleeding on big cutting/projectile wounds (status framework), death
-- handling, hit feedback. Melee (any held item; axes cut) and ranged fire
-- both converge there; props take typed damage via applyPropDamage.
--- Ranged: rangedWeapon capability (damage/range/cadence/spread/ammo/
-- magazine/reload); scrap_pistol + pistol_round content. Client sends pure
-- fire INTENT; the server shoots from the authoritative eye along the
-- authoritative view + server-rolled spread, validates magazine/cadence/
-- reload state (stack meta) and shooter zone; victims are zone-checked
-- too. Tracers broadcast for visuals; R reloads from inventory ammo.
--- Armor: one worn slot (padded_jacket), equip/unequip messages, durability
-- in stack meta, breaks at zero. Medical: bandage (heals + cures
-- bleeding) via the consume path. Both persisted (players.armor, v9).
--- Not yet: projectile ballistics (hitscan only), headshot zones, more
-- weapons, explosive damage sources. Slice covers denial paths + bandage;
-- live pistol fire is unit-tested (fire control) — a full e2e shot needs
-- a cheaper weapon source in the fixture (noted).
--
--### Skills / Progression
--
--- **Implemented (through Stage 10).** 6 skills with XP/levels/recipe
-- gates; constraint types gated by construction level; crops by farming
-- level. Blueprint discovery: recipes flagged blueprint are unknown until
-- a blueprint ITEM is used (persistent players.unlocks, migration v11) —
-- sources today: supply-drop crates (pistol plans) and the market (still
-- schematics, 40 caps). Locked recipes are hidden from the craft menu and
-- rejected server-side (not_unlocked).
--- Jobs/contracts: data-driven JobDefs at markets — deliver (consumes on
-- turn-in) and kill (counts archetype kills) objectives; rewards pay
-- coins, faction reputation, items and skill XP. One active contract,
-- persisted with progress (players.active_job). Offered/accepted/turned
-- in through the trading-post panel.
--
--### Economy
--
--- **Implemented (Stage 8).** Data-driven markets: MarketDef content
-- (faction-owned, sell bundles with finite lazily-restocking stock, buy
-- bundles as sinks) opened via shop props (shop.market). Transactions are
-- server-atomic (proximity, stance, stock, coins, space); stock persists
-- (meta market_<id>); hostile-stance players are refused. Goose's Post
-- replaces the fixed trade sheet and cashes out every production chain —
-- lumber, flatbread, ember peppers, and 18-cap pepper tonic from the new
-- powered Backwoods Still (farm → mill → still → market).
--- Reputation: persistent per-faction scores (players.reputation, v10);
-- trades +2, killing members −15; stanceToward() shifts the faction's
-- base disposition per player — NPC hostility and market access read it.
-- Standing tab in the Tab menu; reputation message on change.
--- Missing: NPC merchants walking the stall (props today), player shops,
-- dynamic pricing, law/guard response to underground goods.
--
--### NPCs / Factions / Reputation
--
--- **NPCs implemented (Stage 7).** Data-driven archetypes (rustjaw_thug,
-- city_warden, townsfolk, dust_hare) composed from profile fields —
-- faction, vitals, perception (view/FOV/hearing), behavior (aggression/
-- flees/wanders/leash/attack), loot, respawn. Pure gameplay domain:
-- perceive() (distance+FOV+injected LOS+hearing) and stepBehavior()
-- (idle/wander/chase/attack/flee/return, deterministic, unit-tested).
--- Server NpcManager with explicit LOD: FULL (active region — perception at
-- 5 Hz staggered, terrain-following kinematic movement every tick, physics
-- capsule, normal replication) vs ABSTRACT (a record: pos/health/respawn;
-- zero per-tick cost). Materialization follows RegionTracker activation;
-- the abstract record IS the persisted state (world_entities kind 'npc').
--- NavigationService boundary (StraightLineNavigation v1; invalidation
-- seam documented for player construction).
--- Combat integration: NPC melee flows through damagePlayer; player melee/
-- bullets damage NPCs; deaths scatter loot-table rolls as physical props;
-- gunshots are heard (sound events). Aggro: damaging any NPC marks the
-- player hostile to DEFENSIVE archetypes (wardens) for 45 s. Factions are
-- content (playerStance) — bandit hostility is data, not code.
--- Client: placeholder tinted body+head visuals, aim/attack targeting,
-- prompts with faction warning, snapshot-interpolated movement.
--- Missing: faction↔faction relations + reputation shifting stances
-- (Stage 8), merchant/job NPCs (Stage 8/10), real navmesh.
--
--### Events / Extraction
--
--- **Implemented (Stage 9).** Generic EventManager: one server-owned
-- lifecycle (scheduled → announced → active → done/cleanup) with typed
-- handlers (create/onAnnounce/onActivate/onTick/onCleanup), one live
-- event per type, cadence scaled by EVENT_INTERVAL_SCALE (tests shrink
-- it). Event state is ephemeral by design (restart cancels — documented).
--- Supply drops migrated onto the engine (same gameplay, no special-cased
-- server code). Extraction: announced beacon sites (content), players
-- hold the circle for 25 s (leaving or dying resets), success SECURES
-- carried valuables (stack meta) and recalls to the city.
--- Secured-vs-at-risk semantics are explicit: `valuable` items
-- (salvage_core, pepper_tonic) drop into a lootable bag on death UNLESS
-- secured; ordinary gear always stays. Server owns every transition; the
-- partition/secure logic is pure and unit-tested.
--- Missing: extraction NPC activity/contest rules, persistent events
-- across restart, event UI beyond announcements (countdown widget).
--
--### Loot
--
--- DROP_LOOT fixed table for crates. No loot-table content schema.
--
--### Vehicles
--
--- **Prototype implemented (Stage 11), grown from item+physics+constraint
-- architecture as required.** cart_chassis (motorized flatbed: thrust
-- power, top speed, 6-slot trunk, health/repair) and cart_wheel items;
-- players physgun the parts together and rig wheels with axis/motor links
-- (the existing rigging tool). Assembly recognition: chassis + >= 2
-- rigged wheels = drivable; a bare chassis refuses (needs_wheels).
--- Driving is server-authoritative force control: intent (WASD) becomes
-- thrust along the chassis' facing plus blended yaw steering, top-speed
-- capped; wheels roll on their bearings; fuel items burn from the trunk
-- exactly like generators (no fuel = coasting). The rider is carried
-- kinematically; the client suppresses prediction while driving
-- (self-state `driving` flag) and adopts authoritative poses.
--- Persistence is free: chassis/wheels are ordinary props, the axles are
-- ordinary constraints — both already persist and restore.
--- Not yet: suspension tuning, multi-seat, headlights, trailer hitches
-- (rope tow actually works today), vehicle-vs-player impact damage.
--
--### World regions / simulation LOD
--
--- **Foundation implemented (Stage 6).** One shared `SpatialHash` (16 m
-- cells) over every entity, maintained by GameWorld (props/resources) and
-- the server (players): replication interest, workstation/shop lookups
-- and sprinkler coupling all query it — no consumer walks all entities by
-- distance anymore. `RegionTracker` (32 m regions) recomputes activation
-- (player ±1 ring) at 1 Hz with metrics (activeRegions/occupiedRegions);
-- `regions.isActive(x, z)` is the seam NPC LOD and event relevance hang
-- off in Stage 7/9. Deactivation unloads nothing (persistence-safe).
--
--### Networking scale
--
--- JSON protocol; per-session snapshot build is O(known entities); interest
-- scan O(total entities × sessions) at 15 Hz. Metrics: bytesOut,
-- messagesOut, snapshotBytes (not in /metrics snapshot output). Fine at
-- current scale; spatial index is the first lever when populations grow.
--
--### Persistence
--
--- **Implemented** for players (token+slot identity, inventory, skills,
-- friends, appearance, stats), world entities (props with container/door/
-- plant state, resources), constraints (welds), guests, meta. Migrations
-- v1-7 forward-only. Slice test verifies restart integrity end-to-end.
--- Missing: per-feature DTOs as systems land (NPC abstract state, events,
-- reputation, blueprints, vehicles, utility networks).
--
--### World clock / environment
--
--- **Implemented (Stage 3).** Server-authoritative EnvironmentState:
-- 20-min day cycle + weighted-transition weather spells (clear/cloudy/
-- rain/storm/fog), ambient temperature curve consumed by survival (and by
-- farming moisture in Stage 4). Persisted in meta; time message carries
-- weather; client renders light dim, fog haze, cloud thickening.
--
--### UI
--
--- HUD: crosshair/prompt/toasts/vitals/hotbar/announce/death; Tab menu
-- (inventory/crafting/equipment/skills/players); container + shop panels.
-- Contextual prompts via promptFor() in main.ts. No map, no jobs/rep tabs.
--
--### Observability
--
--- /metrics JSON + periodic log summary: tick/physics EMA, sessions,
-- entities, awake/settled bodies, bytes/messages out, map layer counts,
-- RSS. Missing: constraint/island counts, NPC LOD tiers, region counts,
-- per-client bandwidth, event/market counters.
--
--### Security posture
--
--- Inbound zod validation, size/rate caps, per-action range/zone/ownership
-- checks, one live session per character, guest IP binding. Slice test
-- covers protection/trust. No dedicated hostile-input test suite per
-- action (added per-stage from Stage 2 on).
--
--### Map editor (do not break)
--
--- Mature subsystem: collaborative editing, locks, terrain sculpt/paint,
-- statics, zones, lights, nodes/props with provenance-based live
-- reconciliation into the running game. Gates: `pnpm audit:editor`
-- (vitest arch audit), `pnpm test:editor` (browser E2E, sequential).
--
--## Baseline quality gates (2026-08-12, start of program)
--
--| Gate | Result |
--| -------------------------- | ------------------------------------------------------ |
--| pnpm typecheck | clean |
--| pnpm lint | clean |
--| pnpm test | 47 files, 634 tests, all pass |
--| pnpm build | clean (client bundle 285 kB main + 2 MB babylon chunk) |
--| pnpm format:check | clean |
--| sliceTest.ts | pass (all phases incl. restart persistence) |
--| audit:editor / test:editor | run at editor-touching changes |
--
--## Dependency notes for upcoming stages
--
--- Constraint tool (Stage 2) touches: physics ConstraintDesc union, GameWorld
-- weld records → generalize to ConstraintRecord, ConstraintDto.type,
-- protocol weld messages → constraint messages, client tool-mode seam
-- (weapons registry currently panel-only; InteractionController hardcodes
-- physgun branches — needs delegation hooks), prompt seam in main.ts.
--- Prop health (Stage 2) is prerequisite for ranged combat vs structures
-- (Stage 5) and NPC melee targets (Stage 7).
--- Container domain module (Stage 3) is prerequisite for machines
-- (Stage 4), vehicle trunks (Stage 11), merchant stock (Stage 8).
--- Environment state (Stage 3) feeds farming moisture (Stage 4), NPC
-- perception modifiers (Stage 7), solar power (later).
--- Spatial hash (Stage 6) must replace interest scan AND workstation/shop
-- proximity scans; NPC perception (Stage 7) queries it.
--- Supply-drop migration (Stage 9) depends on the event engine; extraction
-- secured-loot semantics interact with death/inventory rules (define
-- at-risk vs secured explicitly there).
ddiff --git a/docs/adr/0006-canonical-subjects-and-platform-boundary.md b/docs/adr/0006-canonical-subjects-and-platform-boundary.md
iindex b99728b..6029a11 100644
---- a/docs/adr/0006-canonical-subjects-and-platform-boundary.md
++++ b/docs/adr/0006-canonical-subjects-and-platform-boundary.md
@@@ -1,6 +1,7 @@
# ADR-0006: Canonical Network subjects, and the platform boundary
--**Status:** accepted · **Date:** 2026-09-22 · partly supersedes ADR-0004
++**Status:** accepted, identity adoption superseded by ADR-0007 (the `ovn:` adoption, `migrateIdentity.ts` and
++`identity_legacy_map` described in 1. are deleted) · **Date:** 2026-09-22 · partly supersedes ADR-0004
## Decision
ddiff --git a/docs/adr/0007-rewrite-the-moddable-base.md b/docs/adr/0007-rewrite-the-moddable-base.md
nnew file mode 100644
iindex 0000000..fe2227a
---- /dev/null
++++ b/docs/adr/0007-rewrite-the-moddable-base.md
@@@ -0,0 +1,83 @@
++# ADR-0007: Rewrite the moddable base
++
++**Status:** accepted · **Date:** 2026-09-28 · supersedes ADR-0003, ADR-0004, the identity-adoption part of ADR-0006
++
++## Context
++
++No map, player or progress on openvibe.games is worth keeping (owner, 2026-09-28): nothing is restored and nothing
++old stays compatible. The domain code is strong (Quake-style movement, the physics seam, constraints and islands,
++engine-free gameplay modules, the zod content registry, the map document/diff machinery, the editor core, provenance
++reconciliation, the platform adapters). The shell around it is not: a 2,803-line `gameServer.ts`, synchronous SQLite,
++a renderer inside the authoritative server, world content hard-coded in client render code, a mod API of three calls,
++no mobile path, and pins that lag the platform.
++
++## Decisions
++
++1. **The base is the product; Scraplandia is its first pack.** The engine, systems, editor and mod runtime are the
++ base. Scraplandia (survival, crafting, farming, extraction) is a content pack the base loads by default, and the
++ gameplay modules become systems a pack enables. A second, small game (a round-based physics party game) is built
++ on the same base in M5 to prove it is general.
++2. **Physics: Rapier behind the `PhysicsWorld` seam.** `@dimforge/rapier3d-deterministic-compat` on the server and in
++ the client's prediction world. It removes `@babylonjs/core` and the NullEngine scene from the server, runs in a
++ worker, and has one implementation on both sides (the ADR-0003 rule stays). Havok and `packages/physics/src/havok`
++ are deleted. A mock `PhysicsWorld` and seam tests come first, so the swap is tested, not assumed.
++3. **Determinism is scoped to movement.** The movement module is bit-identical for the same inputs (what prediction
++ needs); everything else is server-trust. The global determinism claim is dropped.
++4. **World model: places and instances.** A place is a map plus its persistent state; an instance is one authoritative
++ process simulating a place. M1–M3 run one instance per place; M4 adds several instances per place, a registry in
++ PostgreSQL, leases in Valkey, and transfer by a signed handoff token over OpenVibe.Events.
++5. **Persistence: PostgreSQL 18 + Valkey 9 through `openvibe-sdk`** (`db`, `events` outbox/inbox, `valkey`, `limits`).
++ A new schema from scratch (places, instances, characters, inventories, entities, constraints, map revisions, mods,
++ the platform tables); no SQLite, no schema-version history, no migration of old rows. **The tick never awaits I/O:**
++ the tick takes a copy-on-write snapshot of dirty rows and a write-behind flusher persists it in one transaction,
++ one flush in flight per instance; a failed flush keeps the rows dirty and is retried, and shutdown drains it.
++6. **Mods in three tiers.**
++ - T0, declarative packs (`games-content@2`): a pack adds definitions (items, recipes, crops, NPCs, zones, maps),
++ not only placements. A runtime pack loader, content versioning, and a def-set handshake so client and server
++ agree on the active definitions.
++ - T1, scripted server mods (`games-quickjs@1`): QuickJS (`quickjs-emscripten`) in-process, with the manifest's
++ `cpuMs`/`memoryMb` enforced by the interrupt handler and the memory limit, capability-checked bindings, seeded
++ randomness, hot reload, and sandbox-escape tests in CI. The runtime adapter interface is the one OpenVibe.Run
++ implements later (plan ADR-036); Games does not wait for it.
++ - T2, client mods in a Worker with no DOM; assets through Media.
++ Capability API v2: `events.subscribe/publish`, `world.entities.read/write`, `players.read`, per-mod key/value in
++ PostgreSQL, `assets.read`, `ui.*`. Money only through Billing/VIP hooks, never a balance. Manifests follow the
++ platform's `[email protected]`; the local copy of that schema is deleted and the test reads the installed
++ contracts package.
++7. **Phones are a real target.** Input is intents (move vector, look delta, named actions), not keys, from M1; touch
++ controls ship with M2; quality tiers, hardware scaling, KTX2 textures and the bundle budget (initial JS ≤ 2.5 MB,
++ largest chunk ≤ 400 KB, editor split out) in M6.
++8. **Netcode.** Authoritative server, 30 Hz tick, 15 Hz snapshots, spatial-hash interest, sleep-aware snapshots and
++ input replay all stay. The WebSocket authenticates at the upgrade (no `hello` identity). Interest becomes region
++ subscriptions with hysteresis in M4; lag compensation for hitscan only. JSON stays until a measurement says
++ otherwise.
++9. **Trust model: a co-operative sandbox.** Server authority, clamped input, per-subject limits in Valkey, movement
++ sanity checks, signed asset manifests, replay telemetry: detection and revocation, no competitive-integrity claim.
++10. **Platform game services live in a new service, OpenVibe.Play**: achievements, stats and leaderboards, player
++ inventories across games, presence and lobbies. Each game is a Play title with its own principal and `play.*`
++ grants scoped to that title. Games is the first consumer (M5); it never keeps its own copy.
++11. **Terrain stays heightfield patches** (the editor and `mapFileV2` keep them).
++12. **Pin drift is a defect.** Contracts and SDK pins move to current; CI fails when the installed contracts package
++ rejects a manifest Games accepts, or the other way round.
++
++## Deleted
++
++The Live legacy import (`platform/liveLegacyImport.ts`, `scripts/importLiveLegacy.ts`, `legacy_live_rows`,
++`docs/legacy-import.md`), identity adoption (`scripts/migrateIdentity.ts`, `identity_legacy_map`, `ovn:` keys,
++`guest_ips`), the `/api/texture`, `/game` and `/canvas` aliases, map v1 migration (`migrateV1ToV2`,
++`MAIN_TERRAIN_MIGRATED_ID`, the client's `migrateLegacyMix`/`legacyLayer`; `hq_token`/`hq_reload_ts` became
++`openvibe.token`/`openvibe.reload_ts`, so an old guest cookie is not read), `EDITOR_KEY`,
++`packages/persistence` (SQLite), Havok, the city geometry in `sceneSetup.ts` (it becomes the Scraplandia pack's map),
++`editor/catalog.ts` def ids (read from the registry), committed `dist-types/`, `docs/GAMEPLAY_DEVELOPMENT_STATUS.md`.
++
++## Build order
++
++- **M1: same game, new foundation.** Deletions above; `gameServer.ts` split into systems; PostgreSQL + Valkey
++ persistence with the write-behind flusher; Rapier; intent input; authenticated upgrade; pins current.
++- **M2: moddable content.** Pack loader, `games-content@2`, def-set handshake, Scraplandia as a pack, touch controls.
++- **M3: scripted mods.** QuickJS runtime, capability API v2, budgets enforced, hot reload, escape tests.
++- **M4: instances and scale.** Several instances per place, registry and leases, transfer, region interest, load tests.
++- **M5: platform game services.** OpenVibe.Play with Games as first consumer; the second game on the same base.
++- **M6: client floor.** Quality tiers, KTX2 and atlases, baked icons, bundle budget, editor E2E in CI.
++
++Each milestone is playable end to end and keeps the slice harness green.
ddiff --git a/docs/legacy-import.md b/docs/legacy-import.md
ddeleted file mode 100644
iindex 190c30f..0000000
---- a/docs/legacy-import.md
++++ /dev/null
@@@ -1,395 +0,0 @@
--# Live legacy game data: import, map or exclude
--
--Roadmap Wave 12 migration gate: OpenVibe.Live's legacy HoboQuest and canvas
--data is either imported into OpenVibe.Games or excluded with a recorded
--reason, and every source row is reconciled.
--
--**Status (2026-09-23):** decisions recorded, importer and tests in place,
--dry run done on copies of both production databases (below).
--**Not applied to production.** The owner runs the apply after review
--([host commands](#production-run)).
--
--- Importer: [`apps/server/scripts/importLiveLegacy.ts`](../apps/server/scripts/importLiveLegacy.ts)
-- (CLI) and [`apps/server/src/platform/liveLegacyImport.ts`](../apps/server/src/platform/liveLegacyImport.ts)
-- (decisions, mapping, reconciliation).
--- Tests: [`apps/server/src/platform/liveLegacyImport.test.ts`](../apps/server/src/platform/liveLegacyImport.test.ts).
--- Destination: `legacy_live_rows` in `world.db` (schema 13).
--
--## What the legacy data is
--
--Until it was retired, Live ran two things that Games is now supposed to own:
--
--- **HoboQuest**: a 2D, tile-based RPG with a procedural world (its seed
-- is in `game_world_state`). It had eight skills (mining, fishing, woodcut,
-- farming, combat, crafting, agility, smithing) with an uncapped curve
-- (`level = floor(sqrt(xp / 25)) + 1`). Players hold 253 distinct item ids
-- (ores, fish, loot, gems, tools, rods, hats, cosmetic effects, joke items)
-- and 128 unlocked recipe ids. It also had achievements, a bank, dungeons,
-- daily quests and tile buildings. The game was removed from Live (`server/index.js`: "Game &
-- Canvas — migrated to openvibe.games"), but the tables stayed and one path
-- still writes to them. [See below](#live-still-creates-game_players-rows).
--- **Canvas**: an r/place-style 512×512 pixel board with placement
-- cooldowns and moderation. Production data covers one day (2026-03-15), three
-- accounts, 429 placements and 417 blocked attempts. Live's `/canvas` still
-- 301-redirects to `https://openvibe.games/canvas`, which serves the Games
-- portal: there is no canvas in Games.
--
--Scraplandia (Games) is a different game: a 3D physics sandbox with six
--skills (woodcutting, mining, scavenging, farming, crafting, construction,
--capped at 50 with `xpToNextLevel = 60 · level^1.45`, 347,327 XP to reach 50),
--59 item definitions, blueprint unlocks by recipe id, per-account character
--slots, and no achievement, fishing, bank, dungeon or daily-quest system.
--
--Measured against the production copy:
--
--- 1 of the 253 distinct HoboQuest item ids in inventories and banks exists
-- in Scraplandia's content: `fertilizer`, a name collision (a HoboQuest
-- farm consumable versus a Scraplandia crop input). It is not the same item.
--- 0 of the 128 HoboQuest recipe ids exist as a Scraplandia recipe or item.
--- 22 of the 70 `game_players` rows have any XP; 13 have XP in one of the four
-- skills whose names also exist in Scraplandia (mining, woodcut, farming,
-- crafting). The largest is 840,227 mining XP (HoboQuest level 184): copied
-- 1:1 it would be 2.4× the XP needed to max Scraplandia mining.
--
--## Decisions
--
--Three outcomes are possible for a table:
--
--- **import**: the row is copied, whole and unchanged, into
-- `legacy_live_rows` under its owner's Network subject;
--- **map**: the row is translated into Scraplandia's gameplay model
-- (characters, inventories, skills, unlocks);
--- **exclude**: the row is not copied, and the reason is recorded here and in
-- `LEGACY_TABLES`.
--
--**No table is mapped.** Every per-player HoboQuest table is imported as an
--archive, and nothing is written into Scraplandia's gameplay state:
--
--1. Items and recipes have no counterpart (above). A map would drop all but a
-- name collision.
--2. Skill XP comes from a different game with a different, uncapped curve.
-- Scraplandia levels unlock recipes and interactions. A raw copy would max
-- the top players on day one. A rescaled copy would still give Scraplandia
-- progression for play that did not happen in Scraplandia. Either way this
-- is a game-design decision, not a migration step.
--3. A Scraplandia character is created by its player (name, slot,
-- appearance) under their account key. Making characters for 11 accounts
-- that may never have opened Scraplandia would put characters they did
-- not create into their slots.
--
--The archive keeps every column of every imported row, keyed by subject. A
--later feature (a "HoboQuest veteran" cosmetic, a legacy profile tab, a
--tuned skill grant) can read it without going back to Live. `ADR-0006`
--applies: rows are keyed by the canonical `usr_…` subject, never by Live's
--integer id (the Live id is stored beside it for audit).
--
--| Live table | Rows | What it is | Equivalent in Games | Decision | Reason |
--| --------------------------------------------------------------------------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
--| `game_players` | 70 | HoboQuest profile: tile position, 8 skill XPs, HP/attack/defense/stamina, 6 equipment slots, sleeping bag, cosmetics, lifetime counters | Partial: `players` has skills (4 of 8 skill names overlap), armor slot, appearance; no stamina, equipment slots, counters | import (archive) | Different game, curve and cap; no character to attach to (see above). **47 placeholder rows are excluded row by row** ([below](#live-still-creates-game_players-rows)) |
--| `game_inventory` | 399 | Carried item stacks `(user, item_id, quantity)` | `players.inventory` (slot-based, Scraplandia item ids) | import (archive) | 252 of 253 item ids have no Scraplandia item; the remaining one is a name collision |
--| `game_bank` | 115 | Banked item stacks (safe on death) | None (no bank; storage boxes are world props) | import (archive) | As above |
--| `game_achievements` | 122 | Completed achievements (26 ids) | None: Scraplandia has no achievement system (README) | import (archive) | Nothing to map onto; kept for a future legacy badge |
--| `game_recipes` | 140 | Unlocked recipes | `players.unlocks` (blueprint recipe ids) | import (archive) | 0 of 128 recipe ids exist in Scraplandia |
--| `game_fish_collection` | 13 | Fish album (times caught, best weight, first catch) | None (no fishing) | import (archive) | Nothing to map onto |
--| `game_battle_stats` | 3 | PvP record (wins, losses, streaks, stolen/lost) | None persisted (combat exists, no record) | import (archive) | Nothing to map onto |
--| `game_structures` | 1 | A furnace on tile (4, 190) | World props are 3D physics entities in a different world | import (archive) | No position or object mapping exists; kept as an ownership record |
--| `game_farm_plots` | 0 | Tile farm plots | Planter boxes (world props) | import (archive) | Empty; listed so a non-empty snapshot would still be handled |
--| `game_world_state` | 1 | Seed of HoboQuest's procedural world | Scraplandia's world is its own definition (`world_id` in `meta`) | exclude | Describes a world that no longer exists |
--| `game_daily_quest_progress` | 131 | Per-day quest counters, 2026-03-10 to 2026-03-17 | None | exclude | Daily quests reset each day; these days' quests can no longer be claimed |
--| `game_daily_quest_claims` | 0 | Claimed daily quests | None | exclude | Empty; past days only |
--| `game_effects` | 0 | Timed buffs | Status effects are runtime state | exclude | Empty; any buff would have expired |
--| `game_dungeon_runs` | 0 | Dungeon sessions | None | exclude | Empty; session state |
--| `game_leaderboard` | 0 | Leaderboard cache | None | exclude | Empty; derived from `game_players` |
--| `canvas_tiles` | 375 | Current pixel of each painted tile, with painter id, username and **IP address** | None | exclude | Canvas is not a Games feature. The rows carry IP addresses |
--| `canvas_actions` | 846 | Placement and "blocked" log, with IP addresses | None | exclude | As above |
--| `canvas_settings` | 8 | Board size, palette, cooldowns | None | exclude | Configuration of the excluded board |
--| `canvas_snapshots`, `canvas_bans`, `canvas_region_locks`, `canvas_user_overrides` | 0 | Canvas snapshots and moderation | None | exclude | Empty |
--
--**Canvas.** The pixel board belongs with Live's community features, or with a
--future product, not with Scraplandia. Its data stays in Live's database and
--backups until the owner decides whether Live drops it. The excluded rows are
--counted in every reconciliation. Follow-up for Live: the `/canvas` redirect
--now lands on the Games portal, so it should be removed or pointed at a
--notice.
--
--**Legacy tables named in the gate:** `game_players` 70, `game_inventory` 399,
--`game_bank` 115, `game_achievements` 122, `game_recipes` 140, `canvas_tiles`
--375, `canvas_actions` 846. These match the audit. The importer also covers the
--other game and canvas tables. If a `game_*` or `canvas_*` table appears
--without a decision, the run is marked not reconciled and `--apply` refuses.
--
--### What the archive loses
--
--- **Nothing inside an imported row.** The payload is the full source row as
-- canonical JSON (sorted keys). Integers and reals stay JSON numbers, and
-- `DATETIME` values stay the text Live stored (UTC, `YYYY-MM-DD HH:MM:SS`).
--- **The link to Live's `users` row.** The owner is re-keyed to the Network
-- subject. The Live user id is kept in `live_user_id`, and the payload's own
-- `user_id`/`owner_id` column still holds it.
--- **Held rows are not imported** (listed below) until their owner gets a
-- Network subject. Excluded tables, and the placeholder `game_players`
-- rows, are not imported at all. The placeholder rows hold only defaults.
--- **Nothing in Scraplandia changes.** No character, inventory, skill or
-- unlock is created or modified. `live_user_id` and the payload are kept for
-- whoever designs a legacy grant.
--
--### Live still creates `game_players` rows
--
--Live's chat profile routes (`server/chat/routes.js` and
--`server/chat/live-context-routes.js`) still call `game.getPlayer(userId)` to
--show game levels on a profile. That function inserts a default row, at the
--outpost spawn, for any user who has none. `game_players` therefore grows
--with every new profile Live shows. Its newest row is from 2026-09-19, and 47
--of the 70 rows are such placeholders.
--
--The importer excludes a `game_players` row one by one
--(`isPlaceholderPlayer`) when all of these hold:
--
--- it was never acted on (`last_action = created_at`);
--- every XP, `total_*`, `battle_*`, `structures_built` and
-- `resources_gathered` column is 0;
--- it has no equipment, sleeping bag, name/particle effect or sprite skin;
--- its user owns no row in any other legacy game table.
--
--Rows of real players are not affected. The exclusion shows in the report as
--`excluded` under `game_players`. A placeholder created after the dry run is
--excluded the same way, so the counts still reconcile.
--
--Follow-up for Live, not done here: make that profile lookup read-only, so
--the table stops growing before Live drops it.
--
--## Identity mapping
--
--A row's owner is its Live user id (`user_id`, or `owner_id` for
--`game_structures`). The Live user id becomes a subject like this:
--
--1. **The Network is the authority.** The importer calls the Network identity
-- service (`POST /internal/identity/resolve-batch` with
-- `{ system: 'live', type: 'user', ids }`) as the `games` service principal,
-- which already holds the grant `identity.subject.resolve` on
-- `openvibe.network`. The Network answers from its `identity_legacy_map`, which
-- holds 327 `live/user` rows.
--2. **Live's own record is a cross-check.** Live stores the subject a signed-in
-- user's token carried in `linked_accounts(service = 'network').subject_id`.
-- On production only 1 of 327 Network links has it filled, so on its own it
-- is not a usable source.
--3. A row is **held**, never guessed, when:
-- - the Network does not know the Live user (`no-subject`);
-- - Live has a subject that the Network does not confirm (`subject-unconfirmed`);
-- - Live and the Network disagree, or Live has two (`subject-conflict`);
-- - the row names no user (`no-owner`);
-- - the row was already imported with different content
-- (`differs-from-earlier-import`: an import is never overwritten).
--
-- The run lists held rows by Live user, with their source keys.
--
--For an offline dry run, `--subjects <file>` takes the same answers as a JSON
--object `{ "<live user id>": "usr_…" }` exported from the Network (command
--below). `--apply` requires `--subjects network` or such a map.
--
--On 2026-09-23 both sources were checked on the host for the 70 Live users
--that own importable rows. The identity service (games principal) and the
--exported map agreed on all 70: 49 resolved, 21 unknown, 0 disagreements.
--After the placeholder rows are excluded, 23 Live users still own
--importable rows: 11 resolve and 12 are held.
--
--The 12 held users are Live accounts created 2026-03-10 to 2026-03-15 and
--last seen the day they were created. They have no linked account of any
--kind and no password, so nobody can sign in to them any more (Live is
--SSO-only). None of them appears in the Network's `live` or `hobostreamer`
--legacy maps. If the Network later records a verified `live/user/<id>`
--mapping for one of them (`POST /internal/identity/legacy-map`), re-run the
--importer. The test "picks up rows whose subject appears later" checks that
--only that user's rows are written.
--
--Re-checked 2026-09-24 (dry run against a fresh Live snapshot, subjects from the
--Network identity service): unchanged. 616 rows import (already in
--`legacy_live_rows`), 200 rows of the same 12 users stay held (`no-subject`),
--1,408 are excluded, and the totals balance. These 12 are the standing
--import-hold. `players.subject_id` needs no backfill: the only Network-keyed
--player (`ovn:1`) moved to its subject on 2026-09-23 (`identity_legacy_map`),
--and the other 27 rows are device-token guests and deploy probes, which have
--no subject until the guest signs in.
--
--Re-checked 2026-09-26 (roadmap WS-M task 8): the 12 still have no linked
--account of any kind, and their stored password fields are disabled markers
--(`!`/`*`, not a hash anything can match); Live's password login is gone (SSO
--only). Nobody can reach these accounts, so the 200 rows stay a documented
--import-hold, never deleted (plan rule 12). They are imported the day the
--Network records a verified `live/user/<id>` mapping for one of them.
--
--## The archive table (`world.db`, schema 13)
--
--```sql
--CREATE TABLE legacy_live_rows (
-- source_table TEXT NOT NULL, -- e.g. game_inventory
-- source_key TEXT NOT NULL, -- the source primary key, '/'-joined (e.g. '2107', '4/first_blood')
-- live_user_id INTEGER NOT NULL,
-- subject_id TEXT NOT NULL, -- usr_…
-- payload TEXT NOT NULL, -- the whole source row, canonical JSON
-- imported_at INTEGER NOT NULL, -- ms epoch
-- PRIMARY KEY (source_table, source_key)
--);
--CREATE INDEX idx_legacy_live_rows_subject ON legacy_live_rows (subject_id);
--```
--
--The table is added by the forward-only migration 12 → 13 in
--`packages/persistence/src/sqlite/sqliteStore.ts`. Only the importer writes
--it, and nothing in the simulation reads it.
--
--**Idempotency.** Rows are keyed by `(source_table, source_key)` and written
--with `ON CONFLICT DO NOTHING` inside one transaction. The number of rows
--written must equal the plan or the transaction rolls back. A re-run of the
--same snapshot writes 0 rows and reports the same `imported` counts.
--
--## Dry run on production copies (2026-09-23)
--
--Sources:
--
--- `live.db` and `world.db` were copied on the host with
-- `sudo sqlite3 -readonly <db> ".backup /tmp/…"` and then copied to a
-- workstation. Both copies passed `integrity_check`.
--- The subject map was exported read-only from the Network's
-- `identity_legacy_map`.
--
--Command: `node --import tsx apps/server/scripts/importLiveLegacy.ts --live <live copy>
----world <world copy> --subjects <exported map>`. The exit code was 0 and
--the world copy was byte-identical afterwards. The local copies were deleted
--after the run.
--
--```
--Live legacy import: DRY RUN (nothing written); subjects from subject map <exported from the Network identity_legacy_map>
--
--table decision read imported written held excluded check
--game_players import 70 11 11 12 47 ok
--game_inventory import 399 316 316 83 0 ok
--game_bank import 115 64 64 51 0 ok
--game_achievements import 122 68 68 54 0 ok
--game_recipes import 140 140 140 0 0 ok
--game_fish_collection import 13 13 13 0 0 ok
--game_battle_stats import 3 3 3 0 0 ok
--game_structures import 1 1 1 0 0 ok
--game_farm_plots import 0 0 0 0 0 ok
--game_world_state exclude 1 0 0 0 1 ok
--game_effects exclude 0 0 0 0 0 ok
--game_dungeon_runs exclude 0 0 0 0 0 ok
--game_leaderboard exclude 0 0 0 0 0 ok
--game_daily_quest_progress exclude 131 0 0 0 131 ok
--game_daily_quest_claims exclude 0 0 0 0 0 ok
--canvas_tiles exclude 375 0 0 0 375 ok
--canvas_actions exclude 846 0 0 0 846 ok
--canvas_settings exclude 8 0 0 0 8 ok
--canvas_snapshots exclude 0 0 0 0 0 ok
--canvas_bans exclude 0 0 0 0 0 ok
--canvas_region_locks exclude 0 0 0 0 0 ok
--canvas_user_overrides exclude 0 0 0 0 0 ok
--TOTAL 2224 616 616 200 1408 ok
--
--read 2224 = imported 616 + held 200 + excluded 1408: balanced
--Live users owning importable rows: 23; mapped to a subject: 11; held: 12 no-subject
--Excluded rows of import tables: game_players: placeholder rows Live's profile lookup created for people who never played: 47
--
--Held rows (by Live user; table[source keys]):
-- live user 2 (no-subject): game_players[2] game_inventory[285,1376,2847,2849,2850,2854,2859,2862,2863,2867,2874,2890,2931,2939,2949] game_bank[2,12,14,27,44,59,129,189,209,210,211,212,217,225,265,456,471,709,712,759,908,1004,1048,1049,1053,1059,1062,1069,1070,1071,1266,1273,1274,1280,1284,1322] game_achievements[2/chest_open_1,2/chest_open_25,2/dragonite_found,2/explorer_10k,2/explorer_1k,2/first_blood,2/first_gather,2/gather_100,2/gather_500,2/level_10_any,2/level_25_any,2/level_50_any,2/mob_slayer_10,2/mob_slayer_50,2/total_level_100,2/total_level_50]
-- live user 4 (no-subject): game_players[4] game_inventory[2107,2108,2147,2149,2163,2173,2175,2177,2178,2180,2183,2188,2198,2199,2219] game_achievements[4/chest_open_1,4/first_blood,4/first_gather,4/mob_slayer_10]
-- live user 5 (no-subject): game_players[5] game_inventory[531,532,533,538,2488] game_achievements[5/chest_open_1,5/explorer_1k,5/first_blood,5/level_10_any]
-- live user 7 (no-subject): game_players[7] game_inventory[2985,2986,2990,2992,2993,2995,2996,3019,3024,3025,3042,3070,3087,3090,3094,3111,3122,3132,3141,3145,3150,3152] game_achievements[7/chest_open_1,7/crystal_wood,7/explorer_1k,7/first_blood,7/first_gather,7/level_10_any,7/level_25_any,7/mob_slayer_10,7/mob_slayer_50,7/total_level_50]
-- live user 10 (no-subject): game_players[10] game_inventory[2489] game_achievements[10/chest_open_1,10/first_blood,10/level_10_any]
-- live user 11 (no-subject): game_players[11] game_inventory[2379,2381,2382,2384,2406,2415] game_bank[3677,3683,3684,3685,3687,3693,3695,3698,3701,3720,3727,3732,3736,3741,3742] game_achievements[11/chest_open_1,11/explorer_1k,11/first_blood,11/first_gather,11/level_10_any,11/level_25_any,11/level_50_any,11/mob_slayer_10,11/total_level_100,11/total_level_50]
-- live user 23 (no-subject): game_players[23]
-- live user 24 (no-subject): game_players[24] game_achievements[24/level_10_any]
-- live user 25 (no-subject): game_players[25] game_achievements[25/level_10_any]
-- live user 27 (no-subject): game_players[27] game_inventory[37123,37133,37139,37140,37141,37142,37144,37147,37153,37154,37155,37156,37163,37168,37170,37171,38652,38661,38662] game_achievements[27/chest_open_1,27/crystal_wood,27/explorer_1k,27/first_blood,27/first_gather]
-- live user 31 (no-subject): game_players[31]
-- live user 44 (no-subject): game_players[44]
-- no-subject: the Live user has no Network subject
--
--RECONCILED
--```
--
--Reading it: 616 rows would be archived for 11 subjects. 200 rows of 12
--unlinkable Live accounts are held. 1,408 rows are excluded: 47 placeholder
--`game_players` rows, the 131 daily-quest rows, the world seed and all 1,229
--canvas rows. Every table balances.
--
--## Production run
--
--Run these on `openvibe-ovh`, logged in as `ubuntu` (the owner of
--`/opt/openvibe.games`; `sqlite3` without `sudo` below runs as `ubuntu` so
--no root-owned `-wal`/`-shm` files appear next to the service's database), after this commit is deployed to
--`/opt/openvibe.games`. Deploying means pull, `pnpm install
----frozen-lockfile` (the server gained the `better-sqlite3` dependency), then
--restart `openvibe-games`. On its first start the server upgrades `world.db`
--to schema 13, and Live is not touched. The importer runs as `ubuntu` with
--the service's environment, the same way as the service, through a
--transient unit.
--
--```bash
--# --- 0. Variables ----------------------------------------------------------
--TS=$(date -u +%Y%m%dT%H%M%SZ)
--W=/opt/openvibe.games/data/legacy-import
--RUN="sudo systemd-run --quiet --wait --pipe --collect --uid=ubuntu --gid=ubuntu \
-- -p EnvironmentFile=/etc/openvibe/games.env -p WorkingDirectory=/opt/openvibe.games/apps/server \
-- /usr/bin/env node --import tsx scripts/importLiveLegacy.ts"
--sudo install -d -o ubuntu -g ubuntu -m 700 "$W"
--curl -fsS http://127.0.0.1:8000/api/ready # the deployed build is up
--
--# --- 1. Snapshot Live (read-only on the production file) -------------------
--sudo sqlite3 -readonly /opt/openvibe.live/data/live.db ".backup $W/live-$TS.db"
--sudo chown ubuntu:ubuntu "$W/live-$TS.db" && sudo chmod 600 "$W/live-$TS.db"
--sqlite3 -readonly "$W/live-$TS.db" "PRAGMA integrity_check" # as ubuntu; expect: ok
--
--# --- 2. Dry run (writes nothing; exit 0 = reconciled) ----------------------
--$RUN --live "$W/live-$TS.db" --world /opt/openvibe.games/data/world.db --subjects network \
-- | tee "$W/dry-run-$TS.txt"
--# Expect the table above: read 2224 = imported 616 + held 200 + excluded 1408, RECONCILED
--# (game_players grows by one placeholder, excluded, for each new profile Live shows).
--
--# --- 3. Apply: backs up world.db to --backup, verifies it, imports in one transaction
--$RUN --live "$W/live-$TS.db" --world /opt/openvibe.games/data/world.db --subjects network \
-- --apply --backup "$W/world-before-$TS.db" | tee "$W/apply-$TS.txt"
--# stderr: "backup written and verified (integrity_check ok): …" and "wrote 616 rows to legacy_live_rows"
--
--# --- 4. Verify -------------------------------------------------------------
--sqlite3 -readonly /opt/openvibe.games/data/world.db \
-- "SELECT source_table, count(*) FROM legacy_live_rows GROUP BY 1 ORDER BY 1"
--# game_achievements 68, game_bank 64, game_battle_stats 3, game_fish_collection 13,
--# game_inventory 316, game_players 11, game_recipes 140, game_structures 1
--sqlite3 -readonly /opt/openvibe.games/data/world.db \
-- "SELECT count(DISTINCT subject_id) FROM legacy_live_rows" # 11
--sqlite3 -readonly /opt/openvibe.games/data/world.db "PRAGMA integrity_check" # ok
--$RUN --live "$W/live-$TS.db" --world /opt/openvibe.games/data/world.db --subjects network \
-- | grep -E '^TOTAL|balanced|RECONCILED' # idempotency: written 0 on every row
--curl -fsS http://127.0.0.1:8000/api/ready
--
--# --- 5. Clean up the Live snapshot (user data); keep the world backup until satisfied
--sudo rm -f "$W/live-$TS.db" "$W/live-$TS.db-wal" "$W/live-$TS.db-shm"
--```
--
--The importer refuses to start (exit 2) in these cases:
--
--- `--apply` without `--backup` or `--subjects`;
--- a `--backup` path that already exists;
--- a `--live` path under `/opt/openvibe.live/`, the production file;
--- a world database that does not exist.
--
--It exits 3 when the plan does not reconcile, and then writes nothing.
--
--**Rollback.** The import only inserts into `legacy_live_rows`, so it can
--be undone without touching the game:
--`sqlite3 /opt/openvibe.games/data/world.db "DELETE FROM legacy_live_rows"` (as `ubuntu`).
--A full restore of `$W/world-before-$TS.db` (stop the service, replace
--`world.db`, remove `world.db-wal`/`-shm`, start) would also discard any
--player progress made since the backup. Use it only if `world.db` itself is
--damaged.
--
--**Offline dry run.** This is how the run above was made. It needs no
--service secret. Export the map on the host, then pass it with
--`--subjects <file>`:
--
--```bash
--sudo sqlite3 -readonly /opt/openvibe.network/data/network.db \
-- "SELECT json_group_object(m.source_id, m.subject_id) FROM identity_legacy_map m
-- JOIN users u ON u.subject_id = m.subject_id
-- WHERE m.source_system = 'live' AND m.source_type = 'user'" > live-subjects.json
--```
ddiff --git a/packages/content/src/mapFile.ts b/packages/content/src/mapFile.ts
iindex c0358d1..784f3de 100644
---- a/packages/content/src/mapFile.ts
++++ b/packages/content/src/mapFile.ts
@@@ -1,14 +1,9 @@
--import type { FaceStyle, MapLight, StaticBody } from './schema/world.js'
--import type { SurfaceMaterialData } from './surface.js'
--import type { MapNodeSpawn, MapOverride, MapPropSpawn } from './terrain.js'
--
/**
-- * The edited-map artifact produced by the /editor tool and consumed by the
-- * game (server AND client). Heights are base64 Float32; the splat mix is a
-- * PNG data URL; extra statics append to the world definition. Model/glTF
-- * imports become new entries here in a later phase — the format is the
-- * contract, the tools are replaceable.
++ * Map document primitives shared by the editor, the server and the client:
++ * the base64 heightfield codec, the custom-texture entry shape, and the
++ * authored light type. The document itself is `MapFileV2` (./mapFileV2.ts).
*/
++
/**
* Editor-placed light. Covers every Babylon punctual/ambient light type:
* point, spot (angle+exponent), directional (sun-like), hemispheric
@@@ -21,18 +16,10 @@ import type { MapNodeSpawn, MapOverride, MapPropSpawn } from './terrain.js'
*/
export type { MapLight } from './schema/world.js'
--/**
-- * v1 seeds predate stable document ids. The migration assigns one to every
-- * object it carries forward, so nothing downstream of `parseMapFile` has to
-- * cope with an identity-less map object.
-- */
--export type LegacyNodeSpawn = Omit<MapNodeSpawn, 'id'> & { id?: string }
--export type LegacyPropSpawn = Omit<MapPropSpawn, 'id'> & { id?: string }
--
--/** A custom texture: either embedded (legacy dataUrl) or server-hosted. */
++/** A custom texture: either embedded (dataUrl) or server-hosted. */
export interface MapTextureEntry {
name: string
-- /** Legacy embedded payload (small uploads from older maps). */
++ /** Embedded payload (small uploads). */
dataUrl?: string
/** Server-hosted asset path (/map-assets/...), preferred. */
url?: string
@@@ -40,48 +27,6 @@ export interface MapTextureEntry {
scale?: number
}
--export interface MapFile {
-- v: 1
-- halfExtent: number
-- sub: number
-- /** base64 of Float32Array little-endian heights, (sub+1)^2 entries. */
-- heights: string
-- /** PNG data URL painted splat mix (R grass / G rock / B mud), optional. */
-- mix?: string
-- /** Editor-placed statics appended to the world def. */
-- statics: StaticBody[]
-- /** Editor-placed resource nodes (trees, deposits, piles…). */
-- nodes?: LegacyNodeSpawn[]
-- /** Props seeded into FRESH worlds (crates, barrels, merchant stalls). */
-- props?: LegacyPropSpawn[]
-- /** Extra sculptable terrain patches (mountains, cave shells…). */
-- terrains?: {
-- id: string
-- origin: [number, number, number]
-- halfExtent: number
-- sub: number
-- heights: string
-- rot?: [number, number, number]
-- /** Canonical scale (X/Z footprint, Y height displacement). */
-- scale?: [number, number, number]
-- tex?: string
-- color?: string
-- mix?: string
-- uv?: FaceStyle
-- /** Base style + paint layers (v2 surface model). */
-- surface?: SurfaceMaterialData
-- }[]
-- /** Imported glTF models (data URLs) placeable as statics via `model`. */
-- models?: { id: string; name: string; glb: string; bounds: [number, number, number] }[]
-- /** Uploaded custom textures usable on statics as `custom:<name>`. */
-- textures?: MapTextureEntry[]
-- /** Editor-placed lights (rendered client-side). */
-- lights?: MapLight[]
-- /** Player spawn point + facing. */
-- spawn?: [number, number, number]
-- spawnYaw?: number
--}
--
export function decodeHeights(b64: string): Float32Array {
const bin = typeof atob === 'function' ? atob(b64) : Buffer.from(b64, 'base64').toString('binary')
const bytes = new Uint8Array(bin.length)
@@@ -95,12 +40,3 @@ export function encodeHeights(heights: Float32Array): string {
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i] as number)
return typeof btoa === 'function' ? btoa(bin) : Buffer.from(bin, 'binary').toString('base64')
}
--
--/**
-- * Legacy v1 → runtime, by way of the v2 migration. v1 input is still
-- * accepted; it is just never the runtime representation.
-- */
--export function mapFileToOverride(_map: MapFile): MapOverride {
-- // Implemented in mapFileV2.ts to avoid an import cycle; see compileV1.
-- throw new Error('mapFileToOverride: use parseMapFile() + compileMapFileV2()')
--}
ddiff --git a/packages/content/src/mapFileV2.test.ts b/packages/content/src/mapFileV2.test.ts
iindex 7691e63..7e567ee 100644
---- a/packages/content/src/mapFileV2.test.ts
++++ b/packages/content/src/mapFileV2.test.ts
@@@ -1,13 +1,10 @@
import { describe, expect, it } from 'vitest'
--import { encodeHeights, type MapFile } from './mapFile.js'
import {
-- MAIN_TERRAIN_MIGRATED_ID,
SPAWN_OBJECT_ID,
compileMapFileV2,
blankHeights,
canonicalizeMapFile,
emptyMapV2,
-- migrateV1ToV2,
parseMapFile,
validateMapFile,
type MapFileV2,
@@@ -15,20 +12,6 @@ import {
} from './mapFileV2.js'
const SUB = 8
--const heightsOf = (fill: number): string => {
-- const a = new Float32Array((SUB + 1) * (SUB + 1))
-- a.fill(fill)
-- return encodeHeights(a)
--}
--
--const v1Map = (over: Partial<MapFile> = {}): MapFile => ({
-- v: 1,
-- halfExtent: 100,
-- sub: SUB,
-- heights: heightsOf(0),
-- statics: [],
-- ...over,
--})
describe('empty map', () => {
it('a brand-new map has no terrain and no geometry at all', () => {
@@@ -42,117 +25,16 @@ describe('empty map', () => {
})
})
--describe('v1 → v2 migration', () => {
-- it('turns the privileged main heightfield into ONE ordinary terrain', () => {
-- const out = migrateV1ToV2(v1Map({ heights: heightsOf(3) }))
-- expect(out.v).toBe(2)
-- expect(out.terrains).toHaveLength(1)
-- const t = out.terrains[0]!
-- expect(t.id).toBe(MAIN_TERRAIN_MIGRATED_ID)
-- expect(t.pos).toEqual([0, 0, 0])
-- expect(t.halfExtent).toBe(100)
-- // Nothing about it is special any more.
-- expect(t).not.toHaveProperty('main')
-- })
--
-- it('is deterministic — the same v1 map yields the same ids twice', () => {
-- const src = v1Map({
-- statics: [
-- { shape: { type: 'box', size: [1, 1, 1] }, pos: [0, 0, 0], yaw: 0, color: '#ffffff' },
-- ],
-- })
-- expect(canonicalizeMapFile(migrateV1ToV2(src))).toBe(canonicalizeMapFile(migrateV1ToV2(src)))
-- })
--
-- it('drops a main terrain that was "deleted" by sinking it below the waterline', () => {
-- // v1 faked deletion with heights.fill(-6) and an invisible collider.
-- const out = migrateV1ToV2(v1Map({ heights: heightsOf(-6) }))
-- expect(out.terrains).toHaveLength(0)
-- })
--
-- it('maps the legacy splat onto paint layers with channels preserved', () => {
-- const out = migrateV1ToV2(v1Map({ mix: 'data:image/png;base64,AAA' }))
-- const paint = out.terrains[0]!.surface!.paint!
-- expect(paint.mask).toBe('data:image/png;base64,AAA')
-- expect(paint.layers.map((l) => [l.tex, l.channel])).toEqual([
-- ['leafy_grass', 'r'],
-- ['gray_rocks', 'g'],
-- ['brown_mud_dry', 'b'],
-- ])
-- })
--
-- it('migrates v1 patches, keeping ids, transforms and surfaces', () => {
-- const out = migrateV1ToV2(
-- v1Map({
-- terrains: [
-- {
-- id: 'patch-7',
-- origin: [10, 2, -4],
-- halfExtent: 16,
-- sub: SUB,
-- heights: heightsOf(1),
-- rot: [0, 0.5, 0],
-- scale: [2, 1, 2],
-- tex: 'red_brick',
-- color: '#aabbcc',
-- },
-- ],
-- }),
-- )
-- const t = out.terrains.find((x) => x.id === 'patch-7')!
-- expect(t.pos).toEqual([10, 2, -4])
-- expect(t.rot).toEqual([0, 0.5, 0])
-- expect(t.scale).toEqual([2, 1, 2])
-- expect(t.surface!.base).toEqual({ tex: 'red_brick', color: '#aabbcc' })
-- })
--
-- it("treats the 'none' texture sentinel as a plain-colour base", () => {
-- const out = migrateV1ToV2(
-- v1Map({
-- terrains: [
-- {
-- id: 'p1',
-- origin: [0, 0, 0],
-- halfExtent: 8,
-- sub: SUB,
-- heights: heightsOf(0),
-- tex: 'none',
-- },
-- ],
-- }),
-- )
-- expect(out.terrains.find((t) => t.id === 'p1')!.surface!.base.tex).toBeUndefined()
-- })
--
-- it('gives ids to v1 objects that never had one', () => {
-- const out = migrateV1ToV2(
-- v1Map({
-- statics: [
-- { shape: { type: 'box', size: [1, 1, 1] }, pos: [0, 0, 0], yaw: 0, color: '#ffffff' },
-- ],
-- nodes: [{ node: 'oak_tree', pos: [1, 0, 1] }],
-- }),
-- )
-- expect(out.statics[0]!.id).toBe('s-v1-0')
-- expect(out.nodes[0]!.id).toBe('n-v1-0')
-- })
--})
--
describe('parseMapFile', () => {
it('accepts a v2 map unchanged', () => {
-- const m = emptyMapV2()
-- const r = parseMapFile(m)
-- expect(r.ok && r.migrated).toBe(false)
++ const r = parseMapFile(emptyMapV2())
expect(r.ok).toBe(true)
})
-- it('accepts a v1 map and reports that it migrated', () => {
-- const r = parseMapFile(v1Map())
-- expect(r.ok).toBe(true)
-- if (r.ok) expect(r.migrated).toBe(true)
-- })
--
-- it('rejects unknown versions and non-objects', () => {
++ it('rejects a v1 map and unknown versions and non-objects', () => {
++ expect(parseMapFile({ v: 1, halfExtent: 100, sub: SUB, heights: '', statics: [] }).ok).toBe(
++ false,
++ )
expect(parseMapFile({ v: 99 }).ok).toBe(false)
expect(parseMapFile(null).ok).toBe(false)
expect(parseMapFile('nope').ok).toBe(false)
@@@ -316,32 +198,6 @@ describe('paint layer tint survives the canonical wire', () => {
expect(r.ok, `expected "${bad}" to be rejected`).toBe(false)
}
})
--
-- it('carries the tint through a v1 migration parse too', () => {
-- const v1 = v1Map({
-- terrains: [
-- {
-- id: 'p1',
-- origin: [0, 0, 0],
-- halfExtent: 8,
-- sub: SUB,
-- heights: blankHeights(SUB),
-- surface: {
-- base: { tex: 'red_brick' },
-- paint: {
-- mask: 'data:x',
-- layers: [{ id: 'l1', tex: 'none', color: '#123456', channel: 'a' }],
-- },
-- },
-- },
-- ],
-- } as Partial<MapFile>)
-- const r = parseMapFile(v1)
-- expect(r.ok).toBe(true)
-- if (!r.ok) return
-- const t = r.map.terrains.find((x) => x.id === 'p1')!
-- expect(t.surface!.paint!.layers[0]!.color).toBe('#123456')
-- })
})
describe('canonicalisation', () => {
ddiff --git a/packages/content/src/mapFileV2.ts b/packages/content/src/mapFileV2.ts
iindex c23b99b..9ffd380 100644
---- a/packages/content/src/mapFileV2.ts
++++ b/packages/content/src/mapFileV2.ts
@@@ -21,20 +21,9 @@ import {
type FaceStyle,
type ZoneDef,
} from './schema/world.js'
--import {
-- decodeHeights,
-- encodeHeights,
-- type MapFile,
-- type MapLight,
-- type MapTextureEntry,
--} from './mapFile.js'
++import { decodeHeights, encodeHeights, type MapLight } from './mapFile.js'
import type { MapOverride, TerrainPatchData } from './terrain.js'
--import {
-- MAX_PAINT_LAYERS,
-- PAINT_CHANNELS,
-- migrateLegacyMix,
-- type SurfaceMaterialData,
--} from './surface.js'
++import { MAX_PAINT_LAYERS, PAINT_CHANNELS, type SurfaceMaterialData } from './surface.js'
const vec3 = z.tuple([z.number(), z.number(), z.number()])
const finiteVec3 = vec3.refine((v) => v.every(Number.isFinite), 'must be finite')
@@@ -275,95 +264,18 @@ export function normalizeMapIds(raw: unknown): unknown {
return out
}
--/** Deterministic ids so a v1 map migrates to the SAME v2 ids every time. */
--const legacyId = (kind: string, index: number): string => `${kind}-v1-${index}`
--export const MAIN_TERRAIN_MIGRATED_ID = 'terrain-v1-main'
--
--/**
-- * v1 → v2. The privileged top-level heightfield becomes ONE ordinary terrain
-- * object with a deterministic id; the old splat becomes paint layers with the
-- * channel weights preserved, so an old map looks the same after migration.
-- *
-- * A v1 main terrain that was "deleted" by sinking every height below the
-- * waterline is dropped rather than carried over as an invisible collider.
-- */
--export function migrateV1ToV2(v1: MapFile): MapFileV2 {
-- const out = emptyMapV2()
-- let layerSeq = 0
-- const mkLayerId = (): string => `pl-v1-${layerSeq++}`
--
-- const mainHeights = decodeHeights(v1.heights)
-- const mainIsGone = mainHeights.every((h) => h <= -4.5)
-- if (!mainIsGone) {
-- out.terrains.push({
-- id: MAIN_TERRAIN_MIGRATED_ID,
-- name: 'Ground',
-- pos: [0, 0, 0],
-- halfExtent: v1.halfExtent,
-- sub: v1.sub,
-- heights: v1.heights,
-- surface: {
-- base: {},
-- ...(v1.mix ? { paint: migrateLegacyMix(v1.mix, mkLayerId)! } : {}),
-- },
-- })
-- }
--
-- ;(v1.terrains ?? []).forEach((t, i) => {
-- const base = {
-- ...(t.tex && t.tex !== 'none' ? { tex: t.tex } : {}),
-- ...(t.color ? { color: t.color } : {}),
-- ...(t.uv ? { uv: t.uv } : {}),
-- }
-- out.terrains.push({
-- id: t.id || legacyId('terrain', i),
-- pos: t.origin,
-- ...(t.rot ? { rot: t.rot } : {}),
-- ...(t.scale ? { scale: t.scale } : {}),
-- halfExtent: t.halfExtent,
-- sub: t.sub,
-- heights: t.heights,
-- surface: t.surface ?? {
-- base,
-- ...(t.mix ? { paint: migrateLegacyMix(t.mix, mkLayerId)! } : {}),
-- },
-- })
-- })
--
-- out.statics = (v1.statics ?? []).map((b, i) => ({ ...b, id: b.id ?? legacyId('s', i) }))
-- out.nodes = (v1.nodes ?? []).map((n, i) => ({ ...n, id: n.id ?? legacyId('n', i) }))
-- out.props = (v1.props ?? []).map((p, i) => ({ ...p, id: p.id ?? legacyId('pr', i) }))
-- out.lights = (v1.lights ?? []) as unknown as MapFileV2['lights']
-- out.models = v1.models ?? []
-- out.textures = (v1.textures ?? []) as MapTextureEntry[]
-- if (v1.spawn) out.spawn = v1.spawn
-- if (v1.spawnYaw !== undefined) out.spawnYaw = v1.spawnYaw
-- return out
--}
--
--export type ParsedMap =
-- { ok: true; map: MapFileV2; migrated: boolean } | { ok: false; issues: string[] }
++export type ParsedMap = { ok: true; map: MapFileV2 } | { ok: false; issues: string[] }
--/** Parse any accepted map version into v2, or report why it was rejected. */
++/** Parse a v2 map document, or report why it was rejected. */
export function parseMapFile(raw: unknown): ParsedMap {
if (raw === null || typeof raw !== 'object') return { ok: false, issues: ['not an object'] }
const version = (raw as { v?: unknown }).v
-- if (version === 2) {
-- const r = MapFileV2Schema.safeParse(normalizeMapIds(raw))
-- if (!r.success) return { ok: false, issues: r.error.issues.map(describeIssue) }
-- const extra = validateMapFile(r.data)
-- if (extra.length > 0) return { ok: false, issues: extra }
-- return { ok: true, map: r.data, migrated: false }
-- }
-- if (version === 1) {
-- const migrated = migrateV1ToV2(raw as MapFile)
-- const r = MapFileV2Schema.safeParse(normalizeMapIds(migrated))
-- if (!r.success) return { ok: false, issues: r.error.issues.map(describeIssue) }
-- const extra = validateMapFile(r.data)
-- if (extra.length > 0) return { ok: false, issues: extra }
-- return { ok: true, map: r.data, migrated: true }
-- }
-- return { ok: false, issues: [`unsupported map version ${String(version)}`] }
++ if (version !== 2) return { ok: false, issues: [`unsupported map version ${String(version)}`] }
++ const r = MapFileV2Schema.safeParse(normalizeMapIds(raw))
++ if (!r.success) return { ok: false, issues: r.error.issues.map(describeIssue) }
++ const extra = validateMapFile(r.data)
++ if (extra.length > 0) return { ok: false, issues: extra }
++ return { ok: true, map: r.data }
}
const describeIssue = (i: z.ZodIssue): string => `${i.path.join('.') || '(root)'}: ${i.message}`
ddiff --git a/packages/content/src/surface.test.ts b/packages/content/src/surface.test.ts
iindex 0a3ba05..d98bafa 100644
---- a/packages/content/src/surface.test.ts
++++ b/packages/content/src/surface.test.ts
@@@ -6,7 +6,6 @@ import {
emptySurface,
freeChannel,
layerForTexture,
-- migrateLegacyMix,
removeLayer,
validateSurface,
type SurfacePaint,
@@@ -134,24 +133,6 @@ describe('base texture survives painting', () => {
})
})
--describe('legacy migration', () => {
-- it('maps the old grass/rock/mud splat onto channels r/g/b', () => {
-- const paint = migrateLegacyMix('data:image/png;base64,AAA', makeId)!
-- expect(paint.mask).toBe('data:image/png;base64,AAA')
-- expect(paint.layers.map((l) => [l.tex, l.channel])).toEqual([
-- ['leafy_grass', 'r'],
-- ['gray_rocks', 'g'],
-- ['brown_mud_dry', 'b'],
-- ])
-- // The alpha channel stays free for a newly painted texture.
-- expect(freeChannel(paint)).toBe('a')
-- })
--
-- it('an unpainted legacy surface migrates to no paint at all', () => {
-- expect(migrateLegacyMix(undefined, makeId)).toBeUndefined()
-- })
--})
--
describe('validation', () => {
const exists = (ref: string): boolean => ref.startsWith('ok')
ddiff --git a/packages/content/src/surface.ts b/packages/content/src/surface.ts
iindex 995c281..059ad1c 100644
---- a/packages/content/src/surface.ts
++++ b/packages/content/src/surface.ts
@@@ -135,26 +135,6 @@ export function removeLayer(paint: SurfacePaint, layerId: string): PaintChannel
return gone?.channel ?? null
}
--/**
-- * Migrate a legacy three-way splat (R grass / G rock / B mud) into v2 paint
-- * layers. The channel assignment is preserved exactly, so an old map's
-- * painted ground renders identically after migration.
-- */
--export function migrateLegacyMix(
-- mix: string | undefined,
-- makeId: () => string,
--): SurfacePaint | undefined {
-- if (!mix) return undefined
-- return {
-- mask: mix,
-- layers: [
-- { id: makeId(), tex: 'leafy_grass', channel: 'r' },
-- { id: makeId(), tex: 'gray_rocks', channel: 'g' },
-- { id: makeId(), tex: 'brown_mud_dry', channel: 'b' },
-- ],
-- }
--}
--
/** Validation for the Issues panel and the save pipeline. */
export function validateSurface(
s: SurfaceMaterialData,
ddiff --git a/packages/content/src/terrain.ts b/packages/content/src/terrain.ts
iindex 934a154..8317fe8 100644
---- a/packages/content/src/terrain.ts
++++ b/packages/content/src/terrain.ts
@@@ -140,8 +140,6 @@ export interface TerrainPatchData {
tex?: string
/** Tint color (hex). */
color?: string
-- /** Legacy painted splat (R grass / G rock / B mud); migrated to `surface`. */
-- mix?: string
/** Base style + paint layers — the v2 surface model. */
surface?: SurfaceMaterialData
/** Whole-surface UV transform from the face-edit tool. */
ddiff --git a/packages/persistence/src/repositories.ts b/packages/persistence/src/repositories.ts
iindex cfe9957..43642e3 100644
---- a/packages/persistence/src/repositories.ts
++++ b/packages/persistence/src/repositories.ts
@@@ -38,11 +38,6 @@ export interface PlayerRepository {
resetAllPositions(pos: [number, number, number], yaw: number): void
}
--export interface GuestRepository {
-- /** Resolve a guest connection (ip + browser token) to its canonical account token. */
-- resolve(ip: string, token: string): string
--}
--
export interface ConstraintRepository {
loadAll(): ConstraintDto[]
upsertMany(constraints: readonly ConstraintDto[]): void
@@@ -51,25 +46,11 @@ export interface ConstraintRepository {
/**
* Canonical identity (ADR-0006): signed-in accounts are keyed by their
-- * openvibe.network subject. Accounts created before that were keyed by a
-- * legacy key (`ovn:<network user id>`); adoption moves them over once and
-- * remembers the mapping.
++ * openvibe.network subject. Adoptions (a guest character joining the account
++ * that signs in on it; a merged account's characters) are recorded once, in
++ * `identity_adoptions`, so a redelivered event cannot move the same rows twice.
*/
export interface IdentityRepository {
-- /**
-- * Moves every character under `legacyKey` to `subjectId` (slots the subject
-- * does not already use) and records the mapping. Idempotent: a second call
-- * moves nothing. Characters whose slot is taken stay under the legacy key
-- * and are reported as `conflicts`.
-- */
-- adoptLegacyAccount(
-- legacyKey: string,
-- subjectId: string,
-- source: string,
-- now: number,
-- ): { moved: number; conflicts: number }
-- /** The subject a legacy key was adopted into, if any. */
-- subjectForLegacy(legacyKey: string): string | null
/**
* Guest conversion (roadmap WS-B task 8): the character a browser played as a guest moves into the
* account that signs in on it, into the first free slot (0..2). Once per guest token (remembered by
@@@ -93,23 +74,24 @@ export interface IdentityRepository {
mergeId: string,
now: number,
): { moved: number; kept: number; already: boolean }
-- /** Distinct account keys under a legacy prefix that still own characters. */
-- legacyAccountKeys(prefix: string): string[]
/**
* What Games keeps about a subject, for their data export (roadmap WS-B task 7, ADR-033): their characters (without
-- * the sign-in token), the world structures those characters own, and the rows imported from Live's old game.
++ * the sign-in token) and the world structures those characters own.
*/
exportSubject(subject: string): {
characters: Record<string, unknown>[]
structures: Record<string, unknown>[]
-- legacyLiveRows: Record<string, unknown>[]
}
/**
* Erase these subjects (a deleted account and the accounts merged into it; ADR-033), in one transaction: their
-- * characters, imported Live rows and identity-map rows go; the structures their characters built stay in the
-- * world without an owner. Returns counts.
++ * characters and adoption rows go; the structures their characters built stay in the world without an owner.
++ * Returns counts.
*/
-- eraseSubjects(subjects: string[]): { characters: number; structures_unowned: number; legacy_rows: number; identity_rows: number }
++ eraseSubjects(subjects: string[]): {
++ characters: number
++ structures_unowned: number
++ identity_rows: number
++ }
}
/** Installed mods, their approved capabilities, placements and audit log. */
@@@ -161,7 +143,6 @@ export interface MetaRepository {
export interface PersistenceStore {
readonly worldEntities: WorldEntityRepository
readonly players: PlayerRepository
-- guests: GuestRepository
readonly constraints: ConstraintRepository
readonly meta: MetaRepository
readonly identity: IdentityRepository
ddiff --git a/packages/persistence/src/sqlite/platformRepositories.test.ts b/packages/persistence/src/sqlite/platformRepositories.test.ts
iindex 1f3c974..69536bd 100644
---- a/packages/persistence/src/sqlite/platformRepositories.test.ts
++++ b/packages/persistence/src/sqlite/platformRepositories.test.ts
@@@ -39,11 +39,31 @@ describe('guest conversion (WS-B task 8)', () => {
it('moves the guest character into the first free slot, once, keyed by a hash of the token', () => {
const store = openSqliteStore(':memory:')
store.players.upsertMany([player('g1', 'guest00042abc', 0), player('a0', SUBJECT, 0)])
-- expect(store.identity.adoptGuestCharacter('guest00042abc', SUBJECT, 10)).toEqual({ moved: 1, slot: 1, full: false })
-- expect(store.players.listByToken(SUBJECT).map((p) => [p.id, p.charSlot]).sort()).toEqual([['a0', 0], ['g1', 1]])
++ expect(store.identity.adoptGuestCharacter('guest00042abc', SUBJECT, 10)).toEqual({
++ moved: 1,
++ slot: 1,
++ full: false,
++ })
++ expect(
++ store.players
++ .listByToken(SUBJECT)
++ .map((p) => [p.id, p.charSlot])
++ .sort(),
++ ).toEqual([
++ ['a0', 0],
++ ['g1', 1],
++ ])
expect(store.players.listByToken('guest00042abc')).toEqual([])
-- expect(store.identity.adoptGuestCharacter('guest00042abc', SUBJECT, 11)).toEqual({ moved: 0, slot: null, full: false })
-- const keys = (store.db.prepare('SELECT legacy_key FROM identity_legacy_map').all() as { legacy_key: string }[]).map((r) => r.legacy_key)
++ expect(store.identity.adoptGuestCharacter('guest00042abc', SUBJECT, 11)).toEqual({
++ moved: 0,
++ slot: null,
++ full: false,
++ })
++ const keys = (
++ store.db.prepare('SELECT adoption_key FROM identity_adoptions').all() as {
++ adoption_key: string
++ }[]
++ ).map((r) => r.adoption_key)
expect(keys.some((k) => k.includes('guest00042abc'))).toBe(false)
expect(keys.some((k) => /^guest:[0-9a-f]{32}$/.test(k))).toBe(true)
store.close()
@@@ -51,10 +71,23 @@ describe('guest conversion (WS-B task 8)', () => {
it('keeps the guest character when every slot is taken, and does nothing without one', () => {
const store = openSqliteStore(':memory:')
-- store.players.upsertMany([player('a0', SUBJECT, 0), player('a1', SUBJECT, 1), player('a2', SUBJECT, 2), player('g1', 'guestfull01', 0)])
-- expect(store.identity.adoptGuestCharacter('guestfull01', SUBJECT, 1)).toEqual({ moved: 0, slot: null, full: true })
++ store.players.upsertMany([
++ player('a0', SUBJECT, 0),
++ player('a1', SUBJECT, 1),
++ player('a2', SUBJECT, 2),
++ player('g1', 'guestfull01', 0),
++ ])
++ expect(store.identity.adoptGuestCharacter('guestfull01', SUBJECT, 1)).toEqual({
++ moved: 0,
++ slot: null,
++ full: true,
++ })
expect(store.players.listByToken('guestfull01').length).toBe(1)
-- expect(store.identity.adoptGuestCharacter('nobodyhere01', SUBJECT, 1)).toEqual({ moved: 0, slot: null, full: false })
++ expect(store.identity.adoptGuestCharacter('nobodyhere01', SUBJECT, 1)).toEqual({
++ moved: 0,
++ slot: null,
++ full: false,
++ })
store.close()
})
})
@@@ -64,11 +97,29 @@ describe('account merge (ADR-029)', () => {
it('moves the folded-in characters into free slots, keeps what does not fit, once per merge', () => {
const store = openSqliteStore(':memory:')
// The survivor has slot 0; the folded-in account has slots 0, 1 and 2.
-- store.players.upsertMany([player('s0', SUBJECT, 0), player('f0', FROM, 0), player('f1', FROM, 1), player('f2', FROM, 2)])
-- expect(store.identity.mergeSubject(FROM, SUBJECT, 'mrg_01J8Z3Q4R5S6T7V8W9X0Y1Z2G9', 5)).toEqual({ moved: 2, kept: 1, already: false })
-- expect(store.players.listByToken(SUBJECT).map((p) => [p.id, p.charSlot]).sort()).toEqual([['f1', 1], ['f2', 2], ['s0', 0]])
++ store.players.upsertMany([
++ player('s0', SUBJECT, 0),
++ player('f0', FROM, 0),
++ player('f1', FROM, 1),
++ player('f2', FROM, 2),
++ ])
++ expect(store.identity.mergeSubject(FROM, SUBJECT, 'mrg_01J8Z3Q4R5S6T7V8W9X0Y1Z2G9', 5)).toEqual(
++ { moved: 2, kept: 1, already: false },
++ )
++ expect(
++ store.players
++ .listByToken(SUBJECT)
++ .map((p) => [p.id, p.charSlot])
++ .sort(),
++ ).toEqual([
++ ['f1', 1],
++ ['f2', 2],
++ ['s0', 0],
++ ])
expect(store.players.listByToken(FROM).map((p) => p.id)).toEqual(['f0'])
-- expect(store.identity.mergeSubject(FROM, SUBJECT, 'mrg_01J8Z3Q4R5S6T7V8W9X0Y1Z2G9', 6)).toEqual({ moved: 0, kept: 0, already: true })
++ expect(store.identity.mergeSubject(FROM, SUBJECT, 'mrg_01J8Z3Q4R5S6T7V8W9X0Y1Z2G9', 6)).toEqual(
++ { moved: 0, kept: 0, already: true },
++ )
expect(store.players.listByToken(FROM).length).toBe(1)
store.close()
})
@@@ -76,87 +127,50 @@ describe('account merge (ADR-029)', () => {
describe('account export and deletion (ADR-033)', () => {
const ALIAS = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2F8'
-- it('exports the subject\'s characters without the token, then erases them and the alias\'s; structures stay unowned', () => {
++ it("exports the subject's characters without the token, then erases them and the alias's; structures stay unowned", () => {
const store = openSqliteStore(':memory:')
-- store.players.upsertMany([player('d0', SUBJECT, 0), player('d1', ALIAS, 0), player('o0', OTHER, 0)])
-- const entity = store.db.prepare("INSERT INTO world_entities (id, kind, def_id, owner_id, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, rot_w, motion, state, updated_at) VALUES (?, 'structure', 'hut', ?, 0, 0, 0, 0, 0, 0, 1, 'static', '{}', 1)")
++ store.players.upsertMany([
++ player('d0', SUBJECT, 0),
++ player('d1', ALIAS, 0),
++ player('o0', OTHER, 0),
++ ])
++ const entity = store.db.prepare(
++ "INSERT INTO world_entities (id, kind, def_id, owner_id, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, rot_w, motion, state, updated_at) VALUES (?, 'structure', 'hut', ?, 0, 0, 0, 0, 0, 0, 1, 'static', '{}', 1)",
++ )
entity.run('e1', 'd0')
entity.run('e2', 'o0')
-- store.db.prepare("INSERT INTO identity_legacy_map (legacy_key, subject_id, source, adopted_at) VALUES ('ovn:9', ?, 'network', 1)").run(SUBJECT)
++ store.db
++ .prepare(
++ "INSERT INTO identity_adoptions (adoption_key, subject_id, source, adopted_at) VALUES ('merge:mrg_01J8Z3Q4R5S6T7V8W9X0Y1Z2G9', ?, 'merge', 1)",
++ )
++ .run(SUBJECT)
const out = store.identity.exportSubject(SUBJECT)
expect(out.characters.map((c) => c.id)).toEqual(['d0'])
expect(out.characters[0]).not.toHaveProperty('token')
expect(out.characters[0]?.skills).toEqual({ woodcutting: 10 })
expect(out.structures.map((e) => e.id)).toEqual(['e1'])
-- expect(store.identity.eraseSubjects([SUBJECT, ALIAS])).toEqual({ characters: 2, structures_unowned: 1, legacy_rows: 0, identity_rows: 1 })
++ expect(store.identity.eraseSubjects([SUBJECT, ALIAS])).toEqual({
++ characters: 2,
++ structures_unowned: 1,
++ identity_rows: 1,
++ })
expect(store.players.listByToken(SUBJECT)).toEqual([])
expect(store.players.listByToken(ALIAS)).toEqual([])
expect(store.players.listByToken(OTHER).map((p) => p.id)).toEqual(['o0'])
-- expect(store.db.prepare('SELECT id, owner_id FROM world_entities ORDER BY id').all()).toEqual([{ id: 'e1', owner_id: null }, { id: 'e2', owner_id: 'o0' }])
-- expect(store.identity.eraseSubjects([SUBJECT])).toEqual({ characters: 0, structures_unowned: 0, legacy_rows: 0, identity_rows: 0 })
-- store.close()
-- })
--})
--
--describe('identity repository (legacy -> canonical subject)', () => {
-- it('moves every legacy character to the subject once and records the map', () => {
-- const store = openSqliteStore(':memory:')
-- store.players.upsertMany([player('p1', 'ovn:57', 0), player('p2', 'ovn:57', 1)])
--
-- expect(store.identity.legacyAccountKeys('ovn:')).toEqual(['ovn:57'])
-- expect(store.identity.adoptLegacyAccount('ovn:57', SUBJECT, 'network', 100)).toEqual({
-- moved: 2,
-- conflicts: 0,
-- })
-- const chars = store.players.listByToken(SUBJECT)
-- expect(chars.map((c) => [c.id, c.charSlot, c.subjectId])).toEqual([
-- ['p1', 0, SUBJECT],
-- ['p2', 1, SUBJECT],
++ expect(store.db.prepare('SELECT id, owner_id FROM world_entities ORDER BY id').all()).toEqual([
++ { id: 'e1', owner_id: null },
++ { id: 'e2', owner_id: 'o0' },
])
-- expect(store.players.listByToken('ovn:57')).toEqual([])
-- expect(store.identity.subjectForLegacy('ovn:57')).toBe(SUBJECT)
-- expect(store.identity.legacyAccountKeys('ovn:')).toEqual([])
--
-- // Idempotent: nothing left to move, the map is unchanged.
-- expect(store.identity.adoptLegacyAccount('ovn:57', SUBJECT, 'network', 200)).toEqual({
-- moved: 0,
-- conflicts: 0,
++ expect(store.identity.eraseSubjects([SUBJECT])).toEqual({
++ characters: 0,
++ structures_unowned: 0,
++ identity_rows: 0,
})
-- expect(store.players.listByToken(SUBJECT)).toHaveLength(2)
-- store.close()
-- })
--
-- it('never overwrites a slot the subject already uses', () => {
-- const store = openSqliteStore(':memory:')
-- store.players.upsertMany([
-- player('new0', SUBJECT, 0, { subjectId: SUBJECT }),
-- player('old0', 'ovn:9', 0),
-- player('old2', 'ovn:9', 2),
-- ])
-- expect(store.identity.adoptLegacyAccount('ovn:9', SUBJECT, 'network', 1)).toEqual({
-- moved: 1,
-- conflicts: 1,
-- })
-- expect(store.players.findByTokenSlot(SUBJECT, 0)?.id).toBe('new0')
-- expect(store.players.findByTokenSlot(SUBJECT, 2)?.id).toBe('old2')
-- // The conflicting character stays reachable under its legacy key.
-- expect(store.players.findByTokenSlot('ovn:9', 0)?.id).toBe('old0')
-- store.close()
-- })
--
-- it('refuses to re-home a legacy key to a different subject', () => {
-- const store = openSqliteStore(':memory:')
-- store.players.upsert(player('p1', 'ovn:1', 0))
-- store.identity.adoptLegacyAccount('ovn:1', SUBJECT, 'network', 1)
-- store.players.upsert(player('p9', 'ovn:1', 1))
-- expect(() => store.identity.adoptLegacyAccount('ovn:1', OTHER, 'network', 2)).toThrow(
-- /already mapped/,
-- )
-- // Nothing moved by the refused call.
-- expect(store.players.findById('p9')?.token).toBe('ovn:1')
store.close()
})
++})
++describe('player subject_id', () => {
it('keeps subject_id through ordinary saves that do not carry it', () => {
const store = openSqliteStore(':memory:')
store.players.upsert(player('p1', SUBJECT, 0, { subjectId: SUBJECT }))
ddiff --git a/packages/persistence/src/sqlite/platformRepositories.ts b/packages/persistence/src/sqlite/platformRepositories.ts
iindex 845210d..7eb7b35 100644
---- a/packages/persistence/src/sqlite/platformRepositories.ts
++++ b/packages/persistence/src/sqlite/platformRepositories.ts
@@@ -12,63 +12,38 @@ import type {
import type { IdentityRepository, MediaMirrorRepository, ModRepository } from '../repositories.js'
/**
-- * SQLite repositories for the platform-integration tables (schema 12+): the
-- * legacy identity map, the mod registry and the Media mirror queue. The game
-- * simulation never touches these; apps/server's platform adapters do.
++ * SQLite repositories for the platform-integration tables: the adoption
++ * ledger, the mod registry and the Media mirror queue. The game simulation
++ * never touches these; apps/server's platform adapters do.
*/
export function createIdentityRepository(db: Database.Database): IdentityRepository {
const takenSlots = db.prepare<[string], { char_slot: number }>(
'SELECT char_slot FROM players WHERE token = ?',
)
-- const legacyRows = db.prepare<[string], { id: string; char_slot: number }>(
++ const rowsByToken = db.prepare<[string], { id: string; char_slot: number }>(
'SELECT id, char_slot FROM players WHERE token = ? ORDER BY char_slot',
)
-- const moveRow = db.prepare('UPDATE players SET token = ?, subject_id = ? WHERE id = ?')
-- const recordMap = db.prepare(`
-- INSERT INTO identity_legacy_map (legacy_key, subject_id, source, moved, conflicts, adopted_at)
++ const moveToSlot = db.prepare(
++ 'UPDATE players SET token = ?, subject_id = ?, char_slot = ? WHERE id = ?',
++ )
++ const recordAdoption = db.prepare(`
++ INSERT INTO identity_adoptions (adoption_key, subject_id, source, moved, conflicts, adopted_at)
VALUES (?, ?, ?, ?, ?, ?)
-- ON CONFLICT(legacy_key) DO UPDATE SET
-- moved = identity_legacy_map.moved + excluded.moved,
++ ON CONFLICT(adoption_key) DO UPDATE SET
++ moved = identity_adoptions.moved + excluded.moved,
conflicts = excluded.conflicts
`)
-- const mapped = db.prepare<[string], { subject_id: string }>(
-- 'SELECT subject_id FROM identity_legacy_map WHERE legacy_key = ?',
++ const adopted = db.prepare<[string], { subject_id: string }>(
++ 'SELECT subject_id FROM identity_adoptions WHERE adoption_key = ?',
)
-- const adopt = db.transaction(
-- (legacyKey: string, subjectId: string, source: string, now: number) => {
-- const existing = mapped.get(legacyKey)
-- if (existing && existing.subject_id !== subjectId) {
-- // A legacy key belongs to exactly one subject; a different answer
-- // later is a data problem to look at, never a silent re-home.
-- throw new Error(
-- `legacy key ${legacyKey} is already mapped to ${existing.subject_id}, not ${subjectId}`,
-- )
-- }
-- const taken = new Set(takenSlots.all(subjectId).map((r) => r.char_slot))
-- let moved = 0
-- let conflicts = 0
-- for (const row of legacyRows.all(legacyKey)) {
-- if (taken.has(row.char_slot)) {
-- conflicts++
-- continue
-- }
-- moveRow.run(subjectId, subjectId, row.id)
-- taken.add(row.char_slot)
-- moved++
-- }
-- recordMap.run(legacyKey, subjectId, source, moved, conflicts, now)
-- return { moved, conflicts }
-- },
-- )
--
-- const moveToSlot = db.prepare('UPDATE players SET token = ?, subject_id = ?, char_slot = ? WHERE id = ?')
const adoptGuest = db.transaction((guestToken: string, subjectId: string, now: number) => {
++ // Once per guest token, remembered by a hash of it, never the token.
const key = `guest:${createHash('sha256').update(guestToken).digest('hex').slice(0, 32)}`
const none = { moved: 0, slot: null, full: false }
-- if (mapped.get(key)) return none
-- const rows = legacyRows.all(guestToken)
++ if (adopted.get(key)) return none
++ const rows = rowsByToken.all(guestToken)
if (rows.length === 0) return none
const taken = new Set(takenSlots.all(subjectId).map((r) => r.char_slot))
let moved = 0
@@@ -82,44 +57,44 @@ export function createIdentityRepository(db: Database.Database): IdentityReposit
moved++
}
if (moved === 0) return { moved: 0, slot: null, full: true }
-- recordMap.run(key, subjectId, 'guest', moved, rows.length - moved, now)
++ recordAdoption.run(key, subjectId, 'guest', moved, rows.length - moved, now)
return { moved, slot, full: false }
})
// Account merge (ADR-029): the folded-in subject's characters join the survivor, once per merge.
-- const mergeAccount = db.transaction((from: string, into: string, mergeId: string, now: number) => {
-- const key = `merge:${mergeId}`
-- if (mapped.get(key)) return { moved: 0, kept: 0, already: true }
-- const taken = new Set(takenSlots.all(into).map((r) => r.char_slot))
-- const rows = legacyRows.all(from)
-- let moved = 0
-- // First every character whose own slot is free there, then the rest into what is left.
-- const clashing = rows.filter((row) => {
-- if (taken.has(row.char_slot)) return true
-- moveToSlot.run(into, into, row.char_slot, row.id)
-- taken.add(row.char_slot)
-- moved++
-- return false
-- })
-- let kept = 0
-- for (const row of clashing) {
-- const slot = [0, 1, 2].find((s) => !taken.has(s))
-- if (slot === undefined) {
-- kept++
-- continue
++ const mergeAccount = db.transaction(
++ (from: string, into: string, mergeId: string, now: number) => {
++ const key = `merge:${mergeId}`
++ if (adopted.get(key)) return { moved: 0, kept: 0, already: true }
++ const taken = new Set(takenSlots.all(into).map((r) => r.char_slot))
++ const rows = rowsByToken.all(from)
++ let moved = 0
++ // First every character whose own slot is free there, then the rest into what is left.
++ const clashing = rows.filter((row) => {
++ if (taken.has(row.char_slot)) return true
++ moveToSlot.run(into, into, row.char_slot, row.id)
++ taken.add(row.char_slot)
++ moved++
++ return false
++ })
++ let kept = 0
++ for (const row of clashing) {
++ const slot = [0, 1, 2].find((s) => !taken.has(s))
++ if (slot === undefined) {
++ kept++
++ continue
++ }
++ moveToSlot.run(into, into, slot, row.id)
++ taken.add(slot)
++ moved++
}
-- moveToSlot.run(into, into, slot, row.id)
-- taken.add(slot)
-- moved++
-- }
-- recordMap.run(key, into, 'merge', moved, kept, now)
-- return { moved, kept, already: false }
-- })
++ recordAdoption.run(key, into, 'merge', moved, kept, now)
++ return { moved, kept, already: false }
++ },
++ )
// Account export and deletion (ADR-033). A character row keyed by the subject (token) or carrying it (subject_id).
const inList = (n: number): string => `(${Array.from({ length: n }, () => '?').join(',')})`
-- const hasTable = (name: string): boolean =>
-- !!db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
const parsed = (row: Record<string, unknown>): Record<string, unknown> => {
const out: Record<string, unknown> = {}
for (const [k, v] of Object.entries(row)) {
@@@ -142,46 +117,42 @@ export function createIdentityRepository(db: Database.Database): IdentityReposit
.all(subject, subject) as Record<string, unknown>[]
const ids = characters.map((c) => String(c.id))
const structures = ids.length
-- ? (db.prepare(`SELECT * FROM world_entities WHERE owner_id IN ${inList(ids.length)}`).all(...ids) as Record<string, unknown>[])
-- : []
-- const legacyLiveRows = hasTable('legacy_live_rows')
-- ? (db.prepare('SELECT source_table, source_key, payload, imported_at FROM legacy_live_rows WHERE subject_id = ?').all(subject) as Record<string, unknown>[])
++ ? (db
++ .prepare(`SELECT * FROM world_entities WHERE owner_id IN ${inList(ids.length)}`)
++ .all(...ids) as Record<string, unknown>[])
: []
-- return { characters: characters.map(parsed), structures: structures.map(parsed), legacyLiveRows: legacyLiveRows.map(parsed) }
++ return { characters: characters.map(parsed), structures: structures.map(parsed) }
}
const eraseSubjects = db.transaction((subjects: string[]) => {
-- const out = { characters: 0, structures_unowned: 0, legacy_rows: 0, identity_rows: 0 }
++ const out = { characters: 0, structures_unowned: 0, identity_rows: 0 }
if (!subjects.length) return out
const q = inList(subjects.length)
-- const ids = (db.prepare(`SELECT id FROM players WHERE token IN ${q} OR subject_id IN ${q}`).all(...subjects, ...subjects) as { id: string }[]).map((r) => r.id)
++ const ids = (
++ db
++ .prepare(`SELECT id FROM players WHERE token IN ${q} OR subject_id IN ${q}`)
++ .all(...subjects, ...subjects) as { id: string }[]
++ ).map((r) => r.id)
if (ids.length) {
-- out.structures_unowned = db.prepare(`UPDATE world_entities SET owner_id = NULL WHERE owner_id IN ${inList(ids.length)}`).run(...ids).changes
-- out.characters = db.prepare(`DELETE FROM players WHERE id IN ${inList(ids.length)}`).run(...ids).changes
++ out.structures_unowned = db
++ .prepare(
++ `UPDATE world_entities SET owner_id = NULL WHERE owner_id IN ${inList(ids.length)}`,
++ )
++ .run(...ids).changes
++ out.characters = db
++ .prepare(`DELETE FROM players WHERE id IN ${inList(ids.length)}`)
++ .run(...ids).changes
}
-- if (hasTable('legacy_live_rows')) out.legacy_rows = db.prepare(`DELETE FROM legacy_live_rows WHERE subject_id IN ${q}`).run(...subjects).changes
-- out.identity_rows = db.prepare(`DELETE FROM identity_legacy_map WHERE subject_id IN ${q}`).run(...subjects).changes
++ out.identity_rows = db
++ .prepare(`DELETE FROM identity_adoptions WHERE subject_id IN ${q}`)
++ .run(...subjects).changes
return out
})
return {
exportSubject,
eraseSubjects: (subjects) => eraseSubjects(subjects),
-- adoptLegacyAccount: (legacyKey, subjectId, source, now) =>
-- adopt(legacyKey, subjectId, source, now),
mergeSubject: (from, into, mergeId, now) => mergeAccount(from, into, mergeId, now),
adoptGuestCharacter: (guestToken, subjectId, now) => adoptGuest(guestToken, subjectId, now),
-- subjectForLegacy(legacyKey) {
-- return mapped.get(legacyKey)?.subject_id ?? null
-- },
-- legacyAccountKeys(prefix) {
-- // substr, not LIKE: `_` and `%` in a prefix must not act as wildcards.
-- const rows = db
-- .prepare(
-- 'SELECT DISTINCT token FROM players WHERE substr(token, 1, length(@p)) = @p ORDER BY token',
-- )
-- .all({ p: prefix }) as { token: string }[]
-- return rows.map((r) => r.token)
-- },
}
}
ddiff --git a/packages/persistence/src/sqlite/sqliteStore.test.ts b/packages/persistence/src/sqlite/sqliteStore.test.ts
iindex 889ce6f..5312460 100644
---- a/packages/persistence/src/sqlite/sqliteStore.test.ts
++++ b/packages/persistence/src/sqlite/sqliteStore.test.ts
@@@ -137,8 +137,8 @@ describe('sqlite store', () => {
expect(player?.skills).toEqual({})
expect(player?.friends).toEqual([])
expect(player?.appearance).toBeNull()
-- // constraints table exists and works post-migration (incl. the v8
-- // params column)
++ // constraints table exists and works post-migration (incl. the params
++ // column added by a later step)
store.constraints.upsertMany([
{ id: 'c1', type: 'weld', entityA: 'x', entityB: 'y', params: null, updatedAt: 1 },
{
@@@ -153,14 +153,8 @@ describe('sqlite store', () => {
const loaded = store.constraints.loadAll()
expect(loaded).toHaveLength(2)
expect(loaded.find((c) => c.id === 'c2')?.params).toEqual({ length: 2 })
-- expect(store.meta.get('schema_version')).toBe('13')
-- // v13 legacy archive table exists.
-- expect(
-- store.db
-- .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='legacy_live_rows'")
-- .get(),
-- ).toBeTruthy()
-- // v9 armor column round-trips.
++ expect(store.meta.get('schema_version')).toBe('11')
++ // The armor column round-trips.
store.players.upsert({
...store.players.findByToken('tok_11111111')!,
armor: { defId: 'padded_jacket', count: 1, meta: { dur: 5 } },
ddiff --git a/packages/persistence/src/sqlite/sqliteStore.ts b/packages/persistence/src/sqlite/sqliteStore.ts
iindex a4e7584..ae66497 100644
---- a/packages/persistence/src/sqlite/sqliteStore.ts
++++ b/packages/persistence/src/sqlite/sqliteStore.ts
@@@ -5,7 +5,6 @@ import type {
ConstraintRepository,
MetaRepository,
PersistenceStore,
-- GuestRepository,
PlayerRepository,
WorldEntityRepository,
} from '../repositories.js'
@@@ -33,7 +32,7 @@ export interface SqlitePersistenceStore extends PersistenceStore {
* a database is upgraded step by step inside a transaction per step.
*/
--const SCHEMA_VERSION = 13
++const SCHEMA_VERSION = 11
const BASE_SCHEMA = `
CREATE TABLE IF NOT EXISTS world_entities (
@@@ -75,10 +74,6 @@ CREATE TABLE IF NOT EXISTS constraints (
params TEXT,
updated_at INTEGER NOT NULL
);
--CREATE TABLE IF NOT EXISTS guest_ips (
-- ip TEXT PRIMARY KEY,
-- token TEXT NOT NULL
--);
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
@@@ -86,14 +81,15 @@ CREATE TABLE IF NOT EXISTS meta (
`
/**
-- * Platform integration tables (schema 12): canonical identity, the mod
-- * registry and the Media mirror queue. Shared by the fresh-database path and
-- * the 11 -> 12 migration so both end up identical.
++ * Platform integration tables: canonical subject accounts, the adoption
++ * ledger, the mod registry and the Media mirror queue. Shared by the
++ * fresh-database path and the migration that adds them so both end up
++ * identical.
*/
const PLATFORM_SCHEMA = `
CREATE INDEX IF NOT EXISTS idx_players_subject ON players (subject_id);
--CREATE TABLE IF NOT EXISTS identity_legacy_map (
-- legacy_key TEXT PRIMARY KEY,
++CREATE TABLE IF NOT EXISTS identity_adoptions (
++ adoption_key TEXT PRIMARY KEY,
subject_id TEXT NOT NULL,
source TEXT NOT NULL,
moved INTEGER NOT NULL DEFAULT 0,
@@@ -156,26 +152,6 @@ CREATE TABLE IF NOT EXISTS media_mirrors (
CREATE INDEX IF NOT EXISTS idx_media_mirrors_due ON media_mirrors (status, next_attempt_at);
`
--/**
-- * Rows imported from OpenVibe.Live's retired HoboQuest game (schema 13),
-- * written only by apps/server/scripts/importLiveLegacy.ts. Scraplandia has no
-- * equivalent for them, so each source row is kept verbatim (canonical JSON)
-- * under its owner's Network subject; `(source_table, source_key)` is the
-- * source row's primary key, which makes the import idempotent.
-- */
--const LEGACY_SCHEMA = `
--CREATE TABLE IF NOT EXISTS legacy_live_rows (
-- source_table TEXT NOT NULL,
-- source_key TEXT NOT NULL,
-- live_user_id INTEGER NOT NULL,
-- subject_id TEXT NOT NULL,
-- payload TEXT NOT NULL,
-- imported_at INTEGER NOT NULL,
-- PRIMARY KEY (source_table, source_key)
--);
--CREATE INDEX IF NOT EXISTS idx_legacy_live_rows_subject ON legacy_live_rows (subject_id);
--`
--
/** Migration from version N applies index N-1. Each runs in a transaction. */
const MIGRATIONS: Record<number, (db: Database.Database) => void> = {
1: (db) => {
@@@ -225,43 +201,31 @@ const MIGRATIONS: Record<number, (db: Database.Database) => void> = {
`)
},
6: (db) => {
-- db.exec(`
-- CREATE TABLE IF NOT EXISTS guest_ips (
-- ip TEXT PRIMARY KEY,
-- token TEXT NOT NULL
-- );
-- `)
-- },
-- 7: (db) => {
// Sandbox constraint set: type-specific parameters (anchors, axes,
// lengths, limits, motor). Existing weld rows carry NULL params.
db.exec('ALTER TABLE constraints ADD COLUMN params TEXT;')
},
-- 8: (db) => {
++ 7: (db) => {
// Worn armor (one slot; durability rides in the stack meta).
db.exec('ALTER TABLE players ADD COLUMN armor TEXT;')
},
-- 9: (db) => {
++ 8: (db) => {
// Faction reputation scores.
db.exec("ALTER TABLE players ADD COLUMN reputation TEXT NOT NULL DEFAULT '{}';")
},
-- 10: (db) => {
++ 9: (db) => {
// Blueprint unlocks + active contract.
db.exec(`
ALTER TABLE players ADD COLUMN unlocks TEXT NOT NULL DEFAULT '[]';
ALTER TABLE players ADD COLUMN active_job TEXT;
`)
},
-- 11: (db) => {
++ 10: (db) => {
// Platform integration: canonical subject per account (ADR-0006), the
-- // legacy identity map, the mod registry and the Media mirror queue.
++ // adoption ledger, the mod registry and the Media mirror queue.
db.exec('ALTER TABLE players ADD COLUMN subject_id TEXT;')
db.exec(PLATFORM_SCHEMA)
},
-- 12: (db) => {
-- // Archive of OpenVibe.Live's HoboQuest rows (docs/legacy-import.md).
-- db.exec(LEGACY_SCHEMA)
-- },
}
interface WorldEntityRow {
@@@ -324,7 +288,6 @@ export function openSqliteStore(path: string): SqlitePersistenceStore {
// Fresh database: create the full current schema.
db.exec(BASE_SCHEMA)
db.exec(PLATFORM_SCHEMA)
-- db.exec(LEGACY_SCHEMA)
db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run(
'schema_version',
String(SCHEMA_VERSION),
@@@ -548,32 +511,10 @@ export function openSqliteStore(path: string): SqlitePersistenceStore {
},
}
-- const guests: GuestRepository = {
-- // Guest identity: the browser token is primary; the IP is the recovery
-- // path. A token that already owns a character keeps it (and re-binds its
-- // IP after a network change); a FRESH token from a known IP inherits
-- // that IP's existing scrapper (cleared cookies / new browser at home).
-- resolve(ip: string, token: string): string {
-- const hasCharacter = db.prepare('SELECT 1 FROM players WHERE token = ? LIMIT 1').get(token)
-- if (hasCharacter) {
-- db.prepare(
-- 'INSERT INTO guest_ips (ip, token) VALUES (?, ?) ON CONFLICT(ip) DO UPDATE SET token = excluded.token',
-- ).run(ip, token)
-- return token
-- }
-- const mapped = db.prepare('SELECT token FROM guest_ips WHERE ip = ?').get(ip) as
-- { token: string } | undefined
-- if (mapped) return mapped.token
-- db.prepare('INSERT OR REPLACE INTO guest_ips (ip, token) VALUES (?, ?)').run(ip, token)
-- return token
-- },
-- }
--
return {
db,
worldEntities,
players,
-- guests,
constraints,
meta,
identity: createIdentityRepository(db),