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:
Changes
@@@ -1,5 +1,6 @@node_modules/dist/++dist-types/*.tsbuildinfo.env*.log@@@ -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 describingit. 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@@@ -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 @openvThe server serves the built client, `/healthz` (liveness), `/api/ready`, `/metrics`, and the gameWebSocket 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 routeanswers 404 (an HTML page with links to those pages; `{"error":"not_found"}` under `/api`; plain textfor a missing asset), never the portal with a 200. `/api/ready` is 200 only while `world.db` answers a query and the simulationhas 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 checkoutrestored, reinstalled and rebuilt and the server restarted again. Players online are reported andreconnect; `--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 onlydo 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 aclient-credentials token of the `games` OAuth client (same client id andsecret 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 Mediamirror are on. `games.world.saved` events reach OpenVibe.Events; no mod isinstalled, 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.@@@ -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 onereal playable vertical implementation.--See `docs/GAMEPLAY_DEVELOPMENT_STATUS.md` for the detailed per-system audit.--## Phase 0 — Vertical slice ✅ doneProved 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; planterfarming 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; guestidentity + openvibe.network SSO ranks; map editor as a mature authoring subsystemwith 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 implementedddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=architectureAudit.audit.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"architectureAudit.audit.d.ts","sourceRoot":"","sources":["../../src/editor/architectureAudit.audit.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=assetController.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=bindings.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"bindings.test.d.ts","sourceRoot":"","sources":["../../src/editor/bindings.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=lockController.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=editorDocument.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=editorObject.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export declare function bootEditor(): Promise<void>;--//# sourceMappingURL=editorApp.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=commandHistory.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=commands.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=actionRouter.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=editorPicker.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=interactionController.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=paintableSurface.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=editorProfiler.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=draftStore.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=faceSelection.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=selectionManager.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=lightZoneTool.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=terrainTool.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=toolManager.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=inspector.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=numberField.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=outliner.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"panels.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/panels.ts"],"names":[],"mappings":"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"}\\ No newline at end of fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=panels.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=editorPreferences.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=editorViewRegistry.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=transformMath.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=transformSession.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=main.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--import '@babylonjs/core/Meshes/instancedMesh.js';--//# sourceMappingURL=paintedStatic.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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@@@ -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)},)}@@@ -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',]@@@ -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))@@@ -113,8 +113,7 @@ export function createSaveController(opts: SaveControllerOptions): SaveControlleconst 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): SaveControllesetMessage('⛔ 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): SaveControlleif (isDirty() && !confirm('Replace your unsaved work with the imported map?')) returnopts.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 }),@@@ -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 = 0return {base: {...(t.tex && t.tex !== 'none' ? { tex: t.tex } : {}),...(t.color ? { color: t.color } : {}),},-- ...(t.mix ? { paint: migrateLegacyMix(t.mix, () => `pl-${this.id}-${seq++}`)! } : {}),}}@@@ -65,8 +65,7 @@ async function start(): Promise<void> {let liveMap: MapFileV2 = emptyMapV2()let mapLightsBoot: MapLight[] | undefinedtry {-- // /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 {@@@ -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 ?? nullif (!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') }}@@@ -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 = 0export 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 ?? nullif (surface) {const layered = new LayeredSurfaceMaterial(scene, `psurf:${patch.id}`, surface, {baseTiling: Math.max(2, patch.halfExtent / 2),ddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=mapProvenance.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"mapProvenance.test.d.ts","sourceRoot":"","sources":["../../src/game/mapProvenance.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=mapStaticLayer.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"mapStaticLayer.test.d.ts","sourceRoot":"","sources":["../../src/game/mapStaticLayer.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=mapTerrainLayer.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"mapTerrainLayer.test.d.ts","sourceRoot":"","sources":["../../src/game/mapTerrainLayer.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=platformIntegration.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"platformIntegration.test.d.ts","sourceRoot":"","sources":["../../src/game/platformIntegration.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=main.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=manifest.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"manifest.test.d.ts","sourceRoot":"","sources":["../../src/mods/manifest.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=moderationEvents.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"moderationEvents.test.d.ts","sourceRoot":"","sources":["../../src/mods/moderationEvents.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=routes.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"routes.test.d.ts","sourceRoot":"","sources":["../../src/mods/routes.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=runtime.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"runtime.test.d.ts","sourceRoot":"","sources":["../../src/mods/runtime.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=editorLocks.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"editorLocks.test.d.ts","sourceRoot":"","sources":["../../src/net/editorLocks.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=httpServer.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"httpServer.test.d.ts","sourceRoot":"","sources":["../../src/net/httpServer.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=mapAssetStore.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"mapAssetStore.test.d.ts","sourceRoot":"","sources":["../../src/net/mapAssetStore.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=mapStore.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"mapStore.test.d.ts","sourceRoot":"","sources":["../../src/net/mapStore.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=networkAuth.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"networkAuth.test.d.ts","sourceRoot":"","sources":["../../src/net/networkAuth.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=sso.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"sso.test.d.ts","sourceRoot":"","sources":["../../src/net/sso.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=readiness.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"readiness.test.d.ts","sourceRoot":"","sources":["../../src/observability/readiness.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=release.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"release.test.d.ts","sourceRoot":"","sources":["../../src/observability/release.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=accounts.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"accounts.test.d.ts","sourceRoot":"","sources":["../../src/platform/accounts.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"gameEvents.d.ts","sourceRoot":"","sources":["../../src/platform/gameEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD,eAAO,MAAM,YAAY,UAAU,CAAA;AAEnC,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,qCAAqC;IACrC,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAID,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,UAAU,CAI7D;AAkBD,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,WAAW,EACd,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GACzC,UAAU,CASZ;AAED,wBAAgB,eAAe,CAC7B,CAAC,EAAE,WAAW,EACd,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GAC9C,UAAU,CAaZ;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAC5B,CAAC,EAAE,WAAW,EACd,MAAM,EAAE,gBAAgB,EACxB,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,MAAM,GACZ,UAAU,EAAE,CA4Bd;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;IAAE,MAAM,EAAE,YAAY,GAAG,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC9F,UAAU,CAeZ;AAED,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,gBAAgB,CAAA;AAE9F,wBAAgB,QAAQ,CACtB,IAAI,EAAE,YAAY,EAClB,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EACrF,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,UAAU,CAkBZ;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACjE,KAAK,EAAE,UAAU,EACjB,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAO,GACvE,UAAU,CAeZ;AAED,oFAAoF;AACpF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;CACjC;AAED,eAAO,MAAM,SAAS,EAAE,SAAiD,CAAA;AAEzE,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAQpD;AAQD;;;;;GAKG;AACH,qBAAa,iBAAiB;IAQ1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG;IAVtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,gBAAgB,CAAQ;gBAGb,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,MAAM,EACb,iBAAiB,EAAE,MAAM,EACzB,GAAG,GAAE,MAAM,MAAiB;IAM/C,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,wEAAwE;IACxE,UAAU,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI;IASpF;;;OAGG;IACH,kBAAkB,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,gBAAgB,CAAA;KAAE,EAAE,GAAG,MAAM,IAAI;IAmB5F,0EAA0E;IAC1E,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,IAAI;IAS5C;;;;OAIG;IACH,gBAAgB,CACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,YAAY,GAAG,UAAU,GAChC,MAAM,IAAI;CAwBd"}\\ No newline at end of fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=gameEvents.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"gameEvents.test.d.ts","sourceRoot":"","sources":["../../src/platform/gameEvents.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=identityMigration.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"identityMigration.test.d.ts","sourceRoot":"","sources":["../../src/platform/identityMigration.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=liveLegacyImport.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"liveLegacyImport.test.d.ts","sourceRoot":"","sources":["../../src/platform/liveLegacyImport.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=mediaMirror.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"mediaMirror.test.d.ts","sourceRoot":"","sources":["../../src/platform/mediaMirror.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=progressSummary.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"progressSummary.test.d.ts","sourceRoot":"","sources":["../../src/platform/progressSummary.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=proposals.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"proposals.test.d.ts","sourceRoot":"","sources":["../../src/platform/proposals.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=revocationEvents.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"revocationEvents.test.d.ts","sourceRoot":"","sources":["../../src/platform/revocationEvents.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -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 fileddeleted file mode 100644@@@ -1,2 +0,0 @@--export {};--//# sourceMappingURL=shutdown.test.d.ts.map\\ No newline at end of fileddeleted file mode 100644@@@ -1 +0,0 @@--{"version":3,"file":"shutdown.test.d.ts","sourceRoot":"","sources":["../src/shutdown.test.ts"],"names":[],"mappings":""}\\ No newline at end of fileddeleted file mode 100644@@@ -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)-- },--)ddeleted file mode 100644@@@ -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)--})@@@ -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,},@@@ -7,8 +7,6 @@ export interface ServerConfig {mapPath: stringeditorKey: string | nullnetworkAuthUrl: 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',@@@ -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: stringsend(text: string): voidclose(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.tokenlet subjectId: string | null = nulllet authIat: number | null = nulllet rank: 'owner' | 'admin' | 'moderator' | null = nullif (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.subjectIdauthIat = 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@@@ -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 = nullconst 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()@@@ -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 = tokenif (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 []@@@ -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: stringsubject: SubjectRef}@@@ -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')@@@ -167,7 +167,7 @@ export function createHttpServer(if (platform?.handle?.(req, res)) returnif (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) ?? undefinedvoid 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@@@ -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.@@@ -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 () => {@@@ -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())@@@ -20,8 +20,8 @@ export function attachWebSocket(http: Server, game: GameServer, log: Logger): Wevoid httpwss.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): Welet windowStart = Date.now()const conn: GameConnection = {-- ip: remote,send: (text) => {if (ws.readyState === ws.OPEN) ws.send(text)},@@@ -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 = falseconst 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 = trueawait 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)@@@ -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 constconst 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'++ : nullif (!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@@@ -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()-- })--})@@@ -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)_/iexport 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 }-- }--}ddeleted file mode 100644@@@ -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()-- })--})ddeleted file mode 100644@@@ -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--}ddeleted file mode 100644@@@ -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()-- })--})ddeleted file mode 100644@@@ -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')--}@@@ -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' } }@@@ -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',@@@ -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).ddeleted file mode 100644@@@ -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).@@@ -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## Decisionnnew file mode 100644@@@ -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.ddeleted file mode 100644@@@ -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>--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--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--```@@@ -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()')--}@@@ -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', () => {@@@ -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}`@@@ -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')@@@ -135,26 +135,6 @@ export function removeLayer(paint: SurfacePaint, layerId: string): PaintChannelreturn 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,@@@ -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. */@@@ -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: WorldEntityRepositoryreadonly players: PlayerRepository-- guests: GuestRepositoryreadonly constraints: ConstraintRepositoryreadonly meta: MetaRepositoryreadonly identity: IdentityRepository@@@ -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 }))@@@ -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 noneconst taken = new Set(takenSlots.all(subjectId).map((r) => r.char_slot))let moved = 0@@@ -82,44 +57,44 @@ export function createIdentityRepository(db: Database.Database): IdentityRepositmoved++}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 outconst 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).changesreturn 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)-- },}}@@@ -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 } },@@@ -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 = 11const 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),
@@ -1,5 +1,6 @@
@@ -1,5 +1,6 @@
node_modules/
node_modules/
dist/
dist/
+dist-types/
*.tsbuildinfo
*.tsbuildinfo
.env
.env
*.log
*.log
@@ -95,12 +95,9 @@ transform, inventory JSON), `meta` (schema version, world-seeded flag).
@@ -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.
Dirty tracking batches writes; flush on interval, on settle, and on shutdown.
Each flush is one transaction, together with the outbox events describing
Each flush is one transaction, together with the outbox events describing
it. First boot seeds from the world definition; afterwards the DB is the
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
## Platform boundary
@@ -14,7 +14,7 @@ WebSocket protocol · SQLite persistence · pnpm monorepo.
@@ -14,7 +14,7 @@ WebSocket protocol · SQLite persistence · pnpm monorepo.
## Owns
## Owns
- the game: the authoritative simulation, the world, characters, inventories, skills, blueprints and
- 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
- the map editor and its assets, the mod registry (`games-content@1` packs, grants, audit) and the
`games.*` events
`games.*` events
- the game WebSocket protocol and the portal, `/play` and `/editor` pages
- 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
@@ -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
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
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
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
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`
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).
@@ -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
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
`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
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
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`,
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
@@ -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).
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
- **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
- **Service principal.** Calls to Events, Media and Network identity carry a
client-credentials token of the `games` OAuth client (same client id and
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
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
@@ -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
- **In production (2026-09-23, release `f11e21c`):** events and the Media
mirror are on. `games.world.saved` events reach OpenVibe.Events; no mod is
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.
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
- **Shared chrome.** The portal, `/play` and `/editor` load the OpenVibe Frame
(navbar, footer, theme loader) from this server's own pinned copy at `/shared/`
(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.
(`apps/server/src/net/sharedAssets.ts`), so they keep their frame while Network is down.
@@ -5,8 +5,6 @@ incomplete, **next** = current development target, **future** = not started.
@@ -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
A system is never "done" because a schema exists — it must have at least one
real playable vertical implementation.
real playable vertical implementation.
-See `docs/GAMEPLAY_DEVELOPMENT_STATUS.md` for the detailed per-system audit.
-
## Phase 0 — Vertical slice ✅ done
## Phase 0 — Vertical slice ✅ done
Proved every major system cooperates end-to-end (verified by
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,
@@ -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;
death/respawn; melee PvP with weapon capability, knockback, zone gating;
container props (storage box) with trusted access; hinged doors; planter
container props (storage box) with trusted access; hinged doors; planter
farming prototype (berry seeds, timestamp growth); merchant trades (bottle
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
identity + openvibe.network SSO ranks; map editor as a mature authoring subsystem
with live world reconciliation.
with live world reconciliation.
@@ -153,15 +151,11 @@ with live world reconciliation.
@@ -153,15 +151,11 @@ with live world reconciliation.
## Platform integration (network roadmap Wave 12) — partial
## 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;
- waiting on the platform: mod principals and grants in OpenVibe.Network;
sandboxed executable mods (OpenVibe.Host Stage C); `games.progress.summary`
sandboxed executable mods (OpenVibe.Host Stage C); `games.progress.summary`
user-module writes are granted but not implemented
user-module writes are granted but not implemented
deleted file mode 100644
deleted file mode 100644
@@ -1,3 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=architectureAudit.audit.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,62 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=assetController.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,32 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=bindings.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,33 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,68 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,89 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=lockController.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,14 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,47 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,171 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=editorDocument.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,42 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=editorObject.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export declare function bootEditor(): Promise<void>;
-//# sourceMappingURL=editorApp.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,107 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=commandHistory.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,91 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=commands.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,54 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=actionRouter.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,79 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=editorPicker.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,65 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=interactionController.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,69 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,9 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,63 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,67 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,61 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,74 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=paintableSurface.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,10 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,43 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,38 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=editorProfiler.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,95 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=draftStore.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,60 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=faceSelection.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,76 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=selectionManager.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,71 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,26 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=lightZoneTool.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,68 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,43 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=terrainTool.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,43 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=toolManager.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,30 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,41 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,33 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,69 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,92 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=inspector.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,27 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,74 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=numberField.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,64 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=outliner.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,106 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -1 +0,0 @@
-{"version":3,"file":"panels.d.ts","sourceRoot":"","sources":["../../../src/editor/ui/panels.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=panels.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,19 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,65 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,77 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,47 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=editorPreferences.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,95 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=editorViewRegistry.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,64 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,59 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=transformMath.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,72 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=transformSession.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,71 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -1 +0,0 @@
-{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/editor/viewport/views/index.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,100 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,56 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,49 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,12 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,77 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=main.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -1 +0,0 @@
-{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,26 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,41 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,57 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,12 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,50 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,22 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,17 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,37 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,16 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,41 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,43 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,21 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,27 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,51 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,56 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,58 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,24 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,43 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-import '@babylonjs/core/Meshes/instancedMesh.js';
-//# sourceMappingURL=paintedStatic.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,50 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,95 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,66 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,25 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,157 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,17 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,14 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,81 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,14 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,10 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,37 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,18 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
@@ -960,39 +960,25 @@ await withMap(
@@ -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(
await withMap(
PORT + 2,
PORT + 2,
{
{
v: 1,
v: 1,
halfExtent: HALF,
halfExtent: HALF,
sub: SUB,
sub: SUB,
- heights: v1b64,
+ heights: '',
statics: [
statics: [
{ shape: { type: 'box', size: [4, 4, 4] }, pos: [6, 2, 0], yaw: 0, color: '#c04040' },
{ shape: { type: 'box', size: [4, 4, 4] }, pos: [6, 2, 0], yaw: 0, color: '#c04040' },
],
],
},
},
async (pg) => {
async (pg) => {
+ // Map v2 is the only format (ADR-0007): nothing converts an old document.
const ids = await probeOf(pg, (p) => p.terrainIds())
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())
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)
},
},
)
)
}
}
@@ -8,8 +8,7 @@
@@ -8,8 +8,7 @@
* a test of its own or it quietly becomes permanent.
* a test of its own or it quietly becomes permanent.
*
*
* This audit fails while any legacy construct is still ACTIVE runtime code.
* 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 { readdirSync, readFileSync, statSync } from 'node:fs'
import { extname, join, relative } from 'node:path'
import { extname, join, relative } from 'node:path'
@@ -20,10 +19,6 @@ const SCAN_ROOTS = ['apps/client/src', 'apps/server/src', 'packages/content/src'
@@ -20,10 +19,6 @@ const SCAN_ROOTS = ['apps/client/src', 'apps/server/src', 'packages/content/src'
/** Files that legitimately mention legacy constructs. */
/** Files that legitimately mention legacy constructs. */
const ALLOWED = [
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.
// The audit itself names everything it forbids.
'apps/client/src/editor/architectureAudit.audit.ts',
'apps/client/src/editor/architectureAudit.audit.ts',
]
]
@@ -110,9 +110,9 @@ export async function bootEditor(): Promise<void> {
@@ -110,9 +110,9 @@ export async function bootEditor(): Promise<void> {
env.attachCamera(camera)
env.attachCamera(camera)
// ── Document ────────────────────────────────────────────────────────
// ── 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 bootResp = await fetch('/map.json')
const bootRevision = bootResp.headers.get('etag')?.replace(/"/g, '') ?? ''
const bootRevision = bootResp.headers.get('etag')?.replace(/"/g, '') ?? ''
const bootParsed = parseMapFile(await bootResp.json().catch(() => null))
const bootParsed = parseMapFile(await bootResp.json().catch(() => null))
@@ -113,8 +113,7 @@ export function createSaveController(opts: SaveControllerOptions): SaveControlle
@@ -113,8 +113,7 @@ export function createSaveController(opts: SaveControllerOptions): SaveControlle
const key = editorKey()
const key = editorKey()
// Persist only a hand-typed override token — never the SSO session,
// Persist only a hand-typed override token — never the SSO session,
// which lives in ovg_sso and is resolved fresh on every request.
// 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)
localStorage.setItem('openvibe.editorkey', manual)
setMessage('saving…')
setMessage('saving…')
await uploadDirtyMasks()
await uploadDirtyMasks()
@@ -262,8 +261,7 @@ export function createSaveController(opts: SaveControllerOptions): SaveControlle
@@ -262,8 +261,7 @@ export function createSaveController(opts: SaveControllerOptions): SaveControlle
setMessage('⛔ that file is not JSON')
setMessage('⛔ that file is not JSON')
return
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)
const parsed = parseMapFile(raw)
if (!parsed.ok) {
if (!parsed.ok) {
setMessage(`⛔ import rejected: ${parsed.issues.slice(0, 3).join('; ')}`)
setMessage(`⛔ import rejected: ${parsed.issues.slice(0, 3).join('; ')}`)
@@ -272,9 +270,7 @@ export function createSaveController(opts: SaveControllerOptions): SaveControlle
@@ -272,9 +270,7 @@ export function createSaveController(opts: SaveControllerOptions): SaveControlle
if (isDirty() && !confirm('Replace your unsaved work with the imported map?')) return
if (isDirty() && !confirm('Replace your unsaved work with the imported map?')) return
opts.onAdopt(parsed.map)
opts.onAdopt(parsed.map)
nonHistoryDirt = true
nonHistoryDirt = true
- setMessage(
- parsed.migrated ? '📥 imported (migrated from v1)' : '📥 imported — Save to publish',
- )
+ setMessage('📥 imported — Save to publish')
notify()
notify()
},
},
conflict: () => ({ open: conflictOpen, summary: conflictSummary }),
conflict: () => ({ open: conflictOpen, summary: conflictSummary }),
@@ -24,7 +24,6 @@ import {
@@ -24,7 +24,6 @@ import {
buildPatchGrid,
buildPatchGrid,
decodeHeights,
decodeHeights,
encodeHeights,
encodeHeights,
- migrateLegacyMix,
type ContentRegistry,
type ContentRegistry,
type MapLightV2,
type MapLightV2,
type MapNodeV2,
type MapNodeV2,
@@ -580,15 +579,13 @@ export class TerrainView implements EditorView {
@@ -580,15 +579,13 @@ export class TerrainView implements EditorView {
/** The document surface, defaulted so a plain terrain still paints. */
/** The document surface, defaulted so a plain terrain still paints. */
surfaceData(): SurfaceMaterialData {
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
if (t.surface) return t.surface as SurfaceMaterialData
- let seq = 0
return {
return {
base: {
base: {
...(t.tex && t.tex !== 'none' ? { tex: t.tex } : {}),
...(t.tex && t.tex !== 'none' ? { tex: t.tex } : {}),
...(t.color ? { color: t.color } : {}),
...(t.color ? { color: t.color } : {}),
},
},
- ...(t.mix ? { paint: migrateLegacyMix(t.mix, () => `pl-${this.id}-${seq++}`)! } : {}),
}
}
}
}
@@ -65,8 +65,7 @@ async function start(): Promise<void> {
@@ -65,8 +65,7 @@ async function start(): Promise<void> {
let liveMap: MapFileV2 = emptyMapV2()
let liveMap: MapFileV2 = emptyMapV2()
let mapLightsBoot: MapLight[] | undefined
let mapLightsBoot: MapLight[] | undefined
try {
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())
const parsed = parseMapFile(await (await fetch('/map.json')).json())
if (parsed.ok) {
if (parsed.ok) {
registerMapAssets({
registerMapAssets({
@@ -332,9 +331,9 @@ async function start(): Promise<void> {
@@ -332,9 +331,9 @@ async function start(): Promise<void> {
// A stale cached bundle is talking to a newer server. A reload
// A stale cached bundle is talking to a newer server. A reload
// revalidates the page and pulls the new client. Guard against a
// revalidates the page and pulls the new client. Guard against a
// reload loop if something still pins the old version.
// 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) {
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.setStatus('game updated — loading the new version…')
hud.toast('Game updated! Reloading…', false)
hud.toast('Game updated! Reloading…', false)
setTimeout(() => location.reload(), 1200)
setTimeout(() => location.reload(), 1200)
@@ -347,9 +346,9 @@ async function start(): Promise<void> {
@@ -347,9 +346,9 @@ async function start(): Promise<void> {
localStorage.removeItem('ovg_sso')
localStorage.removeItem('ovg_sso')
document.cookie = 'ovg_sso=; Path=/; Max-Age=0'
document.cookie = 'ovg_sso=; Path=/; Max-Age=0'
localStorage.removeItem('openvibe.appearance')
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) {
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…')
hud.setStatus('session data was out of date — reloading…')
setTimeout(() => location.reload(), 1200)
setTimeout(() => location.reload(), 1200)
} else {
} else {
@@ -33,11 +33,21 @@ export class Connection {
@@ -33,11 +33,21 @@ export class Connection {
// An upgrade sent while the server restarts can wait with no answer (the proxy holds it):
// 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.
// give up after CONNECT_TIMEOUT_MS so the page can wait for the server and reload instead.
const timer = setTimeout(() => {
const timer = setTimeout(() => {
- try { ws.close() } catch { /* already closing */ }
+ try {
+ ws.close()
+ } catch {
+ /* already closing */
+ }
reject(new Error('connection timed out'))
reject(new Error('connection timed out'))
}, CONNECT_TIMEOUT_MS)
}, 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) => {
ws.onmessage = (event) => {
const msg = decodeServerMessage(String(event.data))
const msg = decodeServerMessage(String(event.data))
@@ -68,21 +78,20 @@ export class Connection {
@@ -68,21 +78,20 @@ export class Connection {
/** Persistent anonymous identity (interim auth — see ADR-0004). */
/** 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 } {
export function getIdentity(): { token: string; name: string | null } {
const cookieTok = document.cookie
const cookieTok = document.cookie
.split('; ')
.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
let token = localStorage.getItem('openvibe.token') ?? cookieTok ?? null
if (!token || !/^[a-z0-9]{8,64}$/i.test(token)) {
if (!token || !/^[a-z0-9]{8,64}$/i.test(token)) {
token = crypto.randomUUID().replaceAll('-', '').slice(0, 32)
token = crypto.randomUUID().replaceAll('-', '').slice(0, 32)
}
}
localStorage.setItem('openvibe.token', token)
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') }
return { token, name: localStorage.getItem('openvibe.name') }
}
}
@@ -29,7 +29,6 @@ import {
@@ -29,7 +29,6 @@ import {
type WorldShape,
type WorldShape,
type StaticBody,
type StaticBody,
effectiveShape,
effectiveShape,
- migrateLegacyMix,
type SurfaceMaterialData,
type SurfaceMaterialData,
type StaticObjectV2,
type StaticObjectV2,
} from '@openvibe/content'
} from '@openvibe/content'
@@ -189,7 +188,6 @@ function childAtIndexPath(root: { getChildren?: () => unknown[] }, path: string)
@@ -189,7 +188,6 @@ function childAtIndexPath(root: { getChildren?: () => unknown[] }, path: string)
}
}
/** Extra terrain patches (mountains, cave shells) from the edited map. */
/** Extra terrain patches (mountains, cave shells) from the edited map. */
-let legacyLayer = 0
export function buildTerrainPatches(
export function buildTerrainPatches(
scene: Scene,
scene: Scene,
content: ContentRegistry,
content: ContentRegistry,
@@ -215,20 +213,8 @@ export function buildTerrainPatches(
@@ -215,20 +213,8 @@ export function buildTerrainPatches(
// applies mesh scaling in the same S→R→T order, so the two agree.
// 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])
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
// 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) {
if (surface) {
const layered = new LayeredSurfaceMaterial(scene, `psurf:${patch.id}`, surface, {
const layered = new LayeredSurfaceMaterial(scene, `psurf:${patch.id}`, surface, {
baseTiling: Math.max(2, patch.halfExtent / 2),
baseTiling: Math.max(2, patch.halfExtent / 2),
deleted file mode 100644
deleted file mode 100644
@@ -1,64 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,54 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,202 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,174 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,53 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,69 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=mapProvenance.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,48 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=mapStaticLayer.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,42 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=mapTerrainLayer.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,49 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,28 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=platformIntegration.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,140 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -1 +0,0 @@
-{"version":3,"file":"playerSession.d.ts","sourceRoot":"","sources":["../../src/game/playerSession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EACL,UAAU,EAIV,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClE,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAEtE,eAAO,MAAM,cAAc,KAAK,CAAA;AAChC,eAAO,MAAM,WAAW,IAAI,CAAA;AAE5B,sCAAsC;AACtC,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,QAAQ,CAAA;IAClB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ;+DAC2D;IAC3D,WAAW,EAAE,IAAI,CAAA;IACjB;;kEAE8D;IAC9D,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB;kFAC8E;IAC9E,OAAO,EAAE,IAAI,CAAA;IACb,uDAAuD;IACvD,IAAI,EAAE,OAAO,CAAA;IACb,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,EAAE,QAAQ,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,gFAAgF;IAChF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;OAGG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,2DAA2D;IAC3D,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,IAAI,CAAA;IAC5C,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,eAAe,CAAA;IACrB,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,QAAQ,CAAA;IAChB,wEAAwE;IACxE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACpB,UAAU,EAAE,UAAU,CAAA;IACtB,UAAU,EAAE,UAAU,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAA;IAClB,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAA;IACrB;8EAC0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,yEAAyE;IACzE,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IACvB,8CAA8C;IAC9C,UAAU,EAAE,aAAa,CAAA;IACzB,kDAAkD;IAClD,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACpB,+CAA+C;IAC/C,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACnD,yDAAyD;IACzD,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAA;IACxB,4EAA4E;IAC5E,KAAK,EAAE,aAAa,CAAA;IACpB,+CAA+C;IAC/C,UAAU,EAAE,OAAO,CAAA;IACnB,wEAAwE;IACxE,aAAa,EAAE,QAAQ,GAAG,IAAI,CAAA;IAC9B,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAA;IACnB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAA;IAClB,8EAA8E;IAC9E,OAAO,EAAE,eAAe,CAAA;IACxB,4DAA4D;IAC5D,UAAU,EAAE,WAAW,EAAE,CAAA;IACzB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAA;IACpB,gBAAgB,EAAE,MAAM,CAAA;IACxB,0EAA0E;IAC1E,KAAK,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;IACpB,yDAAyD;IACzD,KAAK,EAAE,OAAO,CAAA;IACd,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACpD;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,EAAE,QAAQ,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,IAAI,CAAA;IAC7C,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,IAAI,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,QAAQ,CAAA;IAChB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACpB,UAAU,EAAE,UAAU,CAAA;IACtB,KAAK,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,SAAS,CAAA;IAC1C,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IACxB,UAAU,CAAC,EAAE,aAAa,CAAA;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,SAAS,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACpD,OAAO,EAAE,eAAe,CAAA;IACxB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACpD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,aAAa,CA4C9D;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAKnE;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAMrE"}
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,23 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,6 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=main.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -1 +0,0 @@
-{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,106 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,55 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=manifest.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,163 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=moderationEvents.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,101 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,30 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=routes.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,89 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=runtime.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,5 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,34 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=editorLocks.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,14 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,38 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,39 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=httpServer.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,46 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=mapAssetStore.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,32 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=mapStore.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,35 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=networkAuth.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,23 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,93 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=sso.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,6 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,45 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,55 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=readiness.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,41 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=release.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,44 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=accounts.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,126 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=gameEvents.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,31 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=identityMigration.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,156 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=liveLegacyImport.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,41 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=mediaMirror.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,36 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=progressSummary.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=proposals.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,59 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=revocationEvents.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,16 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,16 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -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
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,2 +0,0 @@
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=shutdown.test.d.ts.map
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1 +0,0 @@
@@ -1 +0,0 @@
-{"version":3,"file":"shutdown.test.d.ts","sourceRoot":"","sources":["../src/shutdown.test.ts"],"names":[],"mappings":""}
\ No newline at end of file
\ No newline at end of file
deleted file mode 100644
deleted file mode 100644
@@ -1,171 +0,0 @@
@@ -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)
- },
-)
deleted file mode 100644
deleted file mode 100644
@@ -1,40 +0,0 @@
@@ -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)
-})
@@ -77,7 +77,6 @@ function startServer(): Promise<void> {
@@ -77,7 +77,6 @@ function startServer(): Promise<void> {
...process.env,
...process.env,
PORT: String(PORT),
PORT: String(PORT),
DB_PATH: dbPath,
DB_PATH: dbPath,
- GUEST_IP_BINDING: 'off',
EVENT_INTERVAL_SCALE: '0.05',
EVENT_INTERVAL_SCALE: '0.05',
MAP_PATH: mapPath,
MAP_PATH: mapPath,
},
},
@@ -7,8 +7,6 @@ export interface ServerConfig {
@@ -7,8 +7,6 @@ export interface ServerConfig {
mapPath: string
mapPath: string
editorKey: string | null
editorKey: string | null
networkAuthUrl: 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. */
/** openvibe.network OAuth client (SSO); null until the secret is configured. */
oauth: {
oauth: {
clientId: string
clientId: string
@@ -101,7 +99,6 @@ export function loadConfig(env: NodeJS.ProcessEnv): ServerConfig {
@@ -101,7 +99,6 @@ export function loadConfig(env: NodeJS.ProcessEnv): ServerConfig {
(env.OV_OAUTH_CLIENT_SECRET
(env.OV_OAUTH_CLIENT_SECRET
? `${env.OV_NETWORK_URL ?? 'https://openvibe.network'}/api/auth/me`
? `${env.OV_NETWORK_URL ?? 'https://openvibe.network'}/api/auth/me`
: null),
: null),
- guestIpBinding: env.GUEST_IP_BINDING !== 'off',
oauth: env.OV_OAUTH_CLIENT_SECRET
oauth: env.OV_OAUTH_CLIENT_SECRET
? {
? {
clientId: env.OV_OAUTH_CLIENT_ID ?? 'games',
clientId: env.OV_OAUTH_CLIENT_ID ?? 'games',
@@ -68,7 +68,7 @@ import {
@@ -68,7 +68,7 @@ import {
} from '@openvibe/shared'
} from '@openvibe/shared'
import type { ServerConfig } from '../config.js'
import type { ServerConfig } from '../config.js'
import { canEditMap, resolveNetworkUser } from '../net/networkAuth.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 { EventPlayer, GameEventRecorder, ProgressSnapshot } from '../platform/gameEvents.js'
import type { ProgressSummaryWriter } from '../platform/progressSummary.js'
import type { ProgressSummaryWriter } from '../platform/progressSummary.js'
import type { ModHost, ModRuntime } from '../mods/runtime.js'
import type { ModHost, ModRuntime } from '../mods/runtime.js'
@@ -101,8 +101,6 @@ import { EventManager } from './eventManager.js'
@@ -101,8 +101,6 @@ import { EventManager } from './eventManager.js'
/** A network connection as the game sees it — transport-agnostic. */
/** A network connection as the game sees it — transport-agnostic. */
export interface GameConnection {
export interface GameConnection {
- /** Real client IP (Cloudflare-aware) — guest identity hangs off this. */
- ip: string
send(text: string): void
send(text: string): void
close(code: number, reason: string): void
close(code: number, reason: string): void
}
}
@@ -1233,39 +1231,29 @@ export class GameServer {
@@ -1233,39 +1231,29 @@ export class GameServer {
}
}
const slot = msg.slot ?? 0
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 token = msg.token
let subjectId: string | null = null
let subjectId: string | null = null
let authIat: number | null = null
let authIat: number | null = null
let rank: 'owner' | 'admin' | 'moderator' | null = null
let rank: 'owner' | 'admin' | 'moderator' | null = null
if (msg.auth) {
if (msg.auth) {
const user = await resolveNetworkUser(this.config.networkAuthUrl, 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.send(encodeServerMessage({ t: 'reject', reason: 'auth_failed' }))
conn.close(4009, 'auth_failed')
conn.close(4009, 'auth_failed')
return
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)
authIat = tokenIat(msg.auth)
rank = user.rank
rank = user.rank
// Guest conversion (WS-B task 8): the character this browser played as a guest joins the
// Guest conversion (WS-B task 8): the character this browser played as a guest joins the
// account in its first free slot, once.
// 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())
const g = this.store.identity.adoptGuestCharacter(msg.token, subjectId, Date.now())
if (g.moved > 0)
if (g.moved > 0)
this.log.info('guest character adopted', { subject: subjectId, slot: g.slot ?? -1 })
this.log.info('guest character adopted', { subject: subjectId, slot: g.slot ?? -1 })
@@ -1273,9 +1261,9 @@ export class GameServer {
@@ -1273,9 +1261,9 @@ export class GameServer {
this.log.info('guest character kept: account slots full', { subject: subjectId })
this.log.info('guest character kept: account slots full', { subject: subjectId })
}
}
} else {
} 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)) {
if (!isGuestToken(msg.token)) {
conn.send(encodeServerMessage({ t: 'reject', reason: 'invalid_hello' }))
conn.send(encodeServerMessage({ t: 'reject', reason: 'invalid_hello' }))
conn.close(4007, 'invalid_guest_token')
conn.close(4007, 'invalid_guest_token')
@@ -1286,9 +1274,6 @@ export class GameServer {
@@ -1286,9 +1274,6 @@ export class GameServer {
conn.close(4010, 'guest_one_character')
conn.close(4010, 'guest_one_character')
return
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)
const existing = this.store.players.findByTokenSlot(token, slot)
// One live session per CHARACTER; other characters of the same account
// One live session per CHARACTER; other characters of the same account
@@ -3,8 +3,7 @@
@@ -3,8 +3,7 @@
* (headless Havok, real SQLite file, protocol-level connections), across a
* (headless Havok, real SQLite file, protocol-level connections), across a
* restart:
* 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 guest presenting an account key as its token is refused;
* - a mod's props enter the world through the runtime seam, a denied
* - 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
* capability never takes effect, and a revoked mod's props are gone on
@@ -58,7 +57,6 @@ const dbPath = join(dir, 'world.db')
@@ -58,7 +57,6 @@ const dbPath = join(dir, 'world.db')
const config = loadConfig({
const config = loadConfig({
DB_PATH: dbPath,
DB_PATH: dbPath,
MAP_PATH: join(dir, 'map.json'),
MAP_PATH: join(dir, 'map.json'),
- GUEST_IP_BINDING: 'off',
OV_NETWORK_AUTH_URL: 'http://network.test/api/auth/me',
OV_NETWORK_AUTH_URL: 'http://network.test/api/auth/me',
})
})
let havok: unknown
let havok: unknown
@@ -130,7 +128,6 @@ function connect(game: GameServer) {
@@ -130,7 +128,6 @@ function connect(game: GameServer) {
const inbox: ServerMessage[] = []
const inbox: ServerMessage[] = []
let closed: { code: number; reason: string } | null = null
let closed: { code: number; reason: string } | null = null
const conn: GameConnection = {
const conn: GameConnection = {
- ip: '127.0.0.1',
send: (text) => inbox.push(JSON.parse(text) as ServerMessage),
send: (text) => inbox.push(JSON.parse(text) as ServerMessage),
close: (code, reason) => {
close: (code, reason) => {
closed = { code, reason }
closed = { code, reason }
@@ -167,51 +164,28 @@ describe('Games on platform identity, events and mods (real server, restart)', (
@@ -167,51 +164,28 @@ describe('Games on platform identity, events and mods (real server, restart)', (
},
},
},
},
})
})
- let legacyPlayerId = ''
+ let signedInPlayerId = ''
let timeBefore = ''
let timeBefore = ''
it('runs identity, mods and events against the live world', async () => {
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)
const a = boot(platform)
// A guest cannot claim an account key as its token.
// A guest cannot claim an account key as its token.
const intruder = connect(a.game)
const intruder = connect(a.game)
- const refused = await intruder.hello('ovn:57')
+ const refused = await intruder.hello(SUBJECT)
expect(refused.welcome).toBeUndefined()
expect(refused.welcome).toBeUndefined()
expect(refused.closed).toEqual({ code: 4007, reason: 'invalid_guest_token' })
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 ana = connect(a.game)
const joined = await ana.hello('abcdefgh12', 'tok-ana')
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,
token: SUBJECT,
subjectId: SUBJECT,
subjectId: SUBJECT,
})
})
- expect(a.store.identity.subjectForLegacy('ovn:57')).toBe(SUBJECT)
// Mod A: props granted, announcements requested but denied.
// Mod A: props granted, announcements requested but denied.
a.registry.install(
a.registry.install(
@@ -292,9 +266,8 @@ describe('Games on platform identity, events and mods (real server, restart)', (
@@ -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.
// The account comes back to the same character, by subject.
const ana = connect(b.game)
const ana = connect(b.game)
const again = await ana.hello('zyxwvut98', 'tok-ana')
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.onDisconnect(ana.conn)
b.game.shutdown()
b.game.shutdown()
await b.outbox.stop()
await b.outbox.stop()
@@ -35,7 +35,7 @@ import { createNetworkModGrants } from './mods/networkGrants.js'
@@ -35,7 +35,7 @@ import { createNetworkModGrants } from './mods/networkGrants.js'
import { createGamesActorLimits } from './net/actorLimits.js'
import { createGamesActorLimits } from './net/actorLimits.js'
import { handleModsRequest } from './mods/routes.js'
import { handleModsRequest } from './mods/routes.js'
import { ModRuntime } from './mods/runtime.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 { ProgressSummaryWriter } from './platform/progressSummary.js'
import {
import {
EVENT_SOURCE,
EVENT_SOURCE,
@@ -73,8 +73,8 @@ async function main(): Promise<void> {
@@ -73,8 +73,8 @@ async function main(): Promise<void> {
// same file over /map.json).
// same file over /map.json).
if (existsSync(config.mapPath)) {
if (existsSync(config.mapPath)) {
try {
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')))
const parsed = parseMapFile(JSON.parse(readFileSync(config.mapPath, 'utf8')))
if (parsed.ok) {
if (parsed.ok) {
// The map's statics live in the override, NOT merged into
// The map's statics live in the override, NOT merged into
@@ -85,7 +85,6 @@ async function main(): Promise<void> {
@@ -85,7 +85,6 @@ async function main(): Promise<void> {
log.info('edited map loaded', {
log.info('edited map loaded', {
statics: parsed.map.statics.length,
statics: parsed.map.statics.length,
terrains: parsed.map.terrains.length,
terrains: parsed.map.terrains.length,
- migrated: parsed.migrated,
})
})
} else {
} else {
log.warn('edited map invalid — ignoring', { issues: parsed.issues.slice(0, 5).join('; ') })
log.warn('edited map invalid — ignoring', { issues: parsed.issues.slice(0, 5).join('; ') })
@@ -306,13 +305,12 @@ async function main(): Promise<void> {
@@ -306,13 +305,12 @@ async function main(): Promise<void> {
let account = token
let account = token
if (auth) {
if (auth) {
const user = await resolveNetworkUser(config.networkAuthUrl, 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
// 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.
// 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)) {
} else if (!isGuestToken(token)) {
// Never list an account key's characters for a guest query.
// Never list an account key's characters for a guest query.
return []
return []
@@ -53,7 +53,7 @@ export const TRUST_TIERS: readonly ModTrustTier[] = ['unreviewed', 'reviewed', '
@@ -53,7 +53,7 @@ export const TRUST_TIERS: readonly ModTrustTier[] = ['unreviewed', 'reviewed', '
/** Who performed a registry change: an audit string and an event actor. */
/** Who performed a registry change: an audit string and an event actor. */
export interface ModActor {
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
audit: string
subject: SubjectRef
subject: SubjectRef
}
}
@@ -128,15 +128,6 @@ describe('real routes on the apex', () => {
@@ -128,15 +128,6 @@ describe('real routes on the apex', () => {
expect(r.body).toContain(marker)
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 () => {
it('serves hashed build assets as immutable, and public files', async () => {
const asset = await send(ASSET)
const asset = await send(ASSET)
expect(asset.status).toBe(200)
expect(asset.status).toBe(200)
@@ -258,14 +249,11 @@ describe('play.openvibe.games', () => {
@@ -258,14 +249,11 @@ describe('play.openvibe.games', () => {
expect(r.body).toContain('game page')
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 () => {
it('serves the editor, assets, map and health like the apex', async () => {
expect((await send('/editor', { host: PLAY })).body).toContain('editor page')
expect((await send('/editor', { host: PLAY })).body).toContain('editor page')
@@ -167,7 +167,7 @@ export function createHttpServer(
@@ -167,7 +167,7 @@ export function createHttpServer(
if (platform?.handle?.(req, res)) return
if (platform?.handle?.(req, res)) return
if (url === '/map.json' && mapPath) {
if (url === '/map.json' && mapPath) {
// The canonical v2 document, with its revision as an ETag. This IS the
// 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) => {
void loadMap(mapPath).then((rec) => {
res.writeHead(200, {
res.writeHead(200, {
'content-type': 'application/json',
'content-type': 'application/json',
@@ -246,18 +246,11 @@ export function createHttpServer(
@@ -246,18 +246,11 @@ export function createHttpServer(
})
})
return
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
// Big source files (4k photo textures) live on disk next to the map
// artifact instead of being base64-embedded into map.json.
// 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
const token = (req.headers['x-editor-key'] as string | undefined) ?? undefined
void editorAuthorized(editorAuth, token).then(async (authed) => {
void editorAuthorized(editorAuth, token).then(async (authed) => {
if (!authed) {
if (!authed) {
@@ -673,14 +666,6 @@ export function createHttpServer(
@@ -673,14 +666,6 @@ export function createHttpServer(
res.end()
res.end()
return
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 =
const pageAlias =
url === '/'
url === '/'
? playHost
? playHost
@@ -1,16 +1,8 @@
@@ -1,16 +1,8 @@
/**
/**
* Content-addressed store for map assets (textures, paint masks, models).
* 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
* 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.
* 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.
* Files are immutable, so they can be served with a permanent cache header.
@@ -25,20 +25,14 @@ describe('loadMap', () => {
@@ -25,20 +25,14 @@ describe('loadMap', () => {
expect(r.map.terrains).toEqual([])
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(
writeFileSync(
mapPath,
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)
const r = await loadMap(mapPath)
expect(r.map.v).toBe(2)
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 () => {
it('falls back to an empty map rather than throwing on garbage', async () => {
@@ -129,21 +123,10 @@ describe('save pipeline', () => {
@@ -129,21 +123,10 @@ describe('save pipeline', () => {
expect((await saveMap(mapPath, huge, await loadMap(mapPath), undefined)).status).toBe(413)
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)
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 () => {
it('creates the artifact on a first save even when the map is unchanged', async () => {
@@ -7,7 +7,7 @@
@@ -7,7 +7,7 @@
* live world, and two admins saving at once silently clobbered each other —
* live world, and two admins saving at once silently clobbered each other —
* "revisions" were a string of array lengths compared on the client.
* "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.
* revision check → canonicalise → atomic write → apply live → broadcast.
*/
*/
import { createHash } from 'node:crypto'
import { createHash } from 'node:crypto'
@@ -45,9 +45,9 @@ function record(map: MapFileV2): MapRecord {
@@ -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> {
export async function loadMap(mapPath: string): Promise<MapRecord> {
if (!existsSync(mapPath)) return record(emptyMapV2())
if (!existsSync(mapPath)) return record(emptyMapV2())
@@ -20,8 +20,8 @@ export function attachWebSocket(http: Server, game: GameServer, log: Logger): We
@@ -20,8 +20,8 @@ export function attachWebSocket(http: Server, game: GameServer, log: Logger): We
void http
void http
wss.on('connection', (ws: WebSocket, req) => {
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 fwd = req.headers['x-forwarded-for']
const remote =
const remote =
(typeof req.headers['cf-connecting-ip'] === 'string'
(typeof req.headers['cf-connecting-ip'] === 'string'
@@ -34,7 +34,6 @@ export function attachWebSocket(http: Server, game: GameServer, log: Logger): We
@@ -34,7 +34,6 @@ export function attachWebSocket(http: Server, game: GameServer, log: Logger): We
let windowStart = Date.now()
let windowStart = Date.now()
const conn: GameConnection = {
const conn: GameConnection = {
- ip: remote,
send: (text) => {
send: (text) => {
if (ws.readyState === ws.OPEN) ws.send(text)
if (ws.readyState === ws.OPEN) ws.send(text)
},
},
@@ -12,22 +12,50 @@ import { createAccountData, type AccountEvent } from './accountData.js'
@@ -12,22 +12,50 @@ import { createAccountData, type AccountEvent } from './accountData.js'
* twice; foreign or malformed events are ignored.
* twice; foreign or malformed events are ignored.
*/
*/
const req = createRequire(import.meta.url)
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 log = createConsoleLogger({ app: 'test' }, 'error')
const DANA = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3'
const DANA = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3'
const OLD = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2B4'
const OLD = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2B4'
const OTTO = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2C5'
const OTTO = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2C5'
const char = (id: string, token: string) => ({
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)', () => {
describe('account export and deletion (ADR-033)', () => {
it('exports, then closes sessions, erases and confirms once', async () => {
it('exports, then closes sessions, erases and confirms once', async () => {
const store = openSqliteStore(':memory:')
const store = openSqliteStore(':memory:')
store.players.upsertMany([char('d0', DANA), char('d1', OLD), char('o0', OTTO)])
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
let failNext = false
const closed: string[][] = []
const closed: string[][] = []
const data = createAccountData({
const data = createAccountData({
@@ -46,19 +74,32 @@ describe('account export and deletion (ADR-033)', () => {
@@ -46,19 +74,32 @@ describe('account export and deletion (ADR-033)', () => {
},
},
log,
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({ ...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')
expect(await data.apply(exp)).toBe('exported')
const part = sent[0]!.body
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>[]
const chars = part.files![0]!.content as Record<string, unknown>[]
expect(chars.map((c) => c.id)).toEqual(['d0'])
expect(chars.map((c) => c.id)).toEqual(['d0'])
expect(chars[0]).not.toHaveProperty('token')
expect(chars[0]).not.toHaveProperty('token')
expect(await data.apply(exp)).toBe('unchanged')
expect(await data.apply(exp)).toBe('unchanged')
expect(sent.length).toBe(1)
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
failNext = true
await expect(data.apply(del)).rejects.toThrow(/confirmation refused: 503/)
await expect(data.apply(del)).rejects.toThrow(/confirmation refused: 503/)
expect(closed).toEqual([[DANA, OLD]])
expect(closed).toEqual([[DANA, OLD]])
@@ -67,9 +108,11 @@ describe('account export and deletion (ADR-033)', () => {
@@ -67,9 +108,11 @@ describe('account export and deletion (ADR-033)', () => {
expect(await data.apply(del)).toBe('confirmed')
expect(await data.apply(del)).toBe('confirmed')
expect(closed.length).toBe(1)
expect(closed.length).toBe(1)
const conf = sent[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(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(conf.body.retained).toEqual({ structures_unowned: 0 })
expect(await data.apply(del)).toBe('unchanged')
expect(await data.apply(del)).toBe('unchanged')
expect(sent.length).toBe(2)
expect(sent.length).toBe(2)
@@ -5,18 +5,20 @@
@@ -5,18 +5,20 @@
* failure is redelivered without erasing twice.
* failure is redelivered without erasing twice.
*
*
* network.account.export_requested Games' part (POST /internal/account-exports/:id/parts, service token):
* 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),
* 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 Database from 'better-sqlite3'
import type { Logger } from '@openvibe/shared'
import type { Logger } from '@openvibe/shared'
import type { IdentityRepository } from '@openvibe/persistence'
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 SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/
const EXPORT_RE = /^exp_[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}$/
const DELETION_RE = /^del_[0-9A-HJKMNP-TV-Z]{26}$/
@@ -52,11 +54,14 @@ export function createAccountData(opts: {
@@ -52,11 +54,14 @@ export function createAccountData(opts: {
sent_at TEXT,
sent_at TEXT,
applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
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 = ?')
const markSent = opts.db.prepare('UPDATE account_data_events SET sent_at = ? WHERE id = ?')
async function apply(event: AccountEvent): Promise<string> {
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'
if (event.source !== 'network') return 'ignored:source'
const p = event.payload ?? {}
const p = event.payload ?? {}
const subject = String(p.subject ?? '')
const subject = String(p.subject ?? '')
@@ -69,14 +74,25 @@ export function createAccountData(opts: {
@@ -69,14 +74,25 @@ export function createAccountData(opts: {
const files = [
const files = [
{ name: 'characters.json', content: data.characters },
{ name: 'characters.json', content: data.characters },
...(data.structures.length ? [{ name: 'structures.json', content: data.structures }] : []),
...(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 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}`)
if (!outcome) throw new Error(`export part refused: ${res.status}`)
opts.db
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
return outcome
}
}
const id = String(p.deletion_id ?? '')
const id = String(p.deletion_id ?? '')
@@ -84,7 +100,9 @@ export function createAccountData(opts: {
@@ -84,7 +100,9 @@ export function createAccountData(opts: {
let rec = get.get(id)
let rec = get.get(id)
let result = 'confirmed'
let result = 'confirmed'
if (!rec) {
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]
const subjects = [subject, ...aliases]
await opts.closeSessions(subjects)
await opts.closeSessions(subjects)
const erased = opts.identity.eraseSubjects(subjects)
const erased = opts.identity.eraseSubjects(subjects)
@@ -97,9 +115,16 @@ export function createAccountData(opts: {
@@ -97,9 +115,16 @@ export function createAccountData(opts: {
}
}
if (rec.sent_at) return 'unchanged'
if (rec.sent_at) return 'unchanged'
const o = JSON.parse(rec.outcome ?? '{}') as { erased?: Record<string, number> }
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 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}`)
if (!res.ok && res.status !== 404) throw new Error(`confirmation refused: ${res.status}`)
markSent.run(new Date().toISOString(), id)
markSent.run(new Date().toISOString(), id)
return result
return result
@@ -1,31 +1,8 @@
@@ -1,31 +1,8 @@
-import { openSqliteStore } from '@openvibe/persistence/sqlite'
-import type { PlayerDto } from '@openvibe/persistence'
import { describe, expect, it } from 'vitest'
import { describe, expect, it } from 'vitest'
-import { accountForNetworkUser, isGuestToken, legacyNetworkKey } from './accounts.js'
+import { isGuestToken } from './accounts.js'
const SUBJECT = 'usr_01JABCDEFGHJKMNPQRSTVWXYZ0'
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)', () => {
describe('guest tokens (hostile input)', () => {
it('accepts what the client mints', () => {
it('accepts what the client mints', () => {
expect(isGuestToken('a1B2c3D4')).toBe(true)
expect(isGuestToken('a1B2c3D4')).toBe(true)
@@ -39,14 +16,11 @@ describe('guest tokens (hostile input)', () => {
@@ -39,14 +16,11 @@ describe('guest tokens (hostile input)', () => {
for (const bad of [
for (const bad of [
SUBJECT,
SUBJECT,
'gst_01JABCDEFGHJKMNPQRSTVWXYZ0',
'gst_01JABCDEFGHJKMNPQRSTVWXYZ0',
- 'ovn:57',
- 'ovn:1',
'short',
'short',
'x'.repeat(65),
'x'.repeat(65),
'has space1',
'has space1',
'usr_whatever123',
'usr_whatever123',
'GST_01JABCDEFGHJKMNPQRSTVWXYZ0',
'GST_01JABCDEFGHJKMNPQRSTVWXYZ0',
- 'ovn:57:x',
'',
'',
null,
null,
42,
42,
@@ -55,53 +29,3 @@ describe('guest tokens (hostile input)', () => {
@@ -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()
- })
-})
@@ -3,22 +3,14 @@
@@ -3,22 +3,14 @@
*
*
* A signed-in account is keyed by its canonical openvibe.network subject
* A signed-in account is keyed by its canonical openvibe.network subject
* (`usr_…`, or `gst_…` for a Network guest) taken from the token's
* (`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
* Local guests keep their browser token (ADR-0004). A guest token may use
* letters, digits, `_` and `-` only (the client mints alphanumerics), and
* 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). */
/** The client mints `[A-Za-z0-9]{32}` (apps/client/src/net/connection.ts getIdentity). */
const GUEST_TOKEN = /^[A-Za-z0-9_-]{8,64}$/
const GUEST_TOKEN = /^[A-Za-z0-9_-]{8,64}$/
@@ -28,51 +20,3 @@ const SUBJECT_PREFIX = /^(usr|gst|app|mod|svc)_/i
@@ -28,51 +20,3 @@ const SUBJECT_PREFIX = /^(usr|gst|app|mod|svc)_/i
export function isGuestToken(token: unknown): token is string {
export function isGuestToken(token: unknown): token is string {
return typeof token === 'string' && GUEST_TOKEN.test(token) && !SUBJECT_PREFIX.test(token)
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 }
- }
-}
deleted file mode 100644
deleted file mode 100644
@@ -1,88 +0,0 @@
@@ -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()
- })
-})
deleted file mode 100644
deleted file mode 100644
@@ -1,69 +0,0 @@
@@ -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
-}
deleted file mode 100644
deleted file mode 100644
@@ -1,578 +0,0 @@
@@ -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()
- })
-})
deleted file mode 100644
deleted file mode 100644
@@ -1,746 +0,0 @@
@@ -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')
-}
@@ -12,7 +12,6 @@ import { createRequire } from 'node:module'
@@ -12,7 +12,6 @@ import { createRequire } from 'node:module'
import { verifyUserToken, type UserTokenClaims } from 'openvibe-sdk/auth'
import { verifyUserToken, type UserTokenClaims } from 'openvibe-sdk/auth'
import type { ModActor } from '../mods/registry.js'
import type { ModActor } from '../mods/registry.js'
import { canEditMap, resolveNetworkUser } from '../net/networkAuth.js'
import { canEditMap, resolveNetworkUser } from '../net/networkAuth.js'
-import { legacyNetworkKey } from './accounts.js'
export const CAP_MOD_MANAGE = 'games.mod.manage'
export const CAP_MOD_MANAGE = 'games.mod.manage'
export const GAMES_AUDIENCE = 'openvibe.games'
export const GAMES_AUDIENCE = 'openvibe.games'
@@ -95,7 +94,12 @@ export function createStaffAuthorizer(
@@ -95,7 +94,12 @@ export function createStaffAuthorizer(
if (user.subjectId?.startsWith('usr_')) {
if (user.subjectId?.startsWith('usr_')) {
return { audit: user.subjectId, subject: { type: 'user', id: user.subjectId } }
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)) {
if (opts.editorKey && sameSecret(token, opts.editorKey)) {
return { audit: 'editor-key', subject: { type: 'service', id: 'games' } }
return { audit: 'editor-key', subject: { type: 'service', id: 'games' } }
@@ -74,7 +74,6 @@ describe('SIGTERM', () => {
@@ -74,7 +74,6 @@ describe('SIGTERM', () => {
DB_PATH: join(dir, 'world.db'),
DB_PATH: join(dir, 'world.db'),
MAP_PATH: join(dir, 'map.json'),
MAP_PATH: join(dir, 'map.json'),
STATIC_DIR: '',
STATIC_DIR: '',
- GUEST_IP_BINDING: 'off',
// The platform side on, pointed at nothing: the outbox exists and has to be stopped.
// The platform side on, pointed at nothing: the outbox exists and has to be stopped.
OV_OAUTH_CLIENT_SECRET: 'test-secret-not-real',
OV_OAUTH_CLIENT_SECRET: 'test-secret-not-real',
OV_NETWORK_INTERNAL_URL: 'http://127.0.0.1:9',
OV_NETWORK_INTERNAL_URL: 'http://127.0.0.1:9',
@@ -9,9 +9,9 @@
@@ -9,9 +9,9 @@
# DRY_RUN=1 /opt/openvibe.games/deploy/scripts/deploy.sh ovhost plan games
# 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
# 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
# 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
# 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).
# by hand first: ovhost would find nothing new (pass --restart if you did).
deleted file mode 100644
deleted file mode 100644
@@ -1,418 +0,0 @@
@@ -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).
@@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
# ADR-0006: Canonical Network subjects, and the platform boundary
# 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
## Decision
new file mode 100644
new file mode 100644
@@ -0,0 +1,83 @@
@@ -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.
deleted file mode 100644
deleted file mode 100644
@@ -1,395 +0,0 @@
@@ -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>
-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
-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
-```
@@ -1,14 +1,9 @@
@@ -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:
* Editor-placed light. Covers every Babylon punctual/ambient light type:
* point, spot (angle+exponent), directional (sun-like), hemispheric
* point, spot (angle+exponent), directional (sun-like), hemispheric
@@ -21,18 +16,10 @@ import type { MapNodeSpawn, MapOverride, MapPropSpawn } from './terrain.js'
@@ -21,18 +16,10 @@ import type { MapNodeSpawn, MapOverride, MapPropSpawn } from './terrain.js'
*/
*/
export type { MapLight } from './schema/world.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 {
export interface MapTextureEntry {
name: string
name: string
- /** Legacy embedded payload (small uploads from older maps). */
+ /** Embedded payload (small uploads). */
dataUrl?: string
dataUrl?: string
/** Server-hosted asset path (/map-assets/...), preferred. */
/** Server-hosted asset path (/map-assets/...), preferred. */
url?: string
url?: string
@@ -40,48 +27,6 @@ export interface MapTextureEntry {
@@ -40,48 +27,6 @@ export interface MapTextureEntry {
scale?: number
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 {
export function decodeHeights(b64: string): Float32Array {
const bin = typeof atob === 'function' ? atob(b64) : Buffer.from(b64, 'base64').toString('binary')
const bin = typeof atob === 'function' ? atob(b64) : Buffer.from(b64, 'base64').toString('binary')
const bytes = new Uint8Array(bin.length)
const bytes = new Uint8Array(bin.length)
@@ -95,12 +40,3 @@ export function encodeHeights(heights: Float32Array): string {
@@ -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)
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')
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()')
-}
@@ -1,13 +1,10 @@
@@ -1,13 +1,10 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it } from 'vitest'
-import { encodeHeights, type MapFile } from './mapFile.js'
import {
import {
- MAIN_TERRAIN_MIGRATED_ID,
SPAWN_OBJECT_ID,
SPAWN_OBJECT_ID,
compileMapFileV2,
compileMapFileV2,
blankHeights,
blankHeights,
canonicalizeMapFile,
canonicalizeMapFile,
emptyMapV2,
emptyMapV2,
- migrateV1ToV2,
parseMapFile,
parseMapFile,
validateMapFile,
validateMapFile,
type MapFileV2,
type MapFileV2,
@@ -15,20 +12,6 @@ import {
@@ -15,20 +12,6 @@ import {
} from './mapFileV2.js'
} from './mapFileV2.js'
const SUB = 8
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', () => {
describe('empty map', () => {
it('a brand-new map has no terrain and no geometry at all', () => {
it('a brand-new map has no terrain and no geometry at all', () => {
@@ -42,117 +25,16 @@ describe('empty map', () => {
@@ -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', () => {
describe('parseMapFile', () => {
it('accepts a v2 map unchanged', () => {
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)
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({ v: 99 }).ok).toBe(false)
expect(parseMapFile(null).ok).toBe(false)
expect(parseMapFile(null).ok).toBe(false)
expect(parseMapFile('nope').ok).toBe(false)
expect(parseMapFile('nope').ok).toBe(false)
@@ -316,32 +198,6 @@ describe('paint layer tint survives the canonical wire', () => {
@@ -316,32 +198,6 @@ describe('paint layer tint survives the canonical wire', () => {
expect(r.ok, `expected "${bad}" to be rejected`).toBe(false)
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', () => {
describe('canonicalisation', () => {
@@ -21,20 +21,9 @@ import {
@@ -21,20 +21,9 @@ import {
type FaceStyle,
type FaceStyle,
type ZoneDef,
type ZoneDef,
} from './schema/world.js'
} 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 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 vec3 = z.tuple([z.number(), z.number(), z.number()])
const finiteVec3 = vec3.refine((v) => v.every(Number.isFinite), 'must be finite')
const finiteVec3 = vec3.refine((v) => v.every(Number.isFinite), 'must be finite')
@@ -275,95 +264,18 @@ export function normalizeMapIds(raw: unknown): unknown {
@@ -275,95 +264,18 @@ export function normalizeMapIds(raw: unknown): unknown {
return out
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 {
export function parseMapFile(raw: unknown): ParsedMap {
if (raw === null || typeof raw !== 'object') return { ok: false, issues: ['not an object'] }
if (raw === null || typeof raw !== 'object') return { ok: false, issues: ['not an object'] }
const version = (raw as { v?: unknown }).v
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}`
const describeIssue = (i: z.ZodIssue): string => `${i.path.join('.') || '(root)'}: ${i.message}`
@@ -6,7 +6,6 @@ import {
@@ -6,7 +6,6 @@ import {
emptySurface,
emptySurface,
freeChannel,
freeChannel,
layerForTexture,
layerForTexture,
- migrateLegacyMix,
removeLayer,
removeLayer,
validateSurface,
validateSurface,
type SurfacePaint,
type SurfacePaint,
@@ -134,24 +133,6 @@ describe('base texture survives painting', () => {
@@ -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', () => {
describe('validation', () => {
const exists = (ref: string): boolean => ref.startsWith('ok')
const exists = (ref: string): boolean => ref.startsWith('ok')
@@ -135,26 +135,6 @@ export function removeLayer(paint: SurfacePaint, layerId: string): PaintChannel
@@ -135,26 +135,6 @@ export function removeLayer(paint: SurfacePaint, layerId: string): PaintChannel
return gone?.channel ?? null
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. */
/** Validation for the Issues panel and the save pipeline. */
export function validateSurface(
export function validateSurface(
s: SurfaceMaterialData,
s: SurfaceMaterialData,
@@ -140,8 +140,6 @@ export interface TerrainPatchData {
@@ -140,8 +140,6 @@ export interface TerrainPatchData {
tex?: string
tex?: string
/** Tint color (hex). */
/** Tint color (hex). */
color?: string
color?: string
- /** Legacy painted splat (R grass / G rock / B mud); migrated to `surface`. */
- mix?: string
/** Base style + paint layers — the v2 surface model. */
/** Base style + paint layers — the v2 surface model. */
surface?: SurfaceMaterialData
surface?: SurfaceMaterialData
/** Whole-surface UV transform from the face-edit tool. */
/** Whole-surface UV transform from the face-edit tool. */
@@ -38,11 +38,6 @@ export interface PlayerRepository {
@@ -38,11 +38,6 @@ export interface PlayerRepository {
resetAllPositions(pos: [number, number, number], yaw: number): void
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 {
export interface ConstraintRepository {
loadAll(): ConstraintDto[]
loadAll(): ConstraintDto[]
upsertMany(constraints: readonly ConstraintDto[]): void
upsertMany(constraints: readonly ConstraintDto[]): void
@@ -51,25 +46,11 @@ export interface ConstraintRepository {
@@ -51,25 +46,11 @@ export interface ConstraintRepository {
/**
/**
* Canonical identity (ADR-0006): signed-in accounts are keyed by their
* 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 {
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
* 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
* 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 {
@@ -93,23 +74,24 @@ export interface IdentityRepository {
mergeId: string,
mergeId: string,
now: number,
now: number,
): { moved: number; kept: number; already: boolean }
): { 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
* 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): {
exportSubject(subject: string): {
characters: Record<string, unknown>[]
characters: Record<string, unknown>[]
structures: 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
* 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. */
/** Installed mods, their approved capabilities, placements and audit log. */
@@ -161,7 +143,6 @@ export interface MetaRepository {
@@ -161,7 +143,6 @@ export interface MetaRepository {
export interface PersistenceStore {
export interface PersistenceStore {
readonly worldEntities: WorldEntityRepository
readonly worldEntities: WorldEntityRepository
readonly players: PlayerRepository
readonly players: PlayerRepository
- guests: GuestRepository
readonly constraints: ConstraintRepository
readonly constraints: ConstraintRepository
readonly meta: MetaRepository
readonly meta: MetaRepository
readonly identity: IdentityRepository
readonly identity: IdentityRepository
@@ -39,11 +39,31 @@ describe('guest conversion (WS-B task 8)', () => {
@@ -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', () => {
it('moves the guest character into the first free slot, once, keyed by a hash of the token', () => {
const store = openSqliteStore(':memory:')
const store = openSqliteStore(':memory:')
store.players.upsertMany([player('g1', 'guest00042abc', 0), player('a0', SUBJECT, 0)])
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.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) => k.includes('guest00042abc'))).toBe(false)
expect(keys.some((k) => /^guest:[0-9a-f]{32}$/.test(k))).toBe(true)
expect(keys.some((k) => /^guest:[0-9a-f]{32}$/.test(k))).toBe(true)
store.close()
store.close()
@@ -51,10 +71,23 @@ describe('guest conversion (WS-B task 8)', () => {
@@ -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', () => {
it('keeps the guest character when every slot is taken, and does nothing without one', () => {
const store = openSqliteStore(':memory:')
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.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()
store.close()
})
})
})
})
@@ -64,11 +97,29 @@ describe('account merge (ADR-029)', () => {
@@ -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', () => {
it('moves the folded-in characters into free slots, keeps what does not fit, once per merge', () => {
const store = openSqliteStore(':memory:')
const store = openSqliteStore(':memory:')
// The survivor has slot 0; the folded-in account has slots 0, 1 and 2.
// 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.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)
expect(store.players.listByToken(FROM).length).toBe(1)
store.close()
store.close()
})
})
@@ -76,87 +127,50 @@ describe('account merge (ADR-029)', () => {
@@ -76,87 +127,50 @@ describe('account merge (ADR-029)', () => {
describe('account export and deletion (ADR-033)', () => {
describe('account export and deletion (ADR-033)', () => {
const ALIAS = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2F8'
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:')
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('e1', 'd0')
entity.run('e2', 'o0')
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)
const out = store.identity.exportSubject(SUBJECT)
expect(out.characters.map((c) => c.id)).toEqual(['d0'])
expect(out.characters.map((c) => c.id)).toEqual(['d0'])
expect(out.characters[0]).not.toHaveProperty('token')
expect(out.characters[0]).not.toHaveProperty('token')
expect(out.characters[0]?.skills).toEqual({ woodcutting: 10 })
expect(out.characters[0]?.skills).toEqual({ woodcutting: 10 })
expect(out.structures.map((e) => e.id)).toEqual(['e1'])
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(SUBJECT)).toEqual([])
expect(store.players.listByToken(ALIAS)).toEqual([])
expect(store.players.listByToken(ALIAS)).toEqual([])
expect(store.players.listByToken(OTHER).map((p) => p.id)).toEqual(['o0'])
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()
store.close()
})
})
+})
+describe('player subject_id', () => {
it('keeps subject_id through ordinary saves that do not carry it', () => {
it('keeps subject_id through ordinary saves that do not carry it', () => {
const store = openSqliteStore(':memory:')
const store = openSqliteStore(':memory:')
store.players.upsert(player('p1', SUBJECT, 0, { subjectId: SUBJECT }))
store.players.upsert(player('p1', SUBJECT, 0, { subjectId: SUBJECT }))
@@ -12,63 +12,38 @@ import type {
@@ -12,63 +12,38 @@ import type {
import type { IdentityRepository, MediaMirrorRepository, ModRepository } from '../repositories.js'
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 {
export function createIdentityRepository(db: Database.Database): IdentityRepository {
const takenSlots = db.prepare<[string], { char_slot: number }>(
const takenSlots = db.prepare<[string], { char_slot: number }>(
'SELECT char_slot FROM players WHERE token = ?',
'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',
'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 (?, ?, ?, ?, ?, ?)
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
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) => {
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 key = `guest:${createHash('sha256').update(guestToken).digest('hex').slice(0, 32)}`
const none = { moved: 0, slot: null, full: false }
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
if (rows.length === 0) return none
const taken = new Set(takenSlots.all(subjectId).map((r) => r.char_slot))
const taken = new Set(takenSlots.all(subjectId).map((r) => r.char_slot))
let moved = 0
let moved = 0
@@ -82,44 +57,44 @@ export function createIdentityRepository(db: Database.Database): IdentityReposit
@@ -82,44 +57,44 @@ export function createIdentityRepository(db: Database.Database): IdentityReposit
moved++
moved++
}
}
if (moved === 0) return { moved: 0, slot: null, full: true }
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 }
return { moved, slot, full: false }
})
})
// Account merge (ADR-029): the folded-in subject's characters join the survivor, once per merge.
// 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).
// 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 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 parsed = (row: Record<string, unknown>): Record<string, unknown> => {
const out: Record<string, unknown> = {}
const out: Record<string, unknown> = {}
for (const [k, v] of Object.entries(row)) {
for (const [k, v] of Object.entries(row)) {
@@ -142,46 +117,42 @@ export function createIdentityRepository(db: Database.Database): IdentityReposit
@@ -142,46 +117,42 @@ export function createIdentityRepository(db: Database.Database): IdentityReposit
.all(subject, subject) as Record<string, unknown>[]
.all(subject, subject) as Record<string, unknown>[]
const ids = characters.map((c) => String(c.id))
const ids = characters.map((c) => String(c.id))
const structures = ids.length
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 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
if (!subjects.length) return out
const q = inList(subjects.length)
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) {
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 out
})
})
return {
return {
exportSubject,
exportSubject,
eraseSubjects: (subjects) => eraseSubjects(subjects),
eraseSubjects: (subjects) => eraseSubjects(subjects),
- adoptLegacyAccount: (legacyKey, subjectId, source, now) =>
- adopt(legacyKey, subjectId, source, now),
mergeSubject: (from, into, mergeId, now) => mergeAccount(from, into, mergeId, now),
mergeSubject: (from, into, mergeId, now) => mergeAccount(from, into, mergeId, now),
adoptGuestCharacter: (guestToken, subjectId, now) => adoptGuest(guestToken, subjectId, 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)
- },
}
}
}
}
@@ -137,8 +137,8 @@ describe('sqlite store', () => {
@@ -137,8 +137,8 @@ describe('sqlite store', () => {
expect(player?.skills).toEqual({})
expect(player?.skills).toEqual({})
expect(player?.friends).toEqual([])
expect(player?.friends).toEqual([])
expect(player?.appearance).toBeNull()
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([
store.constraints.upsertMany([
{ id: 'c1', type: 'weld', entityA: 'x', entityB: 'y', params: null, updatedAt: 1 },
{ id: 'c1', type: 'weld', entityA: 'x', entityB: 'y', params: null, updatedAt: 1 },
{
{
@@ -153,14 +153,8 @@ describe('sqlite store', () => {
@@ -153,14 +153,8 @@ describe('sqlite store', () => {
const loaded = store.constraints.loadAll()
const loaded = store.constraints.loadAll()
expect(loaded).toHaveLength(2)
expect(loaded).toHaveLength(2)
expect(loaded.find((c) => c.id === 'c2')?.params).toEqual({ length: 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.upsert({
...store.players.findByToken('tok_11111111')!,
...store.players.findByToken('tok_11111111')!,
armor: { defId: 'padded_jacket', count: 1, meta: { dur: 5 } },
armor: { defId: 'padded_jacket', count: 1, meta: { dur: 5 } },
@@ -5,7 +5,6 @@ import type {
@@ -5,7 +5,6 @@ import type {
ConstraintRepository,
ConstraintRepository,
MetaRepository,
MetaRepository,
PersistenceStore,
PersistenceStore,
- GuestRepository,
PlayerRepository,
PlayerRepository,
WorldEntityRepository,
WorldEntityRepository,
} from '../repositories.js'
} from '../repositories.js'
@@ -33,7 +32,7 @@ export interface SqlitePersistenceStore extends PersistenceStore {
@@ -33,7 +32,7 @@ export interface SqlitePersistenceStore extends PersistenceStore {
* a database is upgraded step by step inside a transaction per step.
* a database is upgraded step by step inside a transaction per step.
*/
*/
-const SCHEMA_VERSION = 13
+const SCHEMA_VERSION = 11
const BASE_SCHEMA = `
const BASE_SCHEMA = `
CREATE TABLE IF NOT EXISTS world_entities (
CREATE TABLE IF NOT EXISTS world_entities (
@@ -75,10 +74,6 @@ CREATE TABLE IF NOT EXISTS constraints (
@@ -75,10 +74,6 @@ CREATE TABLE IF NOT EXISTS constraints (
params TEXT,
params TEXT,
updated_at INTEGER NOT NULL
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 (
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
key TEXT PRIMARY KEY,
value TEXT NOT NULL
value TEXT NOT NULL
@@ -86,14 +81,15 @@ CREATE TABLE IF NOT EXISTS meta (
@@ -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 = `
const PLATFORM_SCHEMA = `
CREATE INDEX IF NOT EXISTS idx_players_subject ON players (subject_id);
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,
subject_id TEXT NOT NULL,
source TEXT NOT NULL,
source TEXT NOT NULL,
moved INTEGER NOT NULL DEFAULT 0,
moved INTEGER NOT NULL DEFAULT 0,
@@ -156,26 +152,6 @@ CREATE TABLE IF NOT EXISTS media_mirrors (
@@ -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);
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. */
/** Migration from version N applies index N-1. Each runs in a transaction. */
const MIGRATIONS: Record<number, (db: Database.Database) => void> = {
const MIGRATIONS: Record<number, (db: Database.Database) => void> = {
1: (db) => {
1: (db) => {
@@ -225,43 +201,31 @@ const MIGRATIONS: Record<number, (db: Database.Database) => void> = {
@@ -225,43 +201,31 @@ const MIGRATIONS: Record<number, (db: Database.Database) => void> = {
`)
`)
},
},
6: (db) => {
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,
// Sandbox constraint set: type-specific parameters (anchors, axes,
// lengths, limits, motor). Existing weld rows carry NULL params.
// lengths, limits, motor). Existing weld rows carry NULL params.
db.exec('ALTER TABLE constraints ADD COLUMN params TEXT;')
db.exec('ALTER TABLE constraints ADD COLUMN params TEXT;')
},
},
- 8: (db) => {
+ 7: (db) => {
// Worn armor (one slot; durability rides in the stack meta).
// Worn armor (one slot; durability rides in the stack meta).
db.exec('ALTER TABLE players ADD COLUMN armor TEXT;')
db.exec('ALTER TABLE players ADD COLUMN armor TEXT;')
},
},
- 9: (db) => {
+ 8: (db) => {
// Faction reputation scores.
// Faction reputation scores.
db.exec("ALTER TABLE players ADD COLUMN reputation TEXT NOT NULL DEFAULT '{}';")
db.exec("ALTER TABLE players ADD COLUMN reputation TEXT NOT NULL DEFAULT '{}';")
},
},
- 10: (db) => {
+ 9: (db) => {
// Blueprint unlocks + active contract.
// Blueprint unlocks + active contract.
db.exec(`
db.exec(`
ALTER TABLE players ADD COLUMN unlocks TEXT NOT NULL DEFAULT '[]';
ALTER TABLE players ADD COLUMN unlocks TEXT NOT NULL DEFAULT '[]';
ALTER TABLE players ADD COLUMN active_job TEXT;
ALTER TABLE players ADD COLUMN active_job TEXT;
`)
`)
},
},
- 11: (db) => {
+ 10: (db) => {
// Platform integration: canonical subject per account (ADR-0006), the
// 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('ALTER TABLE players ADD COLUMN subject_id TEXT;')
db.exec(PLATFORM_SCHEMA)
db.exec(PLATFORM_SCHEMA)
},
},
- 12: (db) => {
- // Archive of OpenVibe.Live's HoboQuest rows (docs/legacy-import.md).
- db.exec(LEGACY_SCHEMA)
- },
}
}
interface WorldEntityRow {
interface WorldEntityRow {
@@ -324,7 +288,6 @@ export function openSqliteStore(path: string): SqlitePersistenceStore {
@@ -324,7 +288,6 @@ export function openSqliteStore(path: string): SqlitePersistenceStore {
// Fresh database: create the full current schema.
// Fresh database: create the full current schema.
db.exec(BASE_SCHEMA)
db.exec(BASE_SCHEMA)
db.exec(PLATFORM_SCHEMA)
db.exec(PLATFORM_SCHEMA)
- db.exec(LEGACY_SCHEMA)
db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run(
db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run(
'schema_version',
'schema_version',
String(SCHEMA_VERSION),
String(SCHEMA_VERSION),
@@ -548,32 +511,10 @@ export function openSqliteStore(path: string): SqlitePersistenceStore {
@@ -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 {
return {
db,
db,
worldEntities,
worldEntities,
players,
players,
- guests,
constraints,
constraints,
meta,
meta,
identity: createIdentityRepository(db),
identity: createIdentityRepository(db),