@jgengine/node 0.15.0-next.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -28,6 +28,49 @@ At publish, rename this heading to the new version and mirror the entries into
28
28
 
29
29
  ### Migrate
30
30
 
31
+ ### Changed
32
+
33
+ ### Added
34
+
35
+ ### Fixed
36
+
37
+ ## 0.16.0
38
+
39
+ ### Migrate
40
+
41
+ - **Bump lockstep SDK packages to `^0.16.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.13.0`; `@jgengine/github` is `0.3.0`.
42
+ - **Placed objects now replicate.** `objects` joined the always-on baseline snapshot modules, so `ctx.hydrate` replaces the client's placed object set with the host's, the way `entities` already did. A game whose peers all run the same authored placement is unaffected; a game that places props on the client only, after hydrate, must move that placement to the host or keep it out of `ctx.scene.object`.
43
+
44
+ ### Changed
45
+
46
+ - **`GeneratedBuilding`'s `kit` prop is now the `BuildingKit`**; the per-part React escape hatch it used to name moved to `partRenderer`, so `kit` means the same thing on `GeneratedBuilding` and `InstancedBuildings`. Rename the prop if you passed a `BuildingKitRenderer`.
47
+
48
+ ### Added
49
+
50
+ - **`modelLoadFallbacks()` from `@jgengine/shell/render/modelLoad`** lists every model the shared loader diagnosed as broken and replaced with a magenta placeholder. A placeholder was only ever a console warning, so a shipped game could render one in its starting area and nobody found out until someone looked at a screenshot; a capture host or smoke test can now require the list to be empty. `clearModelLoadFallbacks()` resets it between runs.
51
+ - **`worldHealthBars.occlude` / `WorldHealthBars`+`WorldNameplates` `occlude` prop.** World bars and nameplates are a screen-space overlay drawn outside the WebGL depth buffer, so nothing in the scene can hide them and an enemy's health bar reads straight through the building it stands behind. Set `occlude: true` to drop a bar when world geometry blocks the line from the player to the entity. Default `false` preserves the always-visible behaviour every shipped game currently relies on.
52
+
53
+ - **`DustField` + `WeatherLayer` mode `"dust"` (`@jgengine/shell/weather`) — wind-borne particulate.** `WeatherLayer` could only do `clear`/`rain`/`snow`/`mixed`, so an arid, volcanic, ashen, or blown-out world had no way to put anything in the air. Dust drifts on horizontal wind with a slow vertical bob rather than falling; `groundBias` packs density toward the surface, motes fade near the top of the volume so recycled ones do not pop against the sky, and faster motes ride higher. Set `dustAlways` to keep it in the air alongside rain or snow instead of replacing them. Defaults `DEFAULT_DUST_COUNT`/`DEFAULT_DUST_DENSITY` sit beside the rain and snow pair.
54
+
55
+ - **Authored `city` volumes render their near-LOD facades with a `BuildingKit`.** `CityRules.buildingKit` names a kit; a game registers the bound art with `registerBuildingKit(kit)` from `@jgengine/shell/structures/buildingKitRegistry` and the scene document carries only the name, so a city stays portable and free of model paths. Unset or unknown names keep the palette blocks. This puts the whole `city` primitive — not just the `building()` feature — on the kit path.
56
+ - **`BuildingKit` from `@jgengine/core/world/buildingKit`** binds a generated building's facade part slots to real models, so procedural buildings render as an instanced asset kit instead of untextured blocks. `defineBuildingKit({ parts: { wall: ["Wall_A.glb", …] } })` maps a part kind — or a `${facade}.${kind}` slot — to ordered variants that `kit.variant` indexes, with `fit`/`offset`/`rotation`/`scale`/`tint`/`autoOrient` per variant. Kinds a kit leaves unbound keep rendering the palette block, so a partial kit is a valid kit, and `omit` drops a kind entirely. Pass one through `building({ kit })` or straight to `InstancedBuildings`; `buildingKitVariantCounts(kit)` feeds `generateBuilding` so its seeded picks span exactly the models the kit binds.
57
+ - **`generateBuilding`, `generateBuildingDistrict`, and `createBuildingConfig` are now documented public API** (`@jgengine/core/world/buildings`) and appear in `capabilities.md` under `building-generator`.
58
+ - **`capture.views` on `defineGame`** — named framings (`GameCaptureView`: `look`, `lookFrom`, `spawn`, `state`, `run`, `settleMs`) that `shoot <game> --view <name>` and `drive --view <name>` replay exactly, so a before/after pair is the same view by construction. `shoot <game> --list-views` prints what a game declares; an unknown name fails listing them, and any explicit capture flag still wins over the view's value.
59
+ - **`modelLoadIdleMs()` from `@jgengine/shell/render/modelLoad`** reports how long the shared GLB loader has been idle (`0` while a model is in flight). Capture hosts wait on it instead of guessing a settle delay, so an establishing shot no longer races streaming models.
60
+ - **Per-instance state on placed objects** — `SceneObject.state` (opaque `Record`), set through `ctx.scene.object.setState`/`patchState`/`place({ state })`. It survives move/rotate/setVisual, replicates and saves with the object, and round-trips as `RuntimeObjectRow.flags` via `toRuntimeObjectRow`/`fromRuntimeObjectRow` (`@jgengine/core/multiplayer`). A chest's locked flag or a machine's fill level no longer needs a game-owned map keyed by `instanceId`.
61
+ - **`slotInventory` is a working feature** — `ctx.scene.object.slots` gives objects whose catalog entry declares it real per-instance container contents: `layout`/`state`/`count`, `put`/`take`, and `transferIn`/`transferOut` between a player inventory and the object, all validated against the declared slot count, `accepts` kind filter, and stack limits, with neither side mutated on a rejection. Contents live on `SceneObject.slots`, so they replicate and save with the placement.
62
+ - **`loadServerSnapshot` and `persistServerSnapshot` are public** (`@jgengine/convex/server`), and `createGameServerFunctions` also returns `helpers` (`loadSnapshot`, `applyCommand`, `persistSnapshot`, `runCommand`) bound to the same runtimes and auth mode. A host mutation can now pair snapshot work with its own table writes in one transaction; the registered `runCommand` mutation delegates to `helpers.runCommand`, so there is one implementation.
63
+
64
+ ### Fixed
65
+
66
+ - **Open storefronts are no longer holes through the building.** `generateBuilding` emitted an open ground-floor store as a lone `storefront` panel with no wall behind it, while every upper-floor window was correctly backed by one. Storefront and window materials render at 0.56 opacity and a generated building is a hollow shell, so those bays let you see straight through the building to the world beyond — and open stores only land on the street-facing facade, so a row of shops along a road was the worst case. The bay now emits its wall first, exactly as an upper floor does.
67
+ - **`joinServer`, `flushSave`, `flushDirtyServers`, and the `auto` autosave can write player profiles again.** All four cleared the snapshot's dirty set before handing it to the persist plan, which gates profile writes on exactly that set — so the functions named "flush" flushed the server row and chunks only, and an `onNewPlayer` grant (starting money, starting items) never reached `jgPlayerProfiles`. `joinServer` now persists the snapshot it just marked; the flush paths mark every player dirty first, since hydrating from storage resets the dirty set. `planServerPersist` also clears `dirtyAt` once everything outstanding is written, so a successful flush stops autosave from re-firing forever, and a profile patch no longer clobbers a host-maintained `dirtyAt`.
68
+
69
+ ## 0.15.0
70
+
71
+ ### Migrate
72
+
73
+ - **Bump lockstep SDK packages to `^0.15.0`:** `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.12.0`; `@jgengine/github` is `0.2.0`.
31
74
  - **`jgengine create` scaffolds a textured world by default (#1550).** `--world` is now the default and `--no-world` is the opt-out (matching `--no-editor`); `--world` is still accepted. The default scaffold previously emitted untextured proxy geometry, which the AGENTS.md it ships declares a failing result. `create` now also pulls the starter asset packs into `public/models` after install, and never fails the scaffold if that pull cannot run (offline, blocked proxy, `--no-install`) — it prints `npx assets pull starter` in next steps instead, and the emitted AGENTS.md documents the same command. Skip the pull explicitly with `--no-assets`.
32
75
  - **Start game code at `@jgengine/shell/gameKit` (#1541).** Prefer that surface for `defineGame`, `GameHost`, stores, systems, authored-scene helpers, and HUD building blocks. `@jgengine/core/authoring` remains for pure core helpers the kit does not re-export but is no longer a competing start-here path.
33
76
  - **Every game must now declare its run-phase story — silence is a gate error (#1337).** The run-phase
@@ -73,10 +116,16 @@ At publish, rename this heading to the new version and mirror the entries into
73
116
 
74
117
  ### Added
75
118
 
119
+ - **`advanceStepCadence` (`@jgengine/core/audio/stepCadence`) — distance-driven footstep timing.** Pure state-in/state-out: feed it the ground distance a mover covered this frame and it reports how many steps landed and which foot led, so foley stays locked to distance instead of a wall-clock timer that desyncs the moment speed changes. Configurable stride, per-advance `strideScale` for crouch/sprint, and a shorter first step from rest so walking off is heard immediately.
120
+ - **`resolveMusicState` (`@jgengine/core/audio/musicState`) — combat-reactive soundtrack selection.** The engine could already crossfade themes but nothing decided *when*, so every game re-handrolled the threshold and got a track that flapped as one enemy walked in and out of range. Give it a tier ladder (intensity → theme) and it resolves the theme to play: escalation is immediate, de-escalation waits for the tier's `holdMs` and needs intensity to fall a `release` margin below the tier it is leaving. Pure and serialisable.
121
+ - **Synth voices take a `sustain` (`ToneVoice`/`NoiseVoice`).** Seconds held at full gain before the decay. Default 0 preserves every existing patch. Without it there was no way to author a *sustained* sound at all: every voice decayed to silence inside its duration, so a patch rendered once and looped as an ambient bed pulsed audibly once per loop. Set `sustain` to `duration` for a flat wind, rain, or machine-hum bed that loops seamlessly.
122
+
76
123
  - **`ModelMaterialOverride.rim` — a fresnel rim light on any entity/object model.** `{ color, strength, power }` adds a bright edge where the surface turns away from the viewer, patched into the material's own shader so it shadows, fogs, and tonemaps with the body rather than floating in front of it. This is the standard fix for an actor that shares a value band with its ground: no albedo choice separates a body from terrain lit by the same key, but an edge does. Omit for no rim (default).
77
124
 
78
125
  ### Fixed
79
126
 
127
+ - **Noise voices longer than the shared noise buffer no longer go silent partway through.** The 1-second buffer is sampled from a random offset, so any `NoiseVoice` whose duration exceeded the remainder ran off the end and finished in silence — invisible on short impact bursts, fatal for anything sustained. The source now wraps; the voice still ends on time.
128
+
80
129
  - **`terrain({ detail: { material: { tint } } })` separates a ground texture's grain from its hue.** A detail material's colour map used to replace the terrain's authored colour outright, so the biome palette, macro sweeps, and slope-rock blend under it were painted over and a whole region flattened to the texture's single hue — despite the API documenting that it "blends with, never replaces". `tint` defaults to 1 (the existing behaviour, so no shipped game changes); drop it toward 0 to keep the authored colour and take only the map's light and shade.
81
130
  - **`sky({ gradientExponent, cloudiness })` are now authorable — and `sky()` no longer silently drops them.** `gradientExponent` controls how fast the gradient reaches `zenithColor` off the horizon (default 0.65, unchanged): below 1 pulls the zenith hue down into the frame, which is what an eye-level camera in flat terrain actually needs — at the default the sky reads as one horizon-coloured wash however saturated the authored zenith is. `cloudiness` (default 0.35, unchanged) is the procedural cloud-band opacity.
82
131
  - **A model that throws while rendering is now reported instead of vanishing (#1588).** `ModelFallbackBoundary` swapped in its fallback (usually nothing) and stayed silent: `probes.fallbacks` stayed empty, no warning was logged, and an entity simply disappeared. It now reports a new `renderError` fallback cause on every failed render and warns once with the model url and the caught error.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/node",
3
- "version": "0.15.0-next.0",
3
+ "version": "0.16.0",
4
4
  "description": "Standalone authoritative game host for JGengine: in-memory server snapshots, tick loop, save-cadence flush, WebSocket server, memory/file persistence.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -31,8 +31,8 @@
31
31
  "test": "bun test src"
32
32
  },
33
33
  "dependencies": {
34
- "@jgengine/core": "^0.15.0-next.0",
35
- "@jgengine/ws": "^0.15.0-next.0",
34
+ "@jgengine/core": "^0.16.0",
35
+ "@jgengine/ws": "^0.16.0",
36
36
  "ws": "^8.18.0"
37
37
  },
38
38
  "devDependencies": {
@@ -6,7 +6,7 @@
6
6
 
7
7
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
8
8
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
9
- - `VERSION` (const): const VERSION: "0.15.0-next.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.16.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
10
10
 
11
11
  ## @jgengine/core/authoring
12
12
 
@@ -228,7 +228,7 @@
228
228
 
229
229
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
230
230
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
231
- - `VERSION` (const): const VERSION: "0.15.0-next.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
231
+ - `VERSION` (const): const VERSION: "0.16.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
232
232
 
233
233
  ## @jgengine/core/runtime/adapter
234
234
 
@@ -264,6 +264,13 @@
264
264
  - `CommandValidationError` (type): type CommandValidationError = { reason: string } — ⚠ undocumented
265
265
  - `RunCommandResult` (type): type RunCommandResult = | { ok: true; snapshot: GameRuntimeSnapshot } | { ok: false; reason: string } — ⚠ undocumented
266
266
 
267
+ ## @jgengine/core/runtime/context/objectSlots
268
+
269
+ - `ObjectSlotRejection` (type): type ObjectSlotRejection = | "unknown-object" | "no-slot-inventory" | "unknown-inventory" | "no-space" | "wrong-kind" | "slot-occupied" | "invalid-slot" | "insufficient" — Why an object slot mutation was refused — the instance, its catalog entry, the player inventory, or the declared `accepts`/capacity.
270
+ - `ObjectSlotResult` (type): type ObjectSlotResult = { status: "ok" } | { status: "rejected"; reason: ObjectSlotRejection } — Outcome of an object slot mutation: applied, or refused with a reason and no state change on either side.
271
+ - `ObjectSlotTransfer` (interface): interface ObjectSlotTransfer — Which player inventory a transfer moves between, and what it moves.
272
+ - `SceneObjectSlots` (interface): interface SceneObjectSlots — Per-instance container contents for placed objects whose catalog entry declares `slotInventory` — the engine side of "install a GPU into a rack", "put an item in a chest". Contents live on `SceneObject.slots`, so they replicate and save with the object rather than needing a game-owned table keyed by `instanceId`, and every mutation is validated against the declared `accepts` and stack limits.
273
+
267
274
  ## @jgengine/core/runtime/gameContext
268
275
 
269
276
  - `CatalogEntityRole` (type): type CatalogEntityRole = "player" | "enemy" | "hostile" | "npc" | "vehicle" — ⚠ undocumented
@@ -402,6 +409,11 @@
402
409
  - `MotionIntentBatch` (interface): interface MotionIntentBatch — ⚠ undocumented
403
410
  - `MotionIntents` (interface): interface MotionIntents — Seam for game code to reach the motion the shell's FrameDriver otherwise owns privately (#162.4). Game code calls `impulse`, `pushHorizontal`, `setVerticalVelocity`, and/or `setY` from `onTick` or commands; the shell calls `takePending()` once per frame, before integrating gravity, to drain what accumulated. `setY` wins over physics for that frame; impulses add to the velocity the driver is about to integrate; a later `setVerticalVelocity` replaces that velocity outright. Horizontal pushes compose with the walk controller (#282.4): they add to its horizontal velocity and decay naturally as it re-blends toward input — knockback, dashes, explosion shoves without raw `setPose` offsets.
404
411
 
412
+ ## @jgengine/core/runtime/objectRows
413
+
414
+ - `fromRuntimeObjectRow` (function): function fromRuntimeObjectRow(row: RuntimeObjectRow): SceneObject — Inverse of {@link toRuntimeObjectRow}: rebuild the live placed object a host persisted.
415
+ - `toRuntimeObjectRow` (function): function toRuntimeObjectRow(object: SceneObject): RuntimeObjectRow — Convert a live placed object into its persisted row — the bridge between `ctx.scene.object` and `GameRuntimeSnapshot.server.objects`/`chunks[].objects`. Per-instance `state` is carried as `flags`. Presentation overrides (`visual`, `animation`) are catalog- and authoring-resolved and deliberately stay off the persisted row — put anything a command must own in `state`.
416
+
405
417
  ## @jgengine/core/runtime/perContext
406
418
 
407
419
  - `perContext` (function): function perContext<T>(init: (ctx: GameContext) => T): (ctx: GameContext) => T — Per-`GameContext` lazy state, keyed by context identity through a `WeakMap` so it is reclaimed when the context is — the seam a game uses instead of a module-global `Map`, which is process-global and bleeds across worlds when one host serves several `serverId`s (#632). `init` runs once per distinct context on first access; the same context always resolves to the same value. This mirrors the engine's built-in per-ctx pattern (`ctx.player.motion`, `stepPlayerMovement`).
@@ -441,7 +453,7 @@
441
453
  - `RuntimeChunkRow` (type): type RuntimeChunkRow = { chunkKey: string; objects: RuntimeObjectRow[]; entities: RuntimeEntityRow[]; flags?: Record<string, unknown>; } — ⚠ undocumented
442
454
  - `RuntimeEntityRow` (type): type RuntimeEntityRow = { instanceId: string; catalogId: string; position?: [number, number, number]; rotationY?: number; parentSpace?: string; group?: string; stats?: Record<string, { current: number; max: number; min?: number }>; targetInstanceId?: string | null; userId?: string; } — ⚠ undocumented
443
455
  - `RuntimeInventorySlot` (type): type RuntimeInventorySlot = { item: string; count: number; slot?: number; } — ⚠ undocumented
444
- - `RuntimeObjectRow` (type): type RuntimeObjectRow = { instanceId: string; catalogId: string; position: [number, number, number]; rotationY?: number; parentSpace?: string; flags?: Record<string, unknown>; } — ⚠ undocumented
456
+ - `RuntimeObjectRow` (type): type RuntimeObjectRow = { instanceId: string; catalogId: string; position: [number, number, number]; rotationY?: number; parentSpace?: string; /** Opaque per-instance state; the persisted name for `SceneObject.state`. */ flags?: Record<string, unknown>; /** Container contents for a catalog `slotInve… — The persisted form of a placed `SceneObject`: identity, placement, per-instance state, and slot contents. Convert with `toRuntimeObjectRow`/`fromRuntimeObjectRow` (`runtime/objectRows`).
445
457
  - `RuntimePlayerRow` (type): type RuntimePlayerRow = { userId: string; inventories: Record<string, RuntimeInventorySlot[]>; economy: Record<string, number>; unlocks: string[]; quests?: unknown; social?: unknown; leaderboard?: Record<string, number>; session?: Record<string, unknown>; } — ⚠ undocumented
446
458
  - `RuntimeProfileRow` (type): type RuntimeProfileRow = { userId: string; gameId: string; player: RuntimePlayerRow; updatedAt: number; } — ⚠ undocumented
447
459
  - `RuntimeServerRow` (type): type RuntimeServerRow = { entities: RuntimeEntityRow[]; objects: RuntimeObjectRow[]; session: Record<string, unknown>; feeds?: Record<string, unknown[]>; } — ⚠ undocumented
@@ -100,6 +100,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
100
100
 
101
101
  - `ws` (function) · `import { ws } from "@jgengine/core/runtime/adapter"`
102
102
 
103
+ ## object-slot-inventory — placed objects hold validated per-instance container contents that replicate and save with the placement
104
+
105
+ - `SceneObjectSlots` (interface) · `import { SceneObjectSlots } from "@jgengine/core/runtime/context/objectSlots"`
106
+
107
+ ## placed-object-rehydrate — rebuild live placed objects, with their state and slot contents, from persisted snapshot rows
108
+
109
+ - `fromRuntimeObjectRow` (function) · `import { fromRuntimeObjectRow } from "@jgengine/core/runtime/objectRows"`
110
+
111
+ ## placed-object-rows — a placed object's per-instance state and container contents convert to and from the persisted snapshot row a host stores
112
+
113
+ - `toRuntimeObjectRow` (function) · `import { toRuntimeObjectRow } from "@jgengine/core/runtime/objectRows"`
114
+
103
115
  ## render-fallback-diagnostics — Enable/disable the dev-only placeholder-fallback probe.
104
116
 
105
117
  - `armFallbackSeams` (function) · `import { armFallbackSeams } from "@jgengine/core/devtools/fallbackSeams"`
@@ -14,14 +14,21 @@
14
14
  - `ConvexSaveBackendOptions` (interface): interface ConvexSaveBackendOptions — Config for {@link createConvexSaveBackend} — the Convex client, optional function refs (defaults to the `saves.*` convention), and an optional key `namespace`.
15
15
  - `ConvexSaveFunctions` (interface): interface ConvexSaveFunctions — The three Convex functions a save backend calls — a `query` that returns the stored string (or `null`) and two `mutation`s. Point them at your app's own module, or lean on the default `saves.*` convention.
16
16
  - `DEFAULT_CONVEX_POSE_RULES` (const): const DEFAULT_CONVEX_POSE_RULES: PoseSyncRules — ⚠ undocumented
17
+ - `GameServerHelpers` (type): type GameServerHelpers = { loadSnapshot: (ctx: JGMutationCtx, serverId: string) => Promise<LoadedServerSnapshot | null>; applyCommand: (args: { gameId: string; loadedRevision: number; currentRevision: number; snapshot: GameRuntimeSnapshot; actorUserId: string; command: string; input: unknown; }) => … — The plain-function half of {@link createGameServerFunctions}, bound to the same runtime registry and auth mode as its mutations. Reach for it when a host mutation must pair snapshot work with its own table writes in one transaction, which a pre-registered mutation cannot do.
17
18
  - `HostedGameConfig` (interface): interface HostedGameConfig — One game the hosted path can serve, bound per `gameId`: its definition plus content lookup.
18
19
  - `HostedWorldInvocation` (interface): interface HostedWorldInvocation<T> — Everything one stateless invocation reconstructs a hosted world from: the persisted record via `store`, plus the member roster and held inputs the snapshot doesn't carry, and the op to apply to the fresh session.
19
20
  - `HostedWorldOutcome` (interface): interface HostedWorldOutcome<T> — What one stateless invocation produced: the op's value, the resulting revision, whether the world changed (and was saved back), and the post-op roster.
21
+ - `JGDataModel` (type): type JGDataModel = DataModelFromSchemaDefinition<typeof schemaForTypes> — Data model of the {@link jgengineTables} schema — the shape a host's own Convex ctx must satisfy to call the exported persistence helpers.
22
+ - `JGMutationCtx` (type): type JGMutationCtx = GenericMutationCtx<JGDataModel> — Mutation ctx accepted by {@link loadServerSnapshot} / {@link persistServerSnapshot}.
20
23
  - `JG_HOSTED_TICK_MS` (const): const JG_HOSTED_TICK_MS: 1000 — Default minimum elapsed ms before `tickHostedWorlds` advances a world — override via the factory's `tickMs`.
21
24
  - `JG_RUNTIME_TICK_MS` (const): const JG_RUNTIME_TICK_MS: 1000 — ⚠ undocumented
22
25
  - `JgAuthMode` (type): type JgAuthMode = "anonymous" | "required" — ⚠ undocumented
23
26
  - `JgCronSpec` (type): type JgCronSpec = { name: string; intervalSeconds: number; functionKey: "tickActiveServers" | "flushDirtyServers"; } — ⚠ undocumented
27
+ - `LoadedServerSnapshot` (type): type LoadedServerSnapshot = { server: ServerDoc; runtime: GameRuntime; snapshot: GameRuntimeSnapshot; } — A server resolved for runtime work: its row, its registered runtime, and its hydrated snapshot.
24
28
  - `REVISION_CONFLICT_REASON` (const): const REVISION_CONFLICT_REASON: "Revision conflict" — ⚠ undocumented
29
+ - `RunCommandArgs` (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; externalId?: string; } — Arguments of the `runCommand` mutation, and of the equivalent {@link GameServerHelpers.runCommand} helper.
30
+ - `RunCommandOutcome` (type): type RunCommandOutcome = { ok: true } | { ok: false; reason: string } — Outcome of a runtime command: applied, or refused with a reason (unknown server, non-member, validation, revision conflict).
31
+ - `ServerDoc` (type): type ServerDoc = DocumentByName<JGDataModel, "jgGameServers"> — A `jgGameServers` row — the server handle the persistence helpers load from and write back to.
25
32
  - `canJoinPrivateServer` (function): function canJoinPrivateServer(args: { isMember: boolean; joinCode: string | undefined; suppliedCode: string | undefined; }): boolean — Private-server join-code gate. Existing members always pass; non-members must present a matching `joinCode` (loose-normalized via {@link normalizeJoinCode}). Callers still decide whether the server is private — this only answers the code/membership half.
26
33
  - `createConvexBackend` (function): function createConvexBackend<TRawPresenceRow extends { actorExternalId: string } = { actorExternalId: string }, TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string>(options: ConvexBackendOptions<TRawPresenceRow, TPresenceRow>): ConvexBackend<TPresenceRow, TPresenceLo… — ⚠ undocumented
27
34
  - `createConvexChatSync` (function): function createConvexChatSync(client: ConvexReactClient, api: ConvexGameApi, config: ConvexGameTransportConfig, serverId: string): ChatSync — ⚠ undocumented
@@ -36,6 +43,8 @@
36
43
  - `defaultConvexGameApi` (function): function defaultConvexGameApi(): ConvexGameApi — ⚠ undocumented
37
44
  - `defaultConvexSaveFunctions` (function): function defaultConvexSaveFunctions(): ConvexSaveFunctions — Default `saves.read` / `saves.write` / `saves.remove` refs for apps that follow the convention — pass your own `functions` to override.
38
45
  - `isListablePublicly` (function): function isListablePublicly(visibility: SessionVisibility | undefined): boolean — True when a server's `visibility` should surface in public listings / browse results.
46
+ - `loadServerSnapshot` (function): function loadServerSnapshot(ctx: JGMutationCtx, server: ServerDoc, runtime: GameRuntime): Promise<GameRuntimeSnapshot> — Hydrate a server's `GameRuntimeSnapshot` from its row, its members' profiles, and its world chunks. Reach for it when writing your own mutation that must read runtime state before touching host tables.
47
+ - `persistServerSnapshot` (function): function persistServerSnapshot(ctx: JGMutationCtx, server: ServerDoc, snapshot: GameRuntimeSnapshot, save: SaveConfig): Promise<void> — Write a snapshot back: server row, dirty player profiles, dirty chunks, and drained leaderboard increments, all under `save`. Pair it with your own table writes to keep both in one transaction.
39
48
  - `randomConvexPlayerId` (function): function randomConvexPlayerId(): string — ⚠ undocumented
40
49
  - `resolveConvexMultiplayer` (function): function resolveConvexMultiplayer(args: { game: GameDefinition; gameId: string; url?: string; client?: ConvexReactClient; api?: ConvexGameApi; userId?: string; force?: boolean; feedActions?: string[]; poseTuning?: PoseSyncTuning; }): MultiplayerSession | null — ⚠ undocumented
41
50
  - `watchConvexQuery` (function): function watchConvexQuery<TArgs extends DefaultFunctionArgs, TResult, TView>(client: ConvexReactClient, query: FunctionReference<"query", "public", TArgs, TResult>, args: TArgs, toView: (result: TResult) => TView, onChange: (view: TView) => void): () => void — ⚠ undocumented
@@ -96,11 +105,20 @@
96
105
  ## @jgengine/convex/server
97
106
 
98
107
  - `DEFAULT_CONVEX_POSE_RULES` (const): const DEFAULT_CONVEX_POSE_RULES: PoseSyncRules — ⚠ undocumented
108
+ - `GameServerHelpers` (type): type GameServerHelpers = { loadSnapshot: (ctx: JGMutationCtx, serverId: string) => Promise<LoadedServerSnapshot | null>; applyCommand: (args: { gameId: string; loadedRevision: number; currentRevision: number; snapshot: GameRuntimeSnapshot; actorUserId: string; command: string; input: unknown; }) => … — The plain-function half of {@link createGameServerFunctions}, bound to the same runtime registry and auth mode as its mutations. Reach for it when a host mutation must pair snapshot work with its own table writes in one transaction, which a pre-registered mutation cannot do.
109
+ - `JGDataModel` (type): type JGDataModel = DataModelFromSchemaDefinition<typeof schemaForTypes> — Data model of the {@link jgengineTables} schema — the shape a host's own Convex ctx must satisfy to call the exported persistence helpers.
110
+ - `JGMutationCtx` (type): type JGMutationCtx = GenericMutationCtx<JGDataModel> — Mutation ctx accepted by {@link loadServerSnapshot} / {@link persistServerSnapshot}.
99
111
  - `JG_RUNTIME_TICK_MS` (const): const JG_RUNTIME_TICK_MS: 1000 — ⚠ undocumented
100
112
  - `JgAuthMode` (type): type JgAuthMode = "anonymous" | "required" — ⚠ undocumented
101
113
  - `JgCronSpec` (type): type JgCronSpec = { name: string; intervalSeconds: number; functionKey: "tickActiveServers" | "flushDirtyServers"; } — ⚠ undocumented
114
+ - `LoadedServerSnapshot` (type): type LoadedServerSnapshot = { server: ServerDoc; runtime: GameRuntime; snapshot: GameRuntimeSnapshot; } — A server resolved for runtime work: its row, its registered runtime, and its hydrated snapshot.
115
+ - `RunCommandArgs` (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; externalId?: string; } — Arguments of the `runCommand` mutation, and of the equivalent {@link GameServerHelpers.runCommand} helper.
116
+ - `RunCommandOutcome` (type): type RunCommandOutcome = { ok: true } | { ok: false; reason: string } — Outcome of a runtime command: applied, or refused with a reason (unknown server, non-member, validation, revision conflict).
117
+ - `ServerDoc` (type): type ServerDoc = DocumentByName<JGDataModel, "jgGameServers"> — A `jgGameServers` row — the server handle the persistence helpers load from and write back to.
102
118
  - `canJoinPrivateServer` (function): function canJoinPrivateServer(args: { isMember: boolean; joinCode: string | undefined; suppliedCode: string | undefined; }): boolean — Private-server join-code gate. Existing members always pass; non-members must present a matching `joinCode` (loose-normalized via {@link normalizeJoinCode}). Callers still decide whether the server is private — this only answers the code/membership half.
103
119
  - `isListablePublicly` (function): function isListablePublicly(visibility: SessionVisibility | undefined): boolean — True when a server's `visibility` should surface in public listings / browse results.
120
+ - `loadServerSnapshot` (function): function loadServerSnapshot(ctx: JGMutationCtx, server: ServerDoc, runtime: GameRuntime): Promise<GameRuntimeSnapshot> — Hydrate a server's `GameRuntimeSnapshot` from its row, its members' profiles, and its world chunks. Reach for it when writing your own mutation that must read runtime state before touching host tables.
121
+ - `persistServerSnapshot` (function): function persistServerSnapshot(ctx: JGMutationCtx, server: ServerDoc, snapshot: GameRuntimeSnapshot, save: SaveConfig): Promise<void> — Write a snapshot back: server row, dirty player profiles, dirty chunks, and drained leaderboard increments, all under `save`. Pair it with your own table writes to keep both in one transaction.
104
122
 
105
123
  ## @jgengine/core/multiplayer
106
124
 
@@ -137,10 +155,12 @@
137
155
  - `createPoseSyncGate` (function): function createPoseSyncGate(tuning: PoseSyncTuning): PoseSyncGate — ⚠ undocumented
138
156
  - `createPushToTalk` (function): function createPushToTalk(config?: { mode?: PushToTalkMode; onChange?: (transmitting: boolean) => void; }): PushToTalk — ⚠ undocumented
139
157
  - `findByJoinCode` (function): function findByJoinCode(listings: readonly SessionListing[], code: string): SessionListing | null — ⚠ undocumented
158
+ - `fromRuntimeObjectRow` (function): function fromRuntimeObjectRow(row: RuntimeObjectRow): SceneObject — Inverse of {@link toRuntimeObjectRow}: rebuild the live placed object a host persisted.
140
159
  - `normalizeJoinCode` (function): function normalizeJoinCode(code: string): string — ⚠ undocumented
141
160
  - `quickMatch` (function): function quickMatch(listings: readonly SessionListing[], filter: MatchFilter = {}): SessionListing | null — ⚠ undocumented
142
161
  - `resolveGuestSession` (function): function resolveGuestSession(seed?: string): AuthSession — ⚠ undocumented
143
162
  - `sessionPlayer` (function): function sessionPlayer(session: AuthSession): PlayerIdentity — ⚠ undocumented
163
+ - `toRuntimeObjectRow` (function): function toRuntimeObjectRow(object: SceneObject): RuntimeObjectRow — Convert a live placed object into its persisted row — the bridge between `ctx.scene.object` and `GameRuntimeSnapshot.server.objects`/`chunks[].objects`. Per-instance `state` is carried as `flags`. Presentation overrides (`visual`, `animation`) are catalog- and authoring-resolved and deliberately stay off the persisted row — put anything a command must own in `state`.
144
164
  - `validateFeedWrite` (function): function validateFeedWrite(gate: FeedWriteGate | undefined, action: string): { ok: true } | { ok: false; reason: string } — ⚠ undocumented
145
165
 
146
166
  ## @jgengine/core/multiplayer/chatContract
@@ -4,6 +4,14 @@
4
4
 
5
5
  Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*.
6
6
 
7
+ ## convex-load-server-snapshot — read a hosted server's runtime snapshot inside a host-written Convex mutation
8
+
9
+ - `loadServerSnapshot` (function) · `import { loadServerSnapshot } from "@jgengine/convex"`
10
+
11
+ ## convex-persist-server-snapshot — write runtime state and host tables in one Convex transaction
12
+
13
+ - `persistServerSnapshot` (function) · `import { persistServerSnapshot } from "@jgengine/convex"`
14
+
7
15
  ## convex-save — save a game to Convex (cloud) through the SaveBackend seam
8
16
 
9
17
  - `createConvexSaveBackend` (function) · `import { createConvexSaveBackend } from "@jgengine/convex"`
@@ -15,3 +23,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
15
23
  ## host-listing-filter — exclude private sessions from public browse results
16
24
 
17
25
  - `isListablePublicly` (function) · `import { isListablePublicly } from "@jgengine/convex"`
26
+
27
+ ## placed-object-rehydrate — rebuild live placed objects, with their state and slot contents, from persisted snapshot rows
28
+
29
+ - `fromRuntimeObjectRow` (function) · `import { fromRuntimeObjectRow } from "@jgengine/core/multiplayer"`
30
+
31
+ ## placed-object-rows — a placed object's per-instance state and container contents convert to and from the persisted snapshot row a host stores
32
+
33
+ - `toRuntimeObjectRow` (function) · `import { toRuntimeObjectRow } from "@jgengine/core/multiplayer"`
@@ -33,7 +33,7 @@ Once a session resolves, the shell (`GameHost`'s internal player) wires it up wi
33
33
 
34
34
  A synced pose (`@jgengine/ws/protocol`'s `WsPose`/`WsPresenceRow`) carries an optional `appearance: Record<string, string | number | boolean>` alongside position/rotation — primitive-valued client-set tags (skin id, mount id, active emote, team color) riding the same pose-sync frames as position, so a remote player's cosmetic state updates on the existing presence channel instead of a second round-trip.
35
35
 
36
- **Server side** — `@jgengine/convex/server` ships an entire authoritative Convex backend as factories, not a template to copy: `jgengineTables()` (schema spread), `createGameServerFunctions({ runtimes?, auth? })`, `createLeaderboardFunctions({ auth? })`, `createPresenceFunctions({ auth?, freshWindowMs? })`, `createChatFunctions({ auth?, historyLimit?, maxBodyLength?, minIntervalMs? })`, and `jgengineCronSpecs()` (tick/flush interval metadata for a `crons.ts`). A consumer's `convex/` directory is ~25 lines: `schema.ts` (`defineSchema({ ...jgengineTables() })`), four one-line re-export files (`runtime.ts`, `leaderboard.ts`, `presence.ts`, `chat.ts`, each just calling its factory), and `crons.ts` registering the tick (1s) + flush (60s) internal mutations — see `examples/convex-host` for the reference shape. No game-specific code lives there; any JGengine game can point at the same deployment. Games without a registered `GameRuntime` fall back to a no-save runtime that only understands `engine.ping`; pass `createGameServerFunctions({ runtimes: [createGameRuntime({ gameId, commands, loop, save })] })` to make `runCommand` / tick / save actually do something.
36
+ **Server side** — `@jgengine/convex/server` ships an entire authoritative Convex backend as factories, not a template to copy: `jgengineTables()` (schema spread), `createGameServerFunctions({ runtimes?, auth? })`, `createLeaderboardFunctions({ auth? })`, `createPresenceFunctions({ auth?, freshWindowMs? })`, `createChatFunctions({ auth?, historyLimit?, maxBodyLength?, minIntervalMs? })`, and `jgengineCronSpecs()` (tick/flush interval metadata for a `crons.ts`). A consumer's `convex/` directory is ~25 lines: `schema.ts` (`defineSchema({ ...jgengineTables() })`), four one-line re-export files (`runtime.ts`, `leaderboard.ts`, `presence.ts`, `chat.ts`, each just calling its factory), and `crons.ts` registering the tick (1s) + flush (60s) internal mutations — see `examples/convex-host` for the reference shape. No game-specific code lives there; any JGengine game can point at the same deployment. Games without a registered `GameRuntime` fall back to a no-save runtime that only understands `engine.ping`; pass `createGameServerFunctions({ runtimes: [createGameRuntime({ gameId, commands, loop, save })] })` to make `runCommand` / tick / save actually do something. When a host mutation must pair snapshot work with its own table writes in one transaction, use the factory's `helpers` (`loadSnapshot`, `applyCommand`, `persistSnapshot`, `runCommand` — same registry and auth mode) or the standalone `loadServerSnapshot` / `persistServerSnapshot` exports.
37
37
 
38
38
  Auth defaults to `"anonymous"` (`JgAuthMode`) on every factory — the client's `externalId` is trusted as claimed, fine for local dev but spoofable. Pass `{ auth: "required" }` to every factory for production; the resolved actor becomes `ctx.auth.getUserIdentity()`'s `subject` and `externalId` is only cross-checked against it, never trusted alone.
39
39