@jgengine/node 0.18.0 → 0.18.1
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 +114 -2
- package/dist/fileWorldStore.d.ts +5 -0
- package/dist/fileWorldStore.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/persistence.js +2 -2
- package/dist/worldServer.js +6 -7
- package/package.json +3 -3
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/jgengine/SKILL.md +2 -0
- package/skills/jgengine/api.md +94 -19
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine-multiplayer/SKILL.md +25 -0
- package/skills/jgengine-multiplayer/api.md +78 -36
- package/skills/jgengine-multiplayer/capabilities.md +37 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
The lockstep game SDK set is `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`
|
|
3
|
+
The lockstep game SDK set is `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets}`
|
|
4
4
|
(this file’s 0.x.y releases). The CLI package `jgengine` and `@jgengine/github` use
|
|
5
5
|
**separate** version lines and may lag. Format follows [Keep a Changelog](https://keepachangelog.com);
|
|
6
6
|
each release **leads with a Migrate block** — the concrete steps to move a game
|
|
@@ -17,6 +17,13 @@ between (`--json` for structured output).
|
|
|
17
17
|
|
|
18
18
|
## [Unreleased]
|
|
19
19
|
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Separate SDK publication validation from external Games quality checks while retaining the full local and main-branch gates.
|
|
23
|
+
|
|
24
|
+
- Correct foot-IK endpoint recomputation after hip rotation; verify reachable targets and clamping without changing bone lengths.
|
|
25
|
+
- Restore release CI prerequisites and make catalog-loader tests independent of the external Games checkout.
|
|
26
|
+
|
|
20
27
|
<!--
|
|
21
28
|
Every PR that changes `packages/*/src` records its consumer-facing change here, so
|
|
22
29
|
the next release's notes are complete by construction. Add a bullet under the right
|
|
@@ -26,6 +33,106 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
33
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
34
|
-->
|
|
28
35
|
|
|
36
|
+
## 0.18.1
|
|
37
|
+
|
|
38
|
+
### Migrate
|
|
39
|
+
|
|
40
|
+
- **Bump lockstep SDK packages to `^0.18.1`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets}`. CLI `jgengine` is `0.15.1`; `@jgengine/github` is `0.5.1`.
|
|
41
|
+
- Declare `topology: "shared"` on runtime definitions. Shared Convex hosts migrate the bounded legacy roster into indexed membership and per-player session rows; use `getServerCapacity` for stable lobby subscriptions.
|
|
42
|
+
- Put every command's read scope on its definition. Omitted scopes now load the actor and no chunks; explicitly request whole-world reads only where needed. Trusted host mutations can compose writes through `helpers.runCommand`.
|
|
43
|
+
- Handle typed join outcomes (`ok` and `reason`) or use `useServerSession` and `JoinGate`. Pass runtimes to `jgengineCronSpecs`; tick registration requires an actual tick hook.
|
|
44
|
+
- Declare currency precision with `decimals` and remove game-side cent conversions. Public amounts stay in major units and wallet writes round through integer minor units.
|
|
45
|
+
- Replace custom presence, chat, territory and tick batching with `createWorldPresenceStore`, chat helpers, chunk territory, and `forEachOnlinePlayer`. Legacy claim backfills must finish before removing their old schema.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- Indexed shared membership and capacity rows, bounded neighborhood poses with 100 ms server/client gates, durable rate limits, authenticated diagnostics and bounded retention.
|
|
50
|
+
- Territory footprint planning and atomic placement claims, chat validation and headless chat/join/territory surfaces, elapsed accrual, safe quantities, and player-profile reset through the initializer.
|
|
51
|
+
- An explicit `--patch` release option.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- Added smooth interpolation for remote presence player poses.
|
|
56
|
+
- WS ping/pong messages and client RTT sampling on the backend object.
|
|
57
|
+
- Added skin-backed HUD chrome variables, font loading, and the procedural `HudSkinPreview` fixture.
|
|
58
|
+
|
|
59
|
+
- Core gamepad snapshot model, deadzone/curve resolver, and controller glyph labels.
|
|
60
|
+
- Shell gamepad source polls connected pads, merges analog actions, supports synthetic `?gamepad=1` input, and exposes `ctx.input.rumble`.
|
|
61
|
+
- Core action context stacks layer gameplay, menus, and passthrough overlays with snapshot/restore; shell action tracking now respects the active context.
|
|
62
|
+
- Shell audio now supports listener orientation and per-sound Web Audio panners. `setListenerPose` accepts `{ position, forward, up }`; bare positions remain supported for one release.
|
|
63
|
+
- Core perception tracks deterministic sight, hearing, occlusion, and bounded observation memory for AI.
|
|
64
|
+
- `syncWorldColliders` now keeps terrain and static object colliders synchronized with a physics backend.
|
|
65
|
+
- Core decision graphs provide deterministic selector, sequence, condition, action, and utility runtimes with snapshot/restore.
|
|
66
|
+
- Core behavior descriptors can run registered decision graphs at the interest-scheduler cadence.
|
|
67
|
+
- Core navigation meshes provide deterministic polygon routing, off-mesh links, closest-point queries, and nav raycasts.
|
|
68
|
+
- WS sessions now issue resume tickets and retain disconnected memberships for a 15-second grace window, allowing reconnects to rejoin without replacing player state.
|
|
69
|
+
- Shell entity sprites can play atlas-backed sprite clips while preserving raw texture sprites.
|
|
70
|
+
- Tilemap layers can render atlas-backed textured tiles as one instanced quad mesh with parallax.
|
|
71
|
+
- Core pixel-perfect orthographic frustum and pixel-grid camera snapping are available to shell cameras.
|
|
72
|
+
- Core sprite atlas adapters and deterministic 2D sprite clip playback primitives.
|
|
73
|
+
- `src/art-direction.md` scaffold and `check-art-direction` gate for created games.
|
|
74
|
+
- Host-authoritative shell sessions now expose each accepted world frame to the local prediction buffer, reconciling and snapping the possessed pose only when drift exceeds its threshold.
|
|
75
|
+
- Shared GLTF loader configuration for Draco and KTX2-compressed assets, with CDN defaults and renderer capability detection.
|
|
76
|
+
- Material map overrides now support metalness, emissive, and height maps on standard and physical materials.
|
|
77
|
+
- Presentation and editor documents now accept gradient, HDRI/EXR, and cube-map environment sources.
|
|
78
|
+
- Authored `light` markers now feed bounded point/spot punctual lights with an eight-light default budget and editor warning.
|
|
79
|
+
- Graphics profiles now resolve per-tier render scale, shadow budgets, culling distance, particles, cascades, and post stages; games can override them with `defineGame({ graphics })`.
|
|
80
|
+
- `defineGame({ physics: { backend } })` now adopts a capsule character controller for shell/headless movement and registers the backend simulation after movement, including floor collision and step-up behavior.
|
|
81
|
+
- Added the `physics-probe` dev scene for Rapier crate, ragdoll, and vehicle motion.
|
|
82
|
+
- Assets now expose import-spec validation and magic-byte classification for supported source files.
|
|
83
|
+
- Hosted WS sessions now support spectator joins, viewer roles, throttled low-priority snapshot modules, and default command rate limits.
|
|
84
|
+
- `@jgengine/navbake` bakes deterministic navigation meshes and editor documents can store named bake data.
|
|
85
|
+
- Core tactical queries derive cover points and score bounded candidate positions.
|
|
86
|
+
- Added createSnapshotBuffer for delayed snapshot interpolation.
|
|
87
|
+
|
|
88
|
+
### Migrate
|
|
89
|
+
|
|
90
|
+
- `AudioEngine.setListenerPose` now accepts `{ position, forward, up }`. Bare `Vec3` positions remain compatible for one release and use forward `[0, 0, -1]` with up `[0, 1, 0]`.
|
|
91
|
+
- **Hosted world stores are asynchronous.** Implement `HostedWorldStore.load()` and `save()` as `Promise`-returning methods and construct persisted sessions with `createHostedWorldSessionAsync`; synchronous tests can keep using `memoryWorldStore`.
|
|
92
|
+
- **Look presets now default to `neutral`.** Games that relied on the previous implicit cinematic
|
|
93
|
+
sky/post stack should set `look: "photoreal"` (or keep `look: "cinematic"`) explicitly.
|
|
94
|
+
|
|
95
|
+
- **`ServerTickPlan.due` is `{ id, runs }[]` instead of a repeated id list.** `planServerTick` repeated a system id once per missed interval, and the obvious way to consume that — `due.includes(id)` or a `Set` — ran the system once while the returned anchor had already advanced by every repeat, silently dropping the catch-up work. A host now reads `runs` (or `tickRunCount(plan, id)`) and loops that many times.
|
|
96
|
+
- **`CommandDef.validate` / `CommandDef.apply` take a fourth argument, `nowMs`.** Existing implementations that ignore it are unaffected. Commands that took a `now` in their `input` should read this instead — the client supplied the old one.
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
|
|
100
|
+
- Hosted game runner queues authoritative inputs by simulation tick and applies the in-force frame each fixed step.
|
|
101
|
+
- Create scaffolds now use a 60 Hz simulation with interpolation enabled by default.
|
|
102
|
+
|
|
103
|
+
- Rigged `ModelConfig` entries can opt into shell foot IK with named thigh/shin/foot bones; the driver raycasts the shared scene terrain, solves each chain, and fades corrections while airborne.
|
|
104
|
+
|
|
105
|
+
- Animation graph clips may carry root-bone position tracks; states with `rootMotion: true` return the sampled displacement and the shell applies it to the entity while restoring the root bone's bind translation.
|
|
106
|
+
|
|
107
|
+
- Added renderer-free two-bone, FABRIK, and look-at inverse-kinematics solvers to `@jgengine/core`.
|
|
108
|
+
|
|
109
|
+
- Shell model animation now routes states and one-shots through the shared animation graph runtime.
|
|
110
|
+
|
|
111
|
+
- CI now clones the external games repository with `GAMES_CLONE_TOKEN` and runs the game shape, content, front-end, art-direction, and feel gates against it; those gates fail loudly in CI when the checkout is missing while remaining no-ops locally.
|
|
112
|
+
|
|
113
|
+
- `jgengine create` now accepts `--player`, `--ground`, and `--scene` so generated games can choose their model, terrain ground, and empty or starter scene.
|
|
114
|
+
|
|
115
|
+
- **Directional shadow frustum config actually reaches the GPU.** `shadowCameraSize` on a single (non-cascaded) shadow light was silently inert: the shell wrote the bounds through R3F pierced props and nothing refreshed the shadow camera's projection matrix, so three rendered depth with its default ~10-unit box at the world origin — the reason no open-world game had cast shadows away from spawn. The shell now syncs the projection (and reallocates the shadow map when `shadowMapSize` changes). `bloom.threshold`'s doc also now states it measures raw HDR before the exposure stage.
|
|
116
|
+
- **CSM (`cascades > 1`) no longer over-lights streamed meshes or clobbers material shaders.** Cascade scoping is injected per material, but the scan ran every 30 frames, so a model that streamed in rendered lit by every cascade light at once (a cascades× sun) until the next scan — a sub-second flash on desktop, the whole shot on capture rigs. The scan now runs every frame behind a WeakSet, and `setupMaterial` chains with a material's own `onBeforeCompile` (rim light, detail maps) instead of replacing it.
|
|
117
|
+
|
|
118
|
+
- **The host's wall clock reaches the runtime hooks.** `RuntimeInitContext` (so `onInit`, `onNewPlayer`, and `onTick` alike) carries `nowMs`, the host timestamp for that call. Anything keyed to real time — a UTC date rollover, a `lastTickAt` anchor read by non-tick code, an RNG seed or `createdAt` stamp — reads it rather than accumulating `dtSeconds` against an epoch persisted in the snapshot, which drifts by whatever the tick loses to stalls and `maxCatchUp` clamping. `GameRuntime.hydrate` takes an optional `nowMs`; `tick`, `joinPlayer`, and `runCommand` take an optional trailing `nowMs`. All default to `Date.now()`, and the shipped WS and Convex hosts pass the timestamp they already computed to plan the tick.
|
|
119
|
+
- **Imported models cast and receive shadows.** `cloneModelScene` never set `castShadow`/`receiveShadow` on a GLB's meshes (only the magenta fallback box had them), so every catalog model in every game rendered shadowless however the lights were configured. Cloned models now default to `ModelConfig.shadows: "both"`; set `"cast"`, `"receive"`, or `"none"` per model (foliage cards, decals, viewmodels).
|
|
120
|
+
- **`jgengine create` installs `game-design` and `jgengine-ui` by default.** The minimal skill set was intake, editor, and verify, so a real project never saw the greenfield design contract or the UI art-direction template unless someone ran `jgengine skills --all`; `MINIMAL_GAME_SKILLS` now carries both.
|
|
121
|
+
|
|
122
|
+
### Added
|
|
123
|
+
|
|
124
|
+
- **Host-authoritative world replication over WS.** Hosted sessions expose revision-cursor pulls, so server subscribers receive compact world diffs and automatically recover with a fresh baseline after missed revisions; viewer-specific projections still receive per-viewer baselines.
|
|
125
|
+
- **Animation graph — `ModelAnimationConfig.graph` and `@jgengine/core/anim/animGraph`.** A serializable, layered animation state machine: `clip`, `blend1D`, and `blend2D` states, transitions as data (parameter comparisons, consumed triggers, exit times, crossfade durations), masked and additive layers, and clip events. `createAnimGraphRuntime` is headless and owns every clip's playback time and weight, so the shell's mixer only seeks and weights actions (`useModelAnimation` takes the graph path when `graph` is set) and a headless host or test advances the same graph without three.js. The shell feeds the entity's smoothed ground speed as `speed`, merges parameters from the entity blackboard key `ANIM_PARAMS_KEY`, arms triggers from `entity.animation` / `combat.hitReaction` / `entity.died`, and emits the new `animation.event` for clip events (foot plants, hit frames). `locomotionGraph()` builds the previous idle/walk/run plus one-shot behavior as one authored graph.
|
|
126
|
+
- **`PhysicsBackend` — one interface over interchangeable rigid-body solvers.** `@jgengine/core/physics/physicsBackend` defines bodies (`BodyDesc` with a box / sphere / capsule / convex / trimesh / heightfield shape union, quaternion rotation, angular velocity, layers and mask, CCD flag), joints, `raycast`, `shapecast`, `overlap`, contact events, and `snapshot`/`restore` with stable handles; `capabilities` says what a backend really does. `createPhysicsWorldBackend` puts the zero-dependency `PhysicsWorld` behind it (translation-only, non-box shapes collide as their bounding box, warned once), and `runPhysicsBackendConformance` is the behavioral contract any adapter runs. `createCharacterController` (`@jgengine/core/movement/characterController`) is the first consumer: a capsule collide-and-slide controller with step-up, slope limit, ceiling test, crouch with headroom check, ground snapping, and moving-platform carry, pure over any backend and retunable during play.
|
|
127
|
+
- **Fixed-step simulation with render interpolation — `defineGame({ simulation: { hz } })` and `ctx.sim`.** Every driver (shell frame, HUD-only, headless runner, authoritative host) now steps through one `SimLoop` (`@jgengine/core/runtime/simLoop`): a fixed `hz` accumulates real time and runs equal-dt steps behind a catch-up cap (`maxCatchUpSteps`, default 5) instead of spiralling; the default stays `"variable"` (one step per frame) so existing games are unchanged until they opt in. Under a fixed rate the shell renders entities at `ctx.sim.renderPose(id)` — the pose interpolated between the previous and current step (`@jgengine/core/runtime/poseInterpolation`), with teleports past `snapDistance` snapping — so a 20 Hz sim no longer stutters. `ctx.sim.tick()` counts steps and `InputFrame.tick` carries it over the wire; `ctx.sim.addStage({ phase, run })` inserts per-step work before movement, after movement, or after `onTick` (the hook a physics backend or netcode buffer registers through). System fixed-rate groups gain the same substep cap. For replay and rollback: `seededRng` streams expose `state()`/`restore()` (`rngStateOf`/`restoreRng` for any `ctx.rng`), `createSimSnapshotRegistry`/`createContextSimSnapshot` capture world, clock, loop, and rng cursor together, `createInputRecorder` logs frames by tick, and `HeadlessRunner` gains `snapshot()`/`restore()`/`tick()` — a test proves that replaying recorded input from a snapshot reproduces every entity pose.
|
|
128
|
+
- **City and street races share one generator.** `extractCircuitRoute` lifts a sealed-off race lap from a city street network; city rules, editor baking, playground modes, and the shell renderer carry the same street dials through the resulting race surface and junction dressing.
|
|
129
|
+
- **`ctx.particles` — the game-reachable particle seam.** Game logic queues one-shot bursts (`ctx.particles.burst(config, count)`) and keyed standing emitters (`attach`/`retune`/`detach`, with `follow` to track a scene entity) as plain serializable data (`@jgengine/core/vfx/particleDirector`); the shell auto-mounts a renderer that turns them into pooled GPU point clouds via the existing `createParticleSystem`/`ParticleField`, caps pool sizes by graphics quality (low 128 / medium 256 / high 512 per effect, degrading density instead of dropping effects), and reaps finished bursts. Dust, sparks, exhaust, and debris no longer require forking the shell; headless tests assert the requested effects straight off the director.
|
|
130
|
+
- **`resolveSourceWalkerStep(source, cache, position, stepX, stepZ, options)`** (`@jgengine/core/movement/solidObstacles`) — `resolveWalkerStep` with no `GameContext`, for movement rules that live in a pure package with no React or three.js. `SolidObstacleSource`, `ObstacleReachCache`, `createObstacleReachCache`, `sourceObstacleReach`, `sourceObstaclesNear`, and `slideStep` are public alongside it instead of `@internal`, so a game no longer has to push the resolver up into its app layer and inject it back down. `resolveWalkerStep`'s doc now states that it already resolves X and Z separately, so a caller doing its own axis-split must hand it one axis per call.
|
|
131
|
+
- **`tickRunCount(plan, id)`** (`@jgengine/core/time/serverTick`) — how many runs a plan owes a system, or 0 when it is not due.
|
|
132
|
+
- **`grass()` exposes the clump/lighting knobs its shader already had.** `GrassEnvironmentConfig` gains optional `bladeBend`, `tuftRadius`, `colorVariation`, and `normalLift`, forwarded by the shell's environment renderer — a world descriptor can now author bushy wind-combed scrub instead of the fixed lawn-blade look; defaults unchanged.
|
|
133
|
+
- **`TerrainDetailConfig.sweeps`** — the detail shader's two macro colour sweeps (sun-dried patches, cooler wet pockets) take per-world RGB multipliers instead of hardcoded meadow tints. The old green-leaning "lush pocket" default painted moss onto arid/ashen biomes; defaults are unchanged, so a desert world should set warm/neutral `sweeps` explicitly.
|
|
134
|
+
- **`@jgengine/rapier`** — a Rapier-backed `PhysicsBackend` adapter with native capsules, shapecasts, rotation, CCD, joints, and collision queries.
|
|
135
|
+
|
|
29
136
|
## 0.18.0
|
|
30
137
|
|
|
31
138
|
### Migrate
|
|
@@ -165,13 +272,18 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
165
272
|
- **`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.
|
|
166
273
|
- **`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.
|
|
167
274
|
- **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.
|
|
168
|
-
|
|
275
|
+
- **A street race is a section of a city, sealed off.** `extractCircuitRoute(network, rules)` (`@jgengine/core/world/raceCircuit`) lifts a closed lap out of an existing street network — the same asphalt the open world already drives on — and seals every side street leaving it. Returns checkpoints ready for `raceTrack()`, start/finish, grid, corners, kerbs, and sectors. Circuit-mode networks take the same path (their loop edges are the cycle). The city volume kind gains a Race group (`race`, `raceLapLength`, `raceArterial`, …) that resolves a world-space `ResolvedRace` on `ResolvedCity.race`; `CityRenderer` draws the lap surface, kerbs, start/finish, grid, and barrier seals. Playground adds a Street race mode over the same shared slider set.
|
|
276
|
+
- **Parked vehicles and junction signals on a city district.** `CityRules.parkedCars` / `signals` seed curb and pad fleets plus mast-arm signals (arterials) and sign posts (the rest); `ResolvedCity.vehicles` / `signals` hand them to `CityRenderer`.
|
|
169
277
|
- **`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).
|
|
170
278
|
|
|
171
279
|
### Fixed
|
|
172
280
|
|
|
173
281
|
- **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.
|
|
174
282
|
|
|
283
|
+
- **CityRenderer junctions use the welded carriageway-union seam end-to-end.** District roads go through `buildTrimmedIntersections` + `buildIntersectionMarkings` with the same compact opts as StreetGeometryPreview / playground (`curbReturnRadius: 2`, `apronMargin: 0.25`). Plaza hull aprons, fallback discs, and fat zebra ladders are gone; race resurfacing stops at junction mouths so welded patches stay visible.
|
|
284
|
+
- **Denser urban fabric for metro/Manhattan districts.** Commercial class setbacks/spacing tighten toward a streetwall; slabs/towers pick L-plans and mid-block courts; dense blocks push footprints to the curb and paint courtyards as hard courts rather than lawn leftovers.
|
|
285
|
+
- **City and track are one slider surface again.** `CityRules` and the editor `generate_streets` bake now forward every `StreetNetworkRules` dial (`elevation`, `maxGrade`, `compactness`, `sidewalkWidth`, `outline`, `residentialBranches`); `check-street-rule-parity` fails CI if either drifts. Playground no longer hides layout sliders when you switch modes — modes are presets over one shared set.
|
|
286
|
+
- **City facades dissolve their window stripes before they alias into speckled static**, and ground floors now read as storefronts rather than repeating floor 30. Block interiors paint developed fabric instead of raw lawn, and crosswalks only land at real junctions.
|
|
175
287
|
- **`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.
|
|
176
288
|
- **`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.
|
|
177
289
|
- **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.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HostedWorldStore } from "@jgengine/core/runtime/hostedWorldSession";
|
|
2
|
+
/** JSON-file implementation of the asynchronous hosted-world persistence seam.
|
|
3
|
+
* @capability hosted-world-persistence Persist hosted authoritative world snapshots in a local file.
|
|
4
|
+
*/
|
|
5
|
+
export declare function fileWorldStore(path: string): HostedWorldStore;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
/** JSON-file implementation of the asynchronous hosted-world persistence seam.
|
|
4
|
+
* @capability hosted-world-persistence Persist hosted authoritative world snapshots in a local file.
|
|
5
|
+
*/
|
|
6
|
+
export function fileWorldStore(path) {
|
|
7
|
+
return {
|
|
8
|
+
async load() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === "ENOENT")
|
|
14
|
+
return null;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
async save(record) {
|
|
19
|
+
await mkdir(dirname(path), { recursive: true });
|
|
20
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
21
|
+
await writeFile(temporary, JSON.stringify(record), "utf8");
|
|
22
|
+
await rename(temporary, path);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/persistence.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { applyLeaderboardRows, leaderboardRowKey, profileLeaderboardStats, topLeaderboardRows, trimFeedEntries, } from "@jgengine/core/runtime/hostPersistence";
|
|
5
|
-
import {
|
|
5
|
+
import { asyncMemoryWorldStore } from "@jgengine/core/runtime/hostedWorldSession";
|
|
6
6
|
export { memoryPersistence } from "@jgengine/ws/host";
|
|
7
7
|
/** Default {@link WorldPersistence}: an isolated in-memory {@link HostedWorldStore} per `gameId`/`serverId`, lost on process exit. */
|
|
8
8
|
export function memoryWorldPersistence() {
|
|
@@ -13,7 +13,7 @@ export function memoryWorldPersistence() {
|
|
|
13
13
|
const existing = stores.get(key);
|
|
14
14
|
if (existing !== undefined)
|
|
15
15
|
return existing;
|
|
16
|
-
const created =
|
|
16
|
+
const created = asyncMemoryWorldStore();
|
|
17
17
|
stores.set(key, created);
|
|
18
18
|
return created;
|
|
19
19
|
},
|
package/dist/worldServer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHostedWorldSessionAsync } from "@jgengine/core/runtime/hostedWorldSession";
|
|
2
2
|
import { createWorldGameHost } from "@jgengine/ws/worldHost";
|
|
3
3
|
import { memoryWorldPersistence } from "./persistence.js";
|
|
4
4
|
import { createGameWsServer } from "./wsServer.js";
|
|
@@ -10,11 +10,11 @@ export function createWorldGameServer(options) {
|
|
|
10
10
|
const liveSessions = new Map();
|
|
11
11
|
const host = createWorldGameHost({
|
|
12
12
|
now: clock,
|
|
13
|
-
session: ({ gameId, serverId }) => {
|
|
13
|
+
session: async ({ gameId, serverId }) => {
|
|
14
14
|
const resolved = resolveGame(gameId);
|
|
15
15
|
if (resolved === null)
|
|
16
16
|
return null;
|
|
17
|
-
const session =
|
|
17
|
+
const session = await createHostedWorldSessionAsync({
|
|
18
18
|
definition: resolved.game,
|
|
19
19
|
content: resolved.content,
|
|
20
20
|
now: clock,
|
|
@@ -37,9 +37,8 @@ export function createWorldGameServer(options) {
|
|
|
37
37
|
interval = null;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
function flush() {
|
|
41
|
-
|
|
42
|
-
session.save();
|
|
40
|
+
async function flush() {
|
|
41
|
+
await Promise.all([...liveSessions.values()].map((session) => session.save()));
|
|
43
42
|
}
|
|
44
43
|
return {
|
|
45
44
|
host,
|
|
@@ -60,7 +59,7 @@ export function createWorldGameServer(options) {
|
|
|
60
59
|
},
|
|
61
60
|
async close() {
|
|
62
61
|
stop();
|
|
63
|
-
flush();
|
|
62
|
+
await flush();
|
|
64
63
|
await ws.close();
|
|
65
64
|
},
|
|
66
65
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/node",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.1",
|
|
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.18.
|
|
35
|
-
"@jgengine/ws": "^0.18.
|
|
34
|
+
"@jgengine/core": "^0.18.1",
|
|
35
|
+
"@jgengine/ws": "^0.18.1",
|
|
36
36
|
"ws": "^8.18.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
@@ -146,4 +146,4 @@ When sufficient, return a concise orientation: objective, progress, decisions, c
|
|
|
146
146
|
- Multi-session engine work often spans `docs/plans/`, GitHub issues, and stacked PRs — point at those; do not paste full plan bodies.
|
|
147
147
|
- Prefer `bun run agent:bootstrap --check` status, branch names, and PR links as state anchors.
|
|
148
148
|
- Local parallelism uses `.claude/worktrees/`; never invent `C:\tmp\…` Codex-shared trees.
|
|
149
|
-
-
|
|
149
|
+
- Durable process changes belong in skills, not only the handoff.
|
package/skills/jgengine/SKILL.md
CHANGED
|
@@ -9,6 +9,8 @@ Read the JGengine README for package truth, commands, layering, and license —
|
|
|
9
9
|
|
|
10
10
|
## 1. Establish the target
|
|
11
11
|
|
|
12
|
+
For greenfield builds, write `src/art-direction.md` before any code.
|
|
13
|
+
|
|
12
14
|
Treat the pitch as a unique composition of needs, not a genre to fill in — never reach for a genre kit, preset, archetype, or class template ("default sports car", "default RPG", "default FPS") in the SDK or skills (see [AGENTS.md](../../../AGENTS.md)). Use `game-design` to turn the pitch into a testable player promise, verbs, loops, progression, failure, and completion scenario. Use `level-design` when play depends on authored spaces, routes, encounters, exploration, or spatial pacing. Capture only decisions that change architecture:
|
|
13
15
|
|
|
14
16
|
- player point of view and controls
|
package/skills/jgengine/api.md
CHANGED
|
@@ -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.18.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.18.1" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
- `OwnershipDecision` (type): type OwnershipDecision = "expose" | "bake" | "reject" — The boundary's resolution for one object.
|
|
25
25
|
- `OwnershipDiagnostic` (interface): interface OwnershipDiagnostic — One boundary problem found while auditing a set of declarations.
|
|
26
26
|
- `OwnershipVerdict` (interface): interface OwnershipVerdict — The full verdict for one object: its decision plus the flags and diagnostic the editor needs to act on it.
|
|
27
|
-
- `PhysicsConfig` (interface): interface PhysicsConfig — World gravity and jump tuning, plus
|
|
27
|
+
- `PhysicsConfig` (interface): interface PhysicsConfig — World gravity and jump tuning, plus the optional rigid-body backend and character capsule.
|
|
28
28
|
- `ProviderCapabilities` (interface): interface ProviderCapabilities — What a provider lets the editor do with one of its objects. Absent flags default to `false`.
|
|
29
29
|
- `RuntimeProvenance` (interface): interface RuntimeProvenance — An object owned entirely by runtime or procedural code with no document entry. Not authorable unless its provider can bake it into schema-valid authored data.
|
|
30
30
|
- `SCENE_OWNERSHIP_MANIFEST_VERSION` (const): const SCENE_OWNERSHIP_MANIFEST_VERSION: 1 — Current {@link SceneOwnershipManifest} schema version.
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
- `plots` (function): function plots(config: PlotsWorldConfig = {}): WorldFeature — Declares a subdivided-plots world — farming, base-building, and other parcel-based layouts.
|
|
53
53
|
- `rain` (function): function rain(config: RainEnvironmentConfig = {}): RainEnvironmentDescriptor — Declares a rainfall weather effect for `environment()` — area, density, speed, wind, and drop width/opacity.
|
|
54
54
|
- `road` (function): function road(config: RoadEnvironmentConfig): RoadEnvironmentDescriptor — Declare a road ribbon for an `environment()` world; the shell drapes and renders it over the terrain.
|
|
55
|
-
- `seededRng` (function): function seededRng(seed: string | number):
|
|
55
|
+
- `seededRng` (function): function seededRng(seed: string | number): SeededRng — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
|
|
56
56
|
- `seededStreams` (function): function seededStreams(seed: string | number): (stream: string) => () => number — Derives independent, deterministic {@link seededRng} streams from one base seed, keyed by stream name.
|
|
57
57
|
- `snow` (function): function snow(config: SnowEnvironmentConfig = {}): SnowEnvironmentDescriptor — Declares a snowfall weather effect for `environment()` — area, density, drift, wind, and flake opacity.
|
|
58
58
|
- `terrain` (function): function terrain(config: TerrainEnvironmentConfig = {}): TerrainEnvironmentDescriptor — Declares a heightfield terrain patch for `environment()` — bounds, noise, materials, and flatten masks.
|
|
@@ -228,14 +228,14 @@
|
|
|
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.18.
|
|
231
|
+
- `VERSION` (const): const VERSION: "0.18.1" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
232
232
|
|
|
233
233
|
## @jgengine/core/runtime/adapter
|
|
234
234
|
|
|
235
235
|
- `MultiplayerAdapterConfig` (type): type MultiplayerAdapterConfig = | { kind: "convex"; topology?: MultiplayerTopology; authority?: MultiplayerAuthority } | { kind: "ws"; topology?: MultiplayerTopology; url?: string; authority?: MultiplayerAuthority } | { kind: "socketio"; topology?: MultiplayerTopology; url?: string; authority?: Mult… — ⚠ undocumented
|
|
236
236
|
- `MultiplayerAuthority` (type): type MultiplayerAuthority = "server" | "client" — Where the world simulation is authoritative.
|
|
237
237
|
- `MultiplayerTopology` (type): type MultiplayerTopology = "shared" | "lobbies" | "private" — ⚠ undocumented
|
|
238
|
-
- `ServersPoolConfig` (type): type ServersPoolConfig = {
|
|
238
|
+
- `ServersPoolConfig` (type): type ServersPoolConfig = { minPlayersToStart?: number; adapter: MultiplayerAdapterConfig; } & ( | { topology: "shared"; maxServers?: number; slotsPerServer?: number } | { topology?: "rooms"; maxServers: number; slotsPerServer: number } ) — ⚠ undocumented
|
|
239
239
|
- `adapterOf` (function): function adapterOf(multiplayer: unknown): MultiplayerAdapterConfig | null — ⚠ undocumented
|
|
240
240
|
- `convex` (function): function convex(config?: { topology?: MultiplayerTopology; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — Convex transport. Omitting `authority` (or passing `"client"`) is **presence-only** — prefer `convexPresence()` to name that intent explicitly. Pass `{ authority: "server" }` for a shared, host-authoritative world — see `examples/HOSTED.md`.
|
|
241
241
|
- `convexPresence` (function): function convexPresence(config?: { topology?: MultiplayerTopology }): MultiplayerAdapterConfig — Presence-only Convex transport — each client runs its own `onTick`; only presence/feeds/chat sync. Sugar for `convex({ ...config, authority: "client" })`.
|
|
@@ -248,7 +248,7 @@
|
|
|
248
248
|
- `offline` (function): function offline(): MultiplayerAdapterConfig — Explicit single-player adapter. Solo games never need this — omitting `multiplayer` in the shell `defineGame` already defaults to offline; pass it only where an adapter value is structurally required.
|
|
249
249
|
- `p2p` (function): function p2p(config?: { topology?: MultiplayerTopology; room?: string; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — Serverless peer-to-peer (WebRTC) session — one peer hosts, friends join by room code.
|
|
250
250
|
- `resolveAuthority` (function): function resolveAuthority(multiplayer: unknown): MultiplayerAuthority | null — Resolved authority for a multiplayer config. - `offline` / missing adapter → `null` (single-player; not multiplayer authority). - unset or `"client"` → `"client"` (presence-only; each client ticks). - `"server"` → host-authoritative shared sim.
|
|
251
|
-
- `servers` (function): function servers(config: ServersPoolConfig):
|
|
251
|
+
- `servers` (function): function servers(config: ServersPoolConfig): { topology: "shared" | "rooms"; maxServers: number; slotsPerServer: number; minPlayersToStart?: number | undefined; adapter: MultiplayerAdapterConfig; } | { topology: "shared" | "rooms"; maxServers: number; slotsPerServer: number; minPlayersToStart?: numb… — Resolve the shared-world pool to one unbounded world; room pools keep explicit capacities.
|
|
252
252
|
- `socketIo` (function): function socketIo(config?: { topology?: MultiplayerTopology; url?: string; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — ⚠ undocumented
|
|
253
253
|
- `ws` (function): function ws(config?: { topology?: MultiplayerTopology; url?: string; authority?: MultiplayerAuthority }): MultiplayerAdapterConfig — WebSocket transport. Omitting `authority` (or passing `"client"`) is **presence-only** — prefer `wsPresence()` to name that intent explicitly. Pass `{ authority: "server" }` for a shared, host-authoritative world — see `examples/HOSTED.md`.
|
|
254
254
|
- `wsPresence` (function): function wsPresence(config?: { topology?: MultiplayerTopology; url?: string }): MultiplayerAdapterConfig — Presence-only WebSocket transport — each client runs its own `onTick`; only presence/feeds/chat sync. Sugar for `ws({ ...config, authority: "client" })`.
|
|
@@ -258,10 +258,16 @@
|
|
|
258
258
|
- `CameraDirector` (interface): interface CameraDirector — ⚠ undocumented
|
|
259
259
|
- `ChaseCameraTuning` (type): type ChaseCameraTuning = Partial< Pick<ChaseCameraConfig, "distance" | "height" | "lookHeight" | "springDamping" | "fov" | "lead" | "bank" | "shakePerSpeed" | "velocityYaw" | "yawResponse"> > — Runtime patch over the static `camera.chase` config — distance/height/fov retuning from gameplay events (#286.11), or a whole driving-feel overlay (speed→FOV, lead, bank, speed shake, drift-lag) applied only while a vehicle is piloted (#1299).
|
|
260
260
|
|
|
261
|
+
## @jgengine/core/runtime/commandInput
|
|
262
|
+
|
|
263
|
+
- `QuantityResult` (type): type QuantityResult = { ok: true; quantity: number } | { ok: false; reason: "invalid-quantity" } — Validated integer quantity or a stable input rejection.
|
|
264
|
+
- `readQuantity` (function): function readQuantity(value: unknown, options: { min?: number; max?: number } = {}): QuantityResult — Read an untrusted whole-item quantity without coercion, truncation, or clamping.
|
|
265
|
+
|
|
261
266
|
## @jgengine/core/runtime/commandRunner
|
|
262
267
|
|
|
263
268
|
- `CommandDef` (type): type CommandDef<TInput = unknown> = { /** * What this command reads and writes, derived from its own input. A host that hydrates through a * scope loads only this slice instead of the whole world, and refuses the command if `apply` then * dirties a player or chunk the scope did not name — writing an… — ⚠ undocumented
|
|
264
269
|
- `CommandScope` (type): type CommandScope = { /** Member ids to hydrate; omit for the whole roster. */ players?: readonly string[]; /** Chunk keys to hydrate; omit for every chunk of the server, `[]` for none. */ chunkKeys?: readonly string[]; } — Which slice of a server has to be hydrated before some work runs against it. Both fields default to "everything", which costs a read per member plus one per chunk — the cost that makes a large shared world unaffordable per mutation. Narrow them when the caller knows what it will touch.
|
|
270
|
+
- `CommandScopeDefinition` (type): type CommandScopeDefinition = Omit<CommandScope, "players"> & { players?: readonly string[] | "actor" } — Declarative read scope; actor is resolved before host hydration.
|
|
265
271
|
- `CommandValidationError` (type): type CommandValidationError = { reason: string } — ⚠ undocumented
|
|
266
272
|
- `RunCommandResult` (type): type RunCommandResult = | { ok: true; snapshot: GameRuntimeSnapshot } | { ok: false; reason: string } — ⚠ undocumented
|
|
267
273
|
|
|
@@ -335,13 +341,14 @@
|
|
|
335
341
|
|
|
336
342
|
## @jgengine/core/runtime/gameRuntime
|
|
337
343
|
|
|
338
|
-
- `GameRuntime` (type): type GameRuntime = { gameId: string; save: SaveConfig; /** * Whether this runtime declares `loop.onTick`. A host's tick cron reads it to skip hydrating and * persisting a server whose `tick` is a no-op by construction — the difference between a world that * costs a
|
|
339
|
-
- `GameRuntimeDefinition` (type): type GameRuntimeDefinition = { gameId: string; save: SaveConfig; commands: Record<string, CommandDef>; loop?: ServerLoopHooks; } — ⚠ undocumented
|
|
340
|
-
- `HydrateInput` (type): type HydrateInput = { gameId: string; serverId: string; serverRow: RuntimeServerRow; playersByUserId: Record<string, RuntimePlayerRow>; chunksByKey: Record<string, RuntimeChunkRow>; revision?: number; } — ⚠ undocumented
|
|
341
|
-
- `RuntimeInitContext` (type): type RuntimeInitContext = { snapshot: GameRuntimeSnapshot; setSnapshot: (snapshot: GameRuntimeSnapshot) => void;
|
|
344
|
+
- `GameRuntime` (type): type GameRuntime = { gameId: string; topology?: "shared" | "rooms"; save: SaveConfig; /** * Whether this runtime declares `loop.onTick`. A host's tick cron reads it to skip hydrating and * persisting a server whose `tick` is a no-op by construction — the difference between a world that * costs a ful… — ⚠ undocumented
|
|
345
|
+
- `GameRuntimeDefinition` (type): type GameRuntimeDefinition = { gameId: string; topology?: "shared" | "rooms"; save: SaveConfig; commands: Record<string, CommandDef>; loop?: ServerLoopHooks; } — ⚠ undocumented
|
|
346
|
+
- `HydrateInput` (type): type HydrateInput = { gameId: string; serverId: string; serverRow: RuntimeServerRow; playersByUserId: Record<string, RuntimePlayerRow>; chunksByKey: Record<string, RuntimeChunkRow>; revision?: number; /** Host wall clock for `onInit`, in ms. Defaults to `Date.now()`. */ nowMs?: number; } — ⚠ undocumented
|
|
347
|
+
- `RuntimeInitContext` (type): type RuntimeInitContext = { snapshot: GameRuntimeSnapshot; setSnapshot: (snapshot: GameRuntimeSnapshot) => void; /** * Host wall clock at the start of this call, in ms. The host already knows it, so anything keyed to * real time — a UTC date rollover, a `lastTickAt` anchor other code paths read, a s… — ⚠ undocumented
|
|
342
348
|
- `RuntimeLoopContext` (type): type RuntimeLoopContext = RuntimeInitContext & { player: { userId: string; isNew: boolean; }; } — ⚠ undocumented
|
|
343
349
|
- `RuntimeWorldContext` (type): type RuntimeWorldContext = RuntimeInitContext & { playerIds: readonly string[]; } — ⚠ undocumented
|
|
344
350
|
- `ServerLoopHooks` (type): type ServerLoopHooks = { onInit?: (ctx: RuntimeInitContext) => void; onNewPlayer?: (ctx: RuntimeLoopContext) => void; onTick?: (ctx: RuntimeWorldContext, dtSeconds: number) => void; /** * What `onNewPlayer` touches, so a join hydrates that instead of the whole world. Only consulted * when `onNewPlay… — ⚠ undocumented
|
|
351
|
+
- `initialPlayerState` (function): function initialPlayerState(runtime: GameRuntime, userId: string, nowMs: number = Date.now()): RuntimePlayerRow — Seed a fresh player through the same onNewPlayer hook as joining, without hydrating or modifying a world.
|
|
345
352
|
|
|
346
353
|
## @jgengine/core/runtime/headlessRunner
|
|
347
354
|
|
|
@@ -375,7 +382,7 @@
|
|
|
375
382
|
|
|
376
383
|
## @jgengine/core/runtime/hostPolicy
|
|
377
384
|
|
|
378
|
-
- `JG_MAX_MEMBERS_PER_SERVER` (const): const JG_MAX_MEMBERS_PER_SERVER: 256 —
|
|
385
|
+
- `JG_MAX_MEMBERS_PER_SERVER` (const): const JG_MAX_MEMBERS_PER_SERVER: 256 — Room-topology ceiling on `slotsPerServer`; shared membership uses indexed rows instead.
|
|
379
386
|
- `JoinCandidate` (type): type JoinCandidate = { memberUserIds: readonly string[]; slotsPerServer: number; visibility?: SessionVisibility | undefined; createdAt: number; } — A row `selectJoinTarget` can choose between — the matchmaking-relevant fields of a server record.
|
|
380
387
|
- `JoinTarget` (type): type JoinTarget<T extends JoinCandidate> = | { kind: "join"; row: T } | { kind: "create" } | { kind: "refuse"; reason: string } — What a join should do with the candidate rows it found.
|
|
381
388
|
- `MatchmakingMode` (type): type MatchmakingMode = "auto" | "singleton" — How auto-match behaves when no `serverId` is supplied.
|
|
@@ -383,11 +390,11 @@
|
|
|
383
390
|
- `isAutoJoinCandidate` (function): function isAutoJoinCandidate(args: { memberUserIds: readonly string[]; slotsPerServer: number; visibility: SessionVisibility | undefined; userId: string; }): boolean — Auto-match candidate when no `serverId` is supplied: already a member, or a public room with free capacity. Private rooms are never auto-picked (join-by-code / direct id only).
|
|
384
391
|
- `isListablePublicly` (function): function isListablePublicly(visibility: SessionVisibility | undefined): boolean — True when a server's `visibility` should surface in public listings / browse results.
|
|
385
392
|
- `isPrivateJoinBlocked` (function): function isPrivateJoinBlocked(args: { visibility: SessionVisibility | undefined; memberUserIds: readonly string[]; userId: string; joinCode: string | undefined; suppliedCode: string | undefined; }): boolean — Whether a private-visibility server blocks this join (non-member without a matching code). Public / undefined visibility never blocks.
|
|
386
|
-
- `isServerFull` (function): function isServerFull(
|
|
393
|
+
- `isServerFull` (function): function isServerFull(memberCount: number, slotsPerServer: number, isMember: boolean): boolean — True when the server has no free slots for a non-member. Existing members never count as "full" so rejoin/leave cycles keep working.
|
|
387
394
|
- `isServerMember` (function): function isServerMember(memberUserIds: readonly string[], userId: string): boolean — Whether `userId` is already on the server's member roster.
|
|
388
395
|
- `selectJoinTarget` (function): function selectJoinTarget<T extends JoinCandidate>(rows: readonly T[], args: { userId: string; mode?: MatchmakingMode; /** Capacity to judge rows against — the host's current option, when it supersedes the stored value. */ slotsPerServer?: number; }): JoinTarget<T> — Resolve an auto-match join against the game's joinable rows under a {@link MatchmakingMode}. Reach for it instead of hand-rolling the "find a room with space, else make one" scan, which is where a singleton world silently shards into per-player copies.
|
|
389
396
|
- `statusAfterLeave` (function): function statusAfterLeave(remainingMemberCount: number, currentStatus: GameServerStatus): GameServerStatus — Status after a leave: empty rooms reopen; non-empty rooms keep their current status.
|
|
390
|
-
- `validateSlotsPerServer` (function): function validateSlotsPerServer(slotsPerServer: number): { ok: true } | { ok: false; reason: string } — Whether a `slotsPerServer` value is a capacity a single hosted server row can actually hold.
|
|
397
|
+
- `validateSlotsPerServer` (function): function validateSlotsPerServer(slotsPerServer: number, topology: "rooms" | "shared" = "rooms"): { ok: true } | { ok: false; reason: string } — Whether a `slotsPerServer` value is a capacity a single hosted server row can actually hold.
|
|
391
398
|
- `withJoinedMember` (function): function withJoinedMember(memberUserIds: readonly string[], userId: string): string[] — Roster after a successful join: unchanged when already a member, else appended.
|
|
392
399
|
- `withoutMember` (function): function withoutMember(memberUserIds: readonly string[], userId: string): string[] — Roster after a leave: `userId` removed; order of remaining members preserved.
|
|
393
400
|
|
|
@@ -405,6 +412,22 @@
|
|
|
405
412
|
- `HostedWorldSessionOptions` (interface): interface HostedWorldSessionOptions<TAssetRef extends ModelAssetRef, TMultiplayer> — Config for {@link createHostedWorldSession}: the game, its persistence store, and the auto-save cadence.
|
|
406
413
|
- `HostedWorldStore` (interface): interface HostedWorldStore — Narrow persistence seam for a hosted world — the {@link HostedWorldRecord} counterpart of `HostPersistence`. Backends implement it (memory/file/sql/convex); the session never names one. A stateful host loads once and saves on a cadence; a stateless host reconstructs from `load()` each invocation.
|
|
407
414
|
- `HostedWorldSync` (type): type HostedWorldSync = | { kind: "baseline"; revision: number; snapshot: WorldSnapshot } | { kind: "diff"; diff: WorldDiff } — A client replication pull: a full baseline (first sync / fell behind) or a diff since the client's cursor.
|
|
415
|
+
- `SyncHostedWorldStore` (interface): interface SyncHostedWorldStore — Synchronous store adapter retained for deterministic in-process tests.
|
|
416
|
+
- `asyncMemoryWorldStore` (function): function asyncMemoryWorldStore(seed?: HostedWorldRecord): HostedWorldStore — Async in-process store for callers exercising the production persistence contract.
|
|
417
|
+
- `createHostedWorldSessionAsync` (function): function createHostedWorldSessionAsync<TAssetRef extends ModelAssetRef, TMultiplayer>(options: Omit<HostedWorldSessionOptions<TAssetRef, TMultiplayer>, "store"> & { store: HostedWorldStore }): Promise<HostedWorldSession> — Build a hosted session from an asynchronous persistence backend.
|
|
418
|
+
|
|
419
|
+
## @jgengine/core/runtime/hostedWorldStore
|
|
420
|
+
|
|
421
|
+
- `HostedWorldRecord` (interface): interface HostedWorldRecord — One hosted world's persisted authoritative state — the unit a {@link HostedWorldStore} loads and saves.
|
|
422
|
+
- `HostedWorldStore` (interface): interface HostedWorldStore — Narrow persistence seam for a hosted world — the {@link HostedWorldRecord} counterpart of `HostPersistence`. Backends implement it (memory/file/sql/convex); the session never names one. A stateful host loads once and saves on a cadence; a stateless host reconstructs from `load()` each invocation.
|
|
423
|
+
- `SyncHostedWorldStore` (interface): interface SyncHostedWorldStore — Synchronous store adapter retained for deterministic in-process tests.
|
|
424
|
+
|
|
425
|
+
## @jgengine/core/runtime/inputRecorder
|
|
426
|
+
|
|
427
|
+
- `InputRecorder` (interface): interface InputRecorder — Tick-indexed input log: record per tick, look up the frame in force at any tick.
|
|
428
|
+
- `InputRecorderState` (interface): interface InputRecorderState — Serializable recorder contents, sorted by tick.
|
|
429
|
+
- `RecordedInput` (interface): interface RecordedInput — An input frame and the first tick it applies to.
|
|
430
|
+
- `createInputRecorder` (function): function createInputRecorder(): InputRecorder — Tick-indexed input log for deterministic replay: record what each tick saw, then feed {@link frameAt} back into a runner stepping from a matching {@link createSimSnapshotRegistry} snapshot.
|
|
408
431
|
|
|
409
432
|
## @jgengine/core/runtime/inputSnapshot
|
|
410
433
|
|
|
@@ -440,6 +463,22 @@
|
|
|
440
463
|
- `planScenarioReset` (function): function planScenarioReset(reset: ScenarioReset): NormalizedScenarioReset — ⚠ undocumented
|
|
441
464
|
- `resetRun` (function): function resetRun<T extends Record<string, unknown>>(scoped: ScopedState<T>): ScopedState<T> — ⚠ undocumented
|
|
442
465
|
|
|
466
|
+
## @jgengine/core/runtime/poseInterpolation
|
|
467
|
+
|
|
468
|
+
- `PoseHistory` (interface): interface PoseHistory — Previous-step pose buffer: capture at each fixed step, sample between steps for rendering.
|
|
469
|
+
- `PoseHistoryState` (interface): interface PoseHistoryState — Serializable previous-step poses keyed by entity id.
|
|
470
|
+
- `RenderPose` (type): type RenderPose = [x: number, y: number, z: number, rotationY: number] — A render pose sampled between two simulation steps: `[x, y, z, rotationY]`.
|
|
471
|
+
- `createPoseHistory` (function): function createPoseHistory(options?: { snapDistance?: number }): PoseHistory — Previous-step pose buffer for render interpolation under a fixed-step loop. Reuses one 4-slot array per entity so a step over N entities allocates only for entities seen for the first time.
|
|
472
|
+
- `lerpAngle` (function): function lerpAngle(from: number, to: number, t: number): number — Shortest-arc interpolation between two yaw angles.
|
|
473
|
+
|
|
474
|
+
## @jgengine/core/runtime/prediction
|
|
475
|
+
|
|
476
|
+
- `PredictionBuffer` (type): type PredictionBuffer<S> = { record(input: InputFrame, dt?: number, observedState?: S): S; reconcile(tick: number, authoritative: S): { error: number; replayed: number; snapped: boolean; state: S }; state(): S; snapshot(): PredictionSnapshot<S>; restore(next: PredictionSnapshot<S>): void; } — Stateful prediction handle supporting recording, reconciliation, inspection, and restoration.
|
|
477
|
+
- `PredictionRecord` (type): type PredictionRecord<S> = { tick: number; input: InputFrame; state: S; dt: number } — One input, tick, and resulting state retained for authoritative replay.
|
|
478
|
+
- `PredictionSnapshot` (type): type PredictionSnapshot<S> = { confirmedTick: number; confirmed: S; predicted: S; records: PredictionRecord<S>[]; } — Serializable prediction state containing the confirmation baseline and pending input history.
|
|
479
|
+
- `PredictionStep` (type): type PredictionStep<S> = (state: S, input: InputFrame, dt: number) => S — Deterministic state transition used to advance one predicted input frame.
|
|
480
|
+
- `createPredictionBuffer` (function): function createPredictionBuffer<S>(config: { initial: S; step: PredictionStep<S>; maxTicks: number; startTick?: number; dt?: number; /** Error above which a caller should snap its rendered pose to the reconciled result. */ snapThreshold?: number; }): PredictionBuffer<S> — Keeps local input predictions ahead of the last server confirmation. Reconciliation replaces the confirmed state and deterministically replays unconfirmed inputs through the supplied step function. `maxTicks` bounds retained work and memory; dropped inputs are folded into the confirmed baseline.
|
|
481
|
+
|
|
443
482
|
## @jgengine/core/runtime/runtimeSave
|
|
444
483
|
|
|
445
484
|
- `RuntimeSave` (interface): interface RuntimeSave — Whole-world save/load bound to a live world and a pluggable backend. `save()` captures `target.snapshot()` and writes it; `load()` reads it back and `target.hydrate()`s the whole world. In `autosave` mode it also writes on a trailing timer while the world keeps changing. Named slots, versioned migration, and offline↔cloud (backend swap) all come for free from the underlying save store.
|
|
@@ -454,18 +493,49 @@
|
|
|
454
493
|
- `SaveConfig` (type): type SaveConfig = | "none" | { auto: string; scope: SaveScope; } — ⚠ undocumented
|
|
455
494
|
- `SaveScope` (type): type SaveScope = "player" | "chunks" | "player+chunks" — ⚠ undocumented
|
|
456
495
|
|
|
496
|
+
## @jgengine/core/runtime/simContext
|
|
497
|
+
|
|
498
|
+
- `SimContext` (interface): interface SimContext — `ctx.sim`: the simulation stepper shared by every driver plus the render-side interpolation it enables. Drivers call {@link advance} with real elapsed seconds and do their per-step work inside the callback; renderers call {@link renderPose} each frame.
|
|
499
|
+
- `SimContextState` (interface): interface SimContextState — Serializable `ctx.sim` state: loop position plus the interpolation pose history.
|
|
500
|
+
- `SimStage` (interface): interface SimStage — Extra work a game or engine seam (a physics backend, a netcode buffer) runs inside every simulation step.
|
|
501
|
+
- `SimStagePhase` (type): type SimStagePhase = "beforeMovement" | "afterMovement" | "afterTick" — Where a registered stage runs inside one simulation step.
|
|
502
|
+
- `createSimContext` (function): function createSimContext(options: { config?: SimulationConfig; entities: EntityStore }): SimContext — Build `ctx.sim` for a context; `createGameContext` calls this from `definition.simulation`.
|
|
503
|
+
|
|
504
|
+
## @jgengine/core/runtime/simLoop
|
|
505
|
+
|
|
506
|
+
- `SimAdvanceResult` (interface): interface SimAdvanceResult — What one {@link SimLoop.advance} did.
|
|
507
|
+
- `SimLoop` (interface): interface SimLoop — The stepper handle: advance with real time, read the tick and interpolation alpha, retune the rate, snapshot and restore.
|
|
508
|
+
- `SimLoopState` (interface): interface SimLoopState — Serializable loop position: the tick count and the real seconds accumulated toward the next fixed step.
|
|
509
|
+
- `SimulationConfig` (interface): interface SimulationConfig — How the simulation is stepped: a fixed rate with a catch-up cap, or once per frame with the frame's dt.
|
|
510
|
+
- `createSimLoop` (function): function createSimLoop(initial?: SimulationConfig): SimLoop — The simulation stepper every driver (shell frame, headless runner, authoritative host) advances through, so the sim sees the same dt sequence regardless of who drives it. Fixed mode accumulates real time and steps in equal slices with a catch-up cap; variable mode steps once per advance.
|
|
511
|
+
|
|
512
|
+
## @jgengine/core/runtime/simSnapshot
|
|
513
|
+
|
|
514
|
+
- `SimSnapshot` (type): type SimSnapshot = Record<string, unknown> — Captured contributor data keyed by contributor id.
|
|
515
|
+
- `SimSnapshotContributor` (interface): interface SimSnapshotContributor<T = unknown> — One piece of simulation state that can be captured and restored by id.
|
|
516
|
+
- `SimSnapshotRegistry` (interface): interface SimSnapshotRegistry — Registry of {@link SimSnapshotContributor}s that captures and restores them together.
|
|
517
|
+
- `createContextSimSnapshot` (function): function createContextSimSnapshot(ctx: GameContext): SimSnapshotRegistry — The engine's own contributors for a context: replicated world state, the sim clock, the loop and pose history, and the rng cursor when `ctx.rng` is a seeded stream. Games register theirs on the same registry.
|
|
518
|
+
- `createSimSnapshotRegistry` (function): function createSimSnapshotRegistry(): SimSnapshotRegistry — A registry of everything that must round-trip for a simulation to resume bit-for-bit: world state, the clock, the rng cursor, the loop accumulator, and whatever a physics backend or game adds. Pair with {@link createInputRecorder} for replay.
|
|
519
|
+
|
|
457
520
|
## @jgengine/core/runtime/snapshot
|
|
458
521
|
|
|
459
522
|
- `GameRuntimeSnapshot` (type): type GameRuntimeSnapshot = { version: number; gameId: string; serverId: string; server: RuntimeServerRow; players: Record<string, RuntimePlayerRow>; chunks: Record<string, RuntimeChunkRow>; revision: number; dirty: { server: boolean; players: string[]; chunks: string[]; }; } — ⚠ undocumented
|
|
460
523
|
- `RUNTIME_SNAPSHOT_VERSION` (const): const RUNTIME_SNAPSHOT_VERSION: 1 — ⚠ undocumented
|
|
461
|
-
- `RuntimeChunkRow` (type): type RuntimeChunkRow = { chunkKey: string; objects: RuntimeObjectRow[]; entities: RuntimeEntityRow[]; flags?: Record<string
|
|
524
|
+
- `RuntimeChunkRow` (type): type RuntimeChunkRow = { /** Cell key to owner id, persisted with its spatial chunk. */ territory?: Record<string, string>; territoryReceipts?: Record<string, { claimedAt: number; costPaid: number }>; chunkKey: string; objects: RuntimeObjectRow[]; entities: RuntimeEntityRow[]; flags?: Record<string,… — ⚠ undocumented
|
|
462
525
|
- `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
|
|
463
526
|
- `RuntimeInventorySlot` (type): type RuntimeInventorySlot = { item: string; count: number; slot?: number; } — ⚠ undocumented
|
|
464
527
|
- `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`).
|
|
465
|
-
- `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?:
|
|
528
|
+
- `RuntimePlayerRow` (type): type RuntimePlayerRow = { territoryOwnedCount?: number; ownedTerritoryChunkKeys?: string[]; userId: string; inventories: Record<string, RuntimeInventorySlot[]>; economy: Record<string, number>; unlocks: string[]; quests?: unknown; social?: unknown; leaderboard?: Record<string, number>; session?: Rec… — ⚠ undocumented
|
|
466
529
|
- `RuntimeProfileRow` (type): type RuntimeProfileRow = { userId: string; gameId: string; player: RuntimePlayerRow; updatedAt: number; } — ⚠ undocumented
|
|
467
530
|
- `RuntimeServerRow` (type): type RuntimeServerRow = { entities: RuntimeEntityRow[]; objects: RuntimeObjectRow[]; session: Record<string, unknown>; feeds?: Record<string, unknown[]>; } — ⚠ undocumented
|
|
468
531
|
|
|
532
|
+
## @jgengine/core/runtime/snapshotBuffer
|
|
533
|
+
|
|
534
|
+
- `SnapshotBuffer` (type): type SnapshotBuffer<T> = { push(atMs: number, value: T): void; sampleAt(nowMs: number): SnapshotBufferSample<T> | null; snapshot(): SnapshotBufferState<T>; restore(next: SnapshotBufferState<T>): void; retune(config: { delayMs?: number; capacity?: number }): void; } — Handle for a timestamped delayed snapshot buffer.
|
|
535
|
+
- `SnapshotBufferSample` (type): type SnapshotBufferSample<T> = { before: T; after: T; alpha: number; } — A sampled pair of snapshots and interpolation factor.
|
|
536
|
+
- `SnapshotBufferState` (type): type SnapshotBufferState<T> = { delayMs: number; capacity: number; entries: Array<{ atMs: number; value: T }>; } — Serializable snapshot-buffer state.
|
|
537
|
+
- `createSnapshotBuffer` (function): function createSnapshotBuffer<T>(config: { delayMs: number; capacity: number; }): SnapshotBuffer<T> — Stores timestamped snapshots and samples a delayed, interpolatable pair.
|
|
538
|
+
|
|
469
539
|
## @jgengine/core/runtime/transport
|
|
470
540
|
|
|
471
541
|
- `FeedUnsubscribe` (type): type FeedUnsubscribe = () => void — ⚠ undocumented
|
|
@@ -473,15 +543,19 @@
|
|
|
473
543
|
- `GameRuntimeFeedView` (type): type GameRuntimeFeedView = { action: string; entries: unknown[]; } — ⚠ undocumented
|
|
474
544
|
- `GameRuntimeFeeds` (type): type GameRuntimeFeeds = { subscribeServer: ( serverId: string, onChange: (view: GameRuntimeServerView | null) => void, ) => FeedUnsubscribe; subscribePlayer: ( args: { serverId: string }, onChange: (view: GameRuntimePlayerView | null) => void, ) => FeedUnsubscribe; subscribeFeed: ( args: { serverId:… — ⚠ undocumented
|
|
475
545
|
- `GameRuntimePlayerView` (type): type GameRuntimePlayerView = { userId: string; gameId: string; playerState: unknown; updatedAt: number; } — ⚠ undocumented
|
|
476
|
-
- `GameRuntimeServerView` (type): type GameRuntimeServerView = { serverId: string; gameId: string; revision: number; memberUserIds: string[]; serverState: unknown; updatedAt: number; } — ⚠ undocumented
|
|
477
|
-
- `GameRuntimeTransport` (type): type GameRuntimeTransport = { joinServer: (args: { gameId: string; serverId?: string }) => Promise<
|
|
478
|
-
- `
|
|
546
|
+
- `GameRuntimeServerView` (type): type GameRuntimeServerView = { serverId: string; gameId: string; revision: number; memberUserIds: string[]; serverState: unknown | WorldSyncFrame; updatedAt: number; } — ⚠ undocumented
|
|
547
|
+
- `GameRuntimeTransport` (type): type GameRuntimeTransport = { joinServer: (args: { gameId: string; serverId?: string; role?: MultiplayerRole }) => Promise<JoinServerOutcome>; leaveServer: (args: { serverId: string }) => Promise<void>; runCommand: (args: RunCommandArgs) => Promise<TransportRunCommandResult>; } — ⚠ undocumented
|
|
548
|
+
- `JoinServerOutcome` (type): type JoinServerOutcome = | (JoinServerResult & { ok: true }) | { ok: false; reason: "full" | "closed" | "unauthorized" } — Join failures are returned to the caller so the triggering surface can offer retry.
|
|
549
|
+
- `JoinServerResult` (type): type JoinServerResult = { serverId: string; isNew: boolean; resumeTicket?: ResumeTicket; } — ⚠ undocumented
|
|
479
550
|
- `LiveGameBackend` (type): type LiveGameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = GameBackend<TPresenceRow, TPresenceLocation, TGameId> & { presenceSync: PresenceSync; pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise<void>; chatSyncFor… — ⚠ undocumented
|
|
551
|
+
- `MultiplayerRole` (type): type MultiplayerRole = "player" | "spectator" — Connection role used for authoritative world access; spectators are read-only.
|
|
480
552
|
- `MultiplayerSession` (type): type MultiplayerSession = { gameId: string; userId: string; backend: LiveGameBackend; feedActions: string[]; } — ⚠ undocumented
|
|
481
553
|
- `PresencePoseRow` (type): type PresencePoseRow = { userId: string; /** Set when the host tracks presence per session, so one user can hold two rows. */ sessionId?: string; /** Actor class, e.g. `"player"` / `"agent"` — a host may clamp each differently. */ kind?: string; /** Display name carried on the row, so a nameplate ne… — ⚠ undocumented
|
|
482
554
|
- `PresenceSync` (type): type PresenceSync = { subscribe: (serverId: string, onChange: (rows: PresencePoseRow[]) => void) => FeedUnsubscribe; syncPose: (serverId: string, pose: PlayerPose) => void; } — ⚠ undocumented
|
|
555
|
+
- `ResumeTicket` (type): type ResumeTicket = { userId: string; serverId: string; token: string; } — A short-lived credential allowing a disconnected client to reclaim one server membership.
|
|
483
556
|
- `RunCommandArgs` (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; } — ⚠ undocumented
|
|
484
557
|
- `TransportRunCommandResult` (type): type TransportRunCommandResult = | { ok: true } | { ok: false; reason: string } — ⚠ undocumented
|
|
558
|
+
- `WorldSyncFrame` (type): type WorldSyncFrame = | { kind: "baseline"; revision: number; snapshot: WorldSnapshot } | { kind: "diff"; revision: number; diff: WorldDiff } — A revision-stamped world baseline or compact diff sent to a server-authoritative client.
|
|
485
559
|
|
|
486
560
|
## @jgengine/core/runtime/visibility
|
|
487
561
|
|
|
@@ -510,6 +584,7 @@
|
|
|
510
584
|
- `chunkCoordAt` (function): function chunkCoordAt(position: readonly [number, number, number], size: number = DEFAULT_CHUNK_SIZE): ChunkCoord — Chunk coordinates containing a world position at `size` units per cell.
|
|
511
585
|
- `chunkKeyAt` (function): function chunkKeyAt(position: readonly [number, number, number], size: number = DEFAULT_CHUNK_SIZE): string — Chunk key containing a world position at `size` units per cell.
|
|
512
586
|
- `chunkKeyOf` (function): function chunkKeyOf(coord: ChunkCoord): string — Chunk key for a cell — the canonical `"cx,cz"` string a `jgWorldChunks` row is stored under. Games used to invent this mapping each time, which made two systems in one game disagree about which row a position belongs to.
|
|
587
|
+
- `chunkKeysAround` (function): function chunkKeysAround(position: readonly [number, number, number], rings = 1, size = DEFAULT_CHUNK_SIZE): string[] — Square chunk neighborhood around a world position, including its own chunk.
|
|
513
588
|
- `chunkKeysInRadius` (function): function chunkKeysInRadius(center: readonly [number, number, number], radius: number, size: number = DEFAULT_CHUNK_SIZE): string[] — Every chunk key whose cell intersects a radius around a world position — the key list a viewer's bounded hydration or client chunk query asks for, instead of loading a whole world.
|
|
514
589
|
- `createEmptyChunkRow` (function): function createEmptyChunkRow(chunkKey: string): RuntimeChunkRow — An empty chunk row for `chunkKey` — the starting value for a cell nothing has been placed in yet.
|
|
515
590
|
- `deleteChunk` (function): function deleteChunk(snapshot: GameRuntimeSnapshot, chunkKey: string): GameRuntimeSnapshot — Drop a chunk from a snapshot and mark the key dirty so the host deletes its stored row.
|
|
@@ -575,5 +650,5 @@
|
|
|
575
650
|
- `environmentContentFromDocument` (function): function environmentContentFromDocument(doc: EditorDocument, options?: EnvironmentContentOptions): EnvironmentContent — Derives the coordinate/placement content of an `environment()` world from its scene document: terrain footprint (via {@link terrainBoundsFromDocument}), ground clearings under authored spawns/POIs (via `clearanceZonesFrom`), and the document's sculpt snapshot. This is the seam that lets a game author its world footprint and flatten regions in the editor instead of hardcoding them — the `world.ts` that consumes it carries only engine tuning.
|
|
576
651
|
- `grounded` (function): function grounded(ctx: Pick<GameContext, "world">, x: number, z: number): readonly [number, number, number] — Ground an XZ point onto the live world surface as `[x, groundY, z]`.
|
|
577
652
|
- `offline` (function): function offline(): MultiplayerAdapterConfig — Explicit single-player adapter. Solo games never need this — omitting `multiplayer` in the shell `defineGame` already defaults to offline; pass it only where an adapter value is structurally required.
|
|
578
|
-
- `seededRng` (function): function seededRng(seed: string | number):
|
|
653
|
+
- `seededRng` (function): function seededRng(seed: string | number): SeededRng — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
|
|
579
654
|
- `useHudLayout` (function): function useHudLayout(options?: { storageKey?: string; snap?: number; locked?: boolean; /** * Scene-document `ui` section — source of truth for panel placement/size. * When provided, hydrates the layout store (and wins over legacy localStorage). */ documentUi?: EditorUiDocument; /** * When true (def… — Layout state for `HudCanvas` — panel placements, edit-mode drag/resize, and per-game persistence.
|
|
@@ -12,10 +12,18 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
12
12
|
|
|
13
13
|
- `collectAuthoredTriggers` (function) · `import { collectAuthoredTriggers } from "@jgengine/shell/gameKit"`
|
|
14
14
|
|
|
15
|
+
## client-side — input prediction with replay reconciliation
|
|
16
|
+
|
|
17
|
+
- `createPredictionBuffer` (function) · `import { createPredictionBuffer } from "@jgengine/core/runtime/prediction"`
|
|
18
|
+
|
|
15
19
|
## collider-autofit — hitboxes and physical bodies fit each kind's rendered model bounds automatically — no hand-tuned sizes
|
|
16
20
|
|
|
17
21
|
- `GameContextModels` (interface) · `import { GameContextModels } from "@jgengine/core/runtime/gameContext"`
|
|
18
22
|
|
|
23
|
+
## command-quantity — validate bounded whole-item counts at command boundaries
|
|
24
|
+
|
|
25
|
+
- `readQuantity` (function) · `import { readQuantity } from "@jgengine/core/runtime/commandInput"`
|
|
26
|
+
|
|
19
27
|
## default-walk-codes — stock WASD + jump key codes for the shell walk controller
|
|
20
28
|
|
|
21
29
|
- `DEFAULT_WALK_CODES` (const) · `import { DEFAULT_WALK_CODES } from "@jgengine/shell/gameKit"`
|
|
@@ -84,6 +92,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
84
92
|
|
|
85
93
|
- `StatBar` (function) · `import { StatBar } from "@jgengine/shell/gameKit"`
|
|
86
94
|
|
|
95
|
+
## input-recorder — tick-indexed input log for replay, rollback, and regression tests
|
|
96
|
+
|
|
97
|
+
- `createInputRecorder` (function) · `import { createInputRecorder } from "@jgengine/core/runtime/inputRecorder"`
|
|
98
|
+
|
|
87
99
|
## loot-table — validate a loot table definition for use with the registry
|
|
88
100
|
|
|
89
101
|
- `lootTable` (function) · `import { lootTable } from "@jgengine/core/authoring"`
|
|
@@ -116,6 +128,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
116
128
|
|
|
117
129
|
- `toRuntimeObjectRow` (function) · `import { toRuntimeObjectRow } from "@jgengine/core/runtime/objectRows"`
|
|
118
130
|
|
|
131
|
+
## pose-interpolation — render entity poses between fixed simulation steps without stutter
|
|
132
|
+
|
|
133
|
+
- `createPoseHistory` (function) · `import { createPoseHistory } from "@jgengine/core/runtime/poseInterpolation"`
|
|
134
|
+
|
|
119
135
|
## render-fallback-diagnostics — Enable/disable the dev-only placeholder-fallback probe.
|
|
120
136
|
|
|
121
137
|
- `armFallbackSeams` (function) · `import { armFallbackSeams } from "@jgengine/core/devtools/fallbackSeams"`
|
|
@@ -126,6 +142,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
126
142
|
- `resetTextureErrors` (function) · `import { resetTextureErrors } from "@jgengine/core/devtools/textureErrors"`
|
|
127
143
|
- `textureErrorsSnapshot` (function) · `import { textureErrorsSnapshot } from "@jgengine/core/devtools/textureErrors"`
|
|
128
144
|
|
|
145
|
+
## runtime — snapshot interpolation
|
|
146
|
+
|
|
147
|
+
- `createSnapshotBuffer` (function) · `import { createSnapshotBuffer } from "@jgengine/core/runtime/snapshotBuffer"`
|
|
148
|
+
|
|
129
149
|
## runtime-save — save/load the whole game world through a pluggable backend, autosave or save points
|
|
130
150
|
|
|
131
151
|
- `createRuntimeSave` (function) · `import { createRuntimeSave } from "@jgengine/core/runtime/runtimeSave"`
|
|
@@ -142,6 +162,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
142
162
|
|
|
143
163
|
- `seededRng` (function) · `import { seededRng } from "@jgengine/shell/gameKit"`
|
|
144
164
|
|
|
165
|
+
## sim-loop — fixed-step or variable simulation stepping with tick count, catch-up cap, and interpolation alpha
|
|
166
|
+
|
|
167
|
+
- `createSimLoop` (function) · `import { createSimLoop } from "@jgengine/core/runtime/simLoop"`
|
|
168
|
+
|
|
169
|
+
## sim-snapshot — capture and restore whole-simulation state for replay, rollback, and regression tests
|
|
170
|
+
|
|
171
|
+
- `createSimSnapshotRegistry` (function) · `import { createSimSnapshotRegistry } from "@jgengine/core/runtime/simSnapshot"`
|
|
172
|
+
|
|
145
173
|
## typed-keyed-store — a cast-free typed handle onto a per-owner keyed family of reactive game-store slots, replayable and host-authoritative
|
|
146
174
|
|
|
147
175
|
- `defineKeyedStore` (function) · `import { defineKeyedStore } from "@jgengine/shell/gameKit"`
|
|
@@ -45,3 +45,28 @@ Use [capabilities.md](capabilities.md) for intent-to-import discovery, [api.md](
|
|
|
45
45
|
- Presence/chat/voice are session channels, not game-state ownership.
|
|
46
46
|
- Do not couple a primitive to WebSocket, Convex, Postgres, or one deployment topology.
|
|
47
47
|
- Reward allocation, inventory mutation, and progression policy remain gameplay/combat concerns.
|
|
48
|
+
|
|
49
|
+
## Shared-world capacity and persistence
|
|
50
|
+
|
|
51
|
+
| Topology | Membership and session state | Command reads | Presence fan-out |
|
|
52
|
+
| --- | --- | --- | --- |
|
|
53
|
+
| Rooms | **256 members maximum** in the room document | Declared actor/player and chunk scope | Neighborhood index |
|
|
54
|
+
| Shared | Indexed `jgServerMembers`, separate `jgServerCapacity`, per-user profile session state; no 256-member engine cap | Declared actor/player and chunk scope; player-only writes avoid the world row | Neighborhood index, 10 Hz maximum default pose writes |
|
|
55
|
+
|
|
56
|
+
Use `createGameServerFunctions({ topology: "shared", runtimes })` for one persistent world. Shared topology defaults to singleton matchmaking. Capacity is not a throughput guarantee; verify the command and subscription read sets under crowd load. Subscribe to `getServerCapacity` for shared membership/status without volatile world revision reads.
|
|
57
|
+
|
|
58
|
+
Every gameplay command declares `scope(input)`: use `{ players: "actor", chunkKeys: [] }` for player-only work and `chunkKeysAround(position, 1)` for spatial work. The omitted-command default is actor-only with no chunks; an explicit `{}` requests the whole world and needs a justified bounded workload. Hosts use `helpers.runCommand` and its `beforePersist` callback to pair game writes with membership and revision checks in one transaction. Never copy that check/apply/persist loop into a game.
|
|
59
|
+
|
|
60
|
+
Every presence query is neighborhood-scoped. Pass `PresenceSession.viewerChunkKey` through `createConvexPresenceTransport`. Hosts with custom authentication, bot ownership, or spawn policy use `createWorldPresenceStore` from `@jgengine/convex/worldPresence`: resolve trusted identities and access first, then call `ensure`, `sync`, `revoke`, `active`, `nearby`, and `reap`. Store rows in `jgPoses`, never a game-owned presence table. Optional snapshot callbacks persist game-owned last-position fields at the store's cadence. Bot identity may differ from its authorized owner; preserve that mapping explicitly. Resident rows implement `PresenceResidentRow.actorExternalId` and `ownerActorId` exactly.
|
|
61
|
+
|
|
62
|
+
Persist only dirty profiles/chunks. Join/leave are roster writes; they do not dirty or rewrite the world. Reset a player with `helpers.resetPlayerProfile`, which runs the same initializer used by joins, and keep the shared world intact.
|
|
63
|
+
|
|
64
|
+
## Scheduled work
|
|
65
|
+
|
|
66
|
+
A cron never collects a whole table. Use presence-gated `forEachOnlinePlayer` with a bounded `batchSize`, an internal batch handler, and a continuation reference: each page schedules its handler and the next page as separate transactions. `OnlinePlayer.homeGameId` carries optional application identity from its pose row. Persist elapsed-time anchors with effects; use `accrueSince` to cap offline catch-up. `allServers` is only for explicitly global systems with a bounded server budget.
|
|
67
|
+
|
|
68
|
+
Pass runtimes to `jgengineCronSpecs` so no tick cron registers when no runtime declares `onTick`; set per-spec `intervalSeconds` deliberately. Retention sweeps and one-shot backfills are bounded, report `remaining`, and stop at convergence. Delete completed migration crons instead of leaving hourly table scans installed forever.
|
|
69
|
+
|
|
70
|
+
Join failures return typed `full`, `closed`, or `unauthorized` outcomes. Surface them through a retry gate; returning a rejection without visible UI is unfinished. Browser adapters leave sessions on `pagehide`.
|
|
71
|
+
|
|
72
|
+
For the explicit shared-builder composition, `jgengine create "World Name" --shape shared-world-builder` generates the connected Convex shell, runtime scope declarations, claim/accrual examples, chat, indexed presence, and an online batch pipeline. The generated README owns setup; replace the demonstration economy and author buildable world content in the editor.
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
- `ConvexBackend` (type): type ConvexBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = LiveGameBackend<TPresenceRow, TPresenceLocation, TGameId> & { leaderboard: ConvexLeaderboardReads; } — ⚠ undocumented
|
|
8
8
|
- `ConvexBackendOptions` (type): type ConvexBackendOptions<TRawPresenceRow extends { actorExternalId: string }, TPresenceRow> = { client: ConvexReactClient; gameId: string; userId: string; api?: ConvexGameApi; poseTuning?: PoseSyncTuning; presence?: { functions: ConvexPresenceFunctions; mapRow: (row: TRawPresenceRow) => TPresenceRo… — ⚠ undocumented
|
|
9
9
|
- `ConvexChatFunctions` (interface): interface ConvexChatFunctions — ⚠ undocumented
|
|
10
|
-
- `ConvexGameApi` (type): type ConvexGameApi = { runtime: { joinServer: FunctionReference< "mutation", "public", { gameId: string; serverId?: string; mode?: string; visibility?: "public" | "private"; joinCode?: string; externalId?: string; },
|
|
10
|
+
- `ConvexGameApi` (type): type ConvexGameApi = { runtime: { joinServer: FunctionReference< "mutation", "public", { gameId: string; serverId?: string; mode?: string; visibility?: "public" | "private"; joinCode?: string; externalId?: string; }, JoinServerOutcome >; leaveServer: FunctionReference<"mutation", "public", { serverI… — ⚠ undocumented
|
|
11
|
+
- `ConvexGameClient` (type): type ConvexGameClient = Pick<ConvexReactClient, "mutation" | "watchQuery"> — Structural client seam avoids requiring the engine and game to share a class instance type.
|
|
11
12
|
- `ConvexGameTransportConfig` (type): type ConvexGameTransportConfig = { gameId: string; userId?: string; } — ⚠ undocumented
|
|
12
13
|
- `ConvexLeaderboardReads` (type): type ConvexLeaderboardReads = ReturnType<typeof createConvexLeaderboardReads> — ⚠ undocumented
|
|
13
14
|
- `ConvexPresenceFunctions` (interface): interface ConvexPresenceFunctions — The Convex functions a **game** supplies for a world it owns: presence keyed by the game's own ids, including which residents exist while offline, which is game content and has no engine analogue. Not satisfied by `createPresenceFunctions` (`@jgengine/convex/server`) — that is the pose lane of a hosted `jgGameServers` server, backing `PresenceSync` rather than `PresenceTransport`.
|
|
@@ -15,21 +16,23 @@
|
|
|
15
16
|
- `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
17
|
- `DEFAULT_CONVEX_POSE_RULES` (const): const DEFAULT_CONVEX_POSE_RULES: PoseSyncRules — ⚠ undocumented
|
|
17
18
|
- `DEFAULT_MAX_SERVERS_PER_TICK` (const): const DEFAULT_MAX_SERVERS_PER_TICK: 32 — How many running servers one `tickActiveServers` transaction may hydrate, tick, and persist.
|
|
18
|
-
- `GameServerHelpers` (type): type GameServerHelpers = { loadSnapshot: ( ctx: JGMutationCtx, serverId: string, scope?: LoadSnapshotScope, ) => Promise<LoadedServerSnapshot | null>; applyCommand: (args: { gameId: string; loadedRevision: number;
|
|
19
|
+
- `GameServerHelpers` (type): type GameServerHelpers = { ensureServer: (ctx: JGMutationCtx, gameId: string) => Promise<ServerDoc>; loadSnapshot: ( ctx: JGMutationCtx, serverId: string, scope?: LoadSnapshotScope, ) => Promise<LoadedServerSnapshot | null>; applyCommand: (args: { gameId: string; loadedRevision: number; currentRevis… — 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.
|
|
19
20
|
- `HostedGameConfig` (interface): interface HostedGameConfig — One game the hosted path can serve, bound per `gameId`: its definition plus content lookup.
|
|
20
21
|
- `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.
|
|
21
22
|
- `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.
|
|
22
23
|
- `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.
|
|
23
24
|
- `JGMutationCtx` (type): type JGMutationCtx = GenericMutationCtx<JGDataModel> — Mutation ctx accepted by {@link loadServerSnapshot} / {@link persistServerSnapshot}.
|
|
25
|
+
- `JGQueryCtx` (type): type JGQueryCtx = GenericQueryCtx<JGDataModel> — Read-only context accepted by engine host queries.
|
|
24
26
|
- `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`.
|
|
25
|
-
- `JG_MAX_MEMBERS_PER_SERVER` (const): const JG_MAX_MEMBERS_PER_SERVER: 256 —
|
|
27
|
+
- `JG_MAX_MEMBERS_PER_SERVER` (const): const JG_MAX_MEMBERS_PER_SERVER: 256 — Room-topology ceiling on `slotsPerServer`; shared membership uses indexed rows instead.
|
|
26
28
|
- `JG_RUNTIME_TICK_MS` (const): const JG_RUNTIME_TICK_MS: 1000 — ⚠ undocumented
|
|
27
29
|
- `JgAuthMode` (type): type JgAuthMode = "anonymous" | "required" — ⚠ undocumented
|
|
28
|
-
- `JgCronSpec` (type): type JgCronSpec = { name: string; intervalSeconds: number; functionKey: "tickActiveServers" | "flushDirtyServers" | "reapIdlePresence"; /** Which factory's exports the function lives in, i.e. which `convex/*.ts` file to reach it through
|
|
30
|
+
- `JgCronSpec` (type): type JgCronSpec = { name: string; intervalSeconds: number; functionKey: "tickActiveServers" | "flushDirtyServers" | "reapIdlePresence" | "pruneChatMessages" | "pruneClientErrors" | "pruneRateLimits"; /** Which factory's exports the function lives in, i.e. which `convex/*.ts` file to reach it through… — ⚠ undocumented
|
|
29
31
|
- `LoadSnapshotScope` (type): type LoadSnapshotScope = CommandScope — How much of a server to hydrate. Both fields default to "everything", which is a document read per member plus one per chunk — the cost that makes a large shared world unaffordable per mutation. Narrow them when the caller knows what it will touch: a command that only moves the actor's own state needs `players: [actorUserId]`, and a spatial edit needs only the chunk keys it writes (`chunkKeysInRadius` from `@jgengine/core/runtime/worldChunks`).
|
|
30
32
|
- `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.
|
|
31
33
|
- `MAX_CHUNKS_PER_QUERY` (const): const MAX_CHUNKS_PER_QUERY: 64 — How many chunk rows one `getChunks` call may return.
|
|
32
34
|
- `MatchmakingMode` (type): type MatchmakingMode = "auto" | "singleton" — How auto-match behaves when no `serverId` is supplied.
|
|
35
|
+
- `OnlinePlayer` (type): type OnlinePlayer = { serverId: string; userId: string; homeGameId?: string } — One active player delivered to an online-system batch.
|
|
33
36
|
- `PresenceListRow` (type): type PresenceListRow = { userId: string; sessionId?: string; kind?: string; label?: string; position: { x: number; y: number; z: number }; rotationY: number; rotationPitch: number; lastSeenAt: number; } — One member's pose as `list` reports it.
|
|
34
37
|
- `PresenceSyncResult` (type): type PresenceSyncResult = { pose: { x: number; y: number; z: number; rotationY: number; rotationPitch: number }; lastSeenAt: number; displaced: boolean; } — What `sync` hands back: the pose the server holds after clamping, and whether this session had been displaced.
|
|
35
38
|
- `REVISION_CONFLICT_REASON` (const): const REVISION_CONFLICT_REASON: "Revision conflict" — ⚠ undocumented
|
|
@@ -37,25 +40,30 @@
|
|
|
37
40
|
- `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).
|
|
38
41
|
- `ServerDoc` (type): type ServerDoc = DocumentByName<JGDataModel, "jgGameServers"> — A `jgGameServers` row — the server handle the persistence helpers load from and write back to.
|
|
39
42
|
- `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.
|
|
43
|
+
- `createClientErrorFunctions` (function): function createClientErrorFunctions(): { reportClientError: RegisteredMutation<"public", { kind?: string | undefined; stack?: string | undefined; componentStack?: string | undefined; url?: string | undefined; pathname?: string | undefined; userAgent?: string | undefined; agentPrompt?: string | undef… — Authenticated, bounded client diagnostics and bounded retention sweeps.
|
|
40
44
|
- `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
|
|
41
|
-
- `createConvexChatSync` (function): function createConvexChatSync(client:
|
|
45
|
+
- `createConvexChatSync` (function): function createConvexChatSync(client: ConvexGameClient, api: ConvexGameApi, config: ConvexGameTransportConfig, serverId: string): ChatSync — ⚠ undocumented
|
|
42
46
|
- `createConvexChatTransport` (function): function createConvexChatTransport<TRawRow = ChatMessage>(functions: ConvexChatFunctions, options?: { mapRow?: (row: TRawRow) => ChatMessage; extraArgs?: Record<string, unknown>; }): ChatTransport — Wires a game's Convex chat functions into the engine's ChatTransport contract: one live query per subscribed channel (the channel's recent history, newest last) and one send mutation. mapRow converts backend rows into ChatMessage (defaults to structural passthrough); extraArgs is spread into both calls for games that scope chat by server or world.
|
|
43
|
-
- `createConvexFeedWrites` (function): function createConvexFeedWrites(client:
|
|
44
|
-
- `createConvexGameFeeds` (function): function createConvexGameFeeds(client:
|
|
45
|
-
- `createConvexGameTransport` (function): function createConvexGameTransport(client:
|
|
47
|
+
- `createConvexFeedWrites` (function): function createConvexFeedWrites(client: ConvexGameClient, api: ConvexGameApi, config: ConvexGameTransportConfig): { pushFeedEntry(args: { serverId: string; action: string; entry: unknown; }): Promise<void>; } — ⚠ undocumented
|
|
48
|
+
- `createConvexGameFeeds` (function): function createConvexGameFeeds(client: ConvexGameClient, api: ConvexGameApi, config: ConvexGameTransportConfig): GameRuntimeFeeds — ⚠ undocumented
|
|
49
|
+
- `createConvexGameTransport` (function): function createConvexGameTransport(client: ConvexGameClient, api: { runtime: Pick<ConvexGameApi["runtime"], "joinServer" | "leaveServer" | "runCommand"> }, config: ConvexGameTransportConfig): GameRuntimeTransport — ⚠ undocumented
|
|
46
50
|
- `createConvexLeaderboardReads` (function): function createConvexLeaderboardReads(api: ConvexGameApi, config: ConvexGameTransportConfig): { getTop(args: { stat: string; scope: LeaderboardScope; serverId?: string | undefined; limit?: number | undefined; }): { query: FunctionReference<"query", "public", { gameId: string; stat: string; scope: Le… — ⚠ undocumented
|
|
47
|
-
- `createConvexPresenceSync` (function): function createConvexPresenceSync(client:
|
|
48
|
-
- `createConvexPresenceTransport` (function): function createConvexPresenceTransport<TRawRow extends
|
|
51
|
+
- `createConvexPresenceSync` (function): function createConvexPresenceSync(client: ConvexGameClient, api: ConvexGameApi, config: ConvexGameTransportConfig, tuning?: PoseSyncTuning): PresenceSync — ⚠ undocumented
|
|
52
|
+
- `createConvexPresenceTransport` (function): function createConvexPresenceTransport<TRawRow extends PresenceResidentRow, TRow, TLocation, TGameId extends string = string>(functions: ConvexPresenceFunctions, mapRow: (row: TRawRow) => TRow): PresenceTransport<TRow, TLocation, TGameId> — Wires a game's Convex presence functions into the engine's PresenceTransport contract: one snapshot subscription (my location + online players), one residents subscription (dormant candidates, decoupled from pose writes), and one tick mutation for pose upload + keep-alive. Dormant rows are derived client-side by subtracting the snapshot's online actors, so pose ticks never re-execute the residents query. mapRow converts backend rows into the game's row type (e.g. branding positions into its coordinate space).
|
|
49
53
|
- `createConvexSaveBackend` (function): function createConvexSaveBackend(options: ConvexSaveBackendOptions): SaveBackend — A cloud {@link SaveBackend} backed by Convex — reads through a query and writes through mutations, so a {@link createSaveStore} configured with it saves to the server instead of `localStorage`. The only change from an offline game is swapping the backend; autosave, slots, and migration behave the same.
|
|
50
54
|
- `defaultConvexGameApi` (function): function defaultConvexGameApi(): ConvexGameApi — ⚠ undocumented
|
|
51
55
|
- `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.
|
|
56
|
+
- `forEachOnlinePlayer` (function): function forEachOnlinePlayer(ctx: JGMutationCtx, options: { batchSize?: number; handler: FunctionReference<"mutation", "internal", { players: OnlinePlayer[]; nowMs: number }>; continuation: FunctionReference<"mutation", "internal", { cursor?: string | null; nowMs?: number }>; cursor?: string | null;… — Scan one bounded presence page and schedule its handler and continuation in separate transactions.
|
|
52
57
|
- `isListablePublicly` (function): function isListablePublicly(visibility: SessionVisibility | undefined): boolean — True when a server's `visibility` should surface in public listings / browse results.
|
|
53
58
|
- `loadServerSnapshot` (function): function loadServerSnapshot(ctx: JGMutationCtx, server: ServerDoc, runtime: GameRuntime, scope?: LoadSnapshotScope): 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. Pass a {@link LoadSnapshotScope} to bound the read instead of loading the whole world.
|
|
54
59
|
- `persistServerSnapshot` (function): function persistServerSnapshot(ctx: JGMutationCtx, server: ServerDoc, snapshot: GameRuntimeSnapshot, save: SaveConfig): Promise<boolean> — 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.
|
|
55
60
|
- `randomConvexPlayerId` (function): function randomConvexPlayerId(): string — ⚠ undocumented
|
|
61
|
+
- `rateLimit` (function): function rateLimit(ctx: JGMutationCtx, args: { key: string; windowMs: number; max: number; nowMs?: number }): Promise<{ ok: boolean; retryAfterMs: number }> — Consume one request in a fixed rate window; concurrent callers serialize on the indexed key.
|
|
62
|
+
- `recentChatMessages` (function): function recentChatMessages(ctx: { db: Pick<JGQueryCtx["db"], "query"> }, args: { serverId: string; channelId?: string; limit?: number }): Promise<{ _id: Id<"jgChatMessages">; _creationTime: number; authorName?: string | undefined; serverId: string; userId: string; channelId: string; body: string; a… — Bounded recent chat for a resolved server; authorization belongs to the calling query.
|
|
56
63
|
- `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
|
|
57
|
-
- `
|
|
58
|
-
- `
|
|
64
|
+
- `sendChatMessage` (function): function sendChatMessage(ctx: JGMutationCtx, args: { serverId: string; userId: string; body: string; channelId?: string; authorName?: string; maxBodyLength?: number; minIntervalMs?: number }): Promise<ChatSendOutcome> — Write validated chat in a host transaction after the caller resolves its actor and world.
|
|
65
|
+
- `validateSlotsPerServer` (function): function validateSlotsPerServer(slotsPerServer: number, topology?: "rooms" | "shared"): { ok: true; } | { ok: false; reason: string; } — Whether a `slotsPerServer` value is a capacity a single hosted server row can actually hold.
|
|
66
|
+
- `watchConvexQuery` (function): function watchConvexQuery<TArgs extends DefaultFunctionArgs, TResult, TView>(client: ConvexGameClient, query: FunctionReference<"query", "public", TArgs, TResult>, args: TArgs, toView: (result: TResult) => TView, onChange: (view: TView) => void): () => void — ⚠ undocumented
|
|
59
67
|
|
|
60
68
|
## @jgengine/convex/convexChatTransport
|
|
61
69
|
|
|
@@ -65,7 +73,7 @@
|
|
|
65
73
|
## @jgengine/convex/convexPresenceTransport
|
|
66
74
|
|
|
67
75
|
- `ConvexPresenceFunctions` (interface): interface ConvexPresenceFunctions — The Convex functions a **game** supplies for a world it owns: presence keyed by the game's own ids, including which residents exist while offline, which is game content and has no engine analogue. Not satisfied by `createPresenceFunctions` (`@jgengine/convex/server`) — that is the pose lane of a hosted `jgGameServers` server, backing `PresenceSync` rather than `PresenceTransport`.
|
|
68
|
-
- `createConvexPresenceTransport` (function): function createConvexPresenceTransport<TRawRow extends
|
|
76
|
+
- `createConvexPresenceTransport` (function): function createConvexPresenceTransport<TRawRow extends PresenceResidentRow, TRow, TLocation, TGameId extends string = string>(functions: ConvexPresenceFunctions, mapRow: (row: TRawRow) => TRow): PresenceTransport<TRow, TLocation, TGameId> — Wires a game's Convex presence functions into the engine's PresenceTransport contract: one snapshot subscription (my location + online players), one residents subscription (dormant candidates, decoupled from pose writes), and one tick mutation for pose upload + keep-alive. Dormant rows are derived client-side by subtracting the snapshot's online actors, so pose ticks never re-execute the residents query. mapRow converts backend rows into the game's row type (e.g. branding positions into its coordinate space).
|
|
69
77
|
|
|
70
78
|
## @jgengine/convex/convexSaveBackend
|
|
71
79
|
|
|
@@ -83,16 +91,17 @@
|
|
|
83
91
|
|
|
84
92
|
## @jgengine/convex/createConvexGameTransport
|
|
85
93
|
|
|
86
|
-
- `ConvexGameApi` (type): type ConvexGameApi = { runtime: { joinServer: FunctionReference< "mutation", "public", { gameId: string; serverId?: string; mode?: string; visibility?: "public" | "private"; joinCode?: string; externalId?: string; },
|
|
94
|
+
- `ConvexGameApi` (type): type ConvexGameApi = { runtime: { joinServer: FunctionReference< "mutation", "public", { gameId: string; serverId?: string; mode?: string; visibility?: "public" | "private"; joinCode?: string; externalId?: string; }, JoinServerOutcome >; leaveServer: FunctionReference<"mutation", "public", { serverI… — ⚠ undocumented
|
|
95
|
+
- `ConvexGameClient` (type): type ConvexGameClient = Pick<ConvexReactClient, "mutation" | "watchQuery"> — Structural client seam avoids requiring the engine and game to share a class instance type.
|
|
87
96
|
- `ConvexGameTransportConfig` (type): type ConvexGameTransportConfig = { gameId: string; userId?: string; } — ⚠ undocumented
|
|
88
|
-
- `createConvexChatSync` (function): function createConvexChatSync(client:
|
|
89
|
-
- `createConvexFeedWrites` (function): function createConvexFeedWrites(client:
|
|
90
|
-
- `createConvexGameFeeds` (function): function createConvexGameFeeds(client:
|
|
91
|
-
- `createConvexGameTransport` (function): function createConvexGameTransport(client:
|
|
97
|
+
- `createConvexChatSync` (function): function createConvexChatSync(client: ConvexGameClient, api: ConvexGameApi, config: ConvexGameTransportConfig, serverId: string): ChatSync — ⚠ undocumented
|
|
98
|
+
- `createConvexFeedWrites` (function): function createConvexFeedWrites(client: ConvexGameClient, api: ConvexGameApi, config: ConvexGameTransportConfig): { pushFeedEntry(args: { serverId: string; action: string; entry: unknown; }): Promise<void>; } — ⚠ undocumented
|
|
99
|
+
- `createConvexGameFeeds` (function): function createConvexGameFeeds(client: ConvexGameClient, api: ConvexGameApi, config: ConvexGameTransportConfig): GameRuntimeFeeds — ⚠ undocumented
|
|
100
|
+
- `createConvexGameTransport` (function): function createConvexGameTransport(client: ConvexGameClient, api: { runtime: Pick<ConvexGameApi["runtime"], "joinServer" | "leaveServer" | "runCommand"> }, config: ConvexGameTransportConfig): GameRuntimeTransport — ⚠ undocumented
|
|
92
101
|
- `createConvexLeaderboardReads` (function): function createConvexLeaderboardReads(api: ConvexGameApi, config: ConvexGameTransportConfig): { getTop(args: { stat: string; scope: LeaderboardScope; serverId?: string | undefined; limit?: number | undefined; }): { query: FunctionReference<"query", "public", { gameId: string; stat: string; scope: Le… — ⚠ undocumented
|
|
93
|
-
- `createConvexPresenceSync` (function): function createConvexPresenceSync(client:
|
|
102
|
+
- `createConvexPresenceSync` (function): function createConvexPresenceSync(client: ConvexGameClient, api: ConvexGameApi, config: ConvexGameTransportConfig, tuning?: PoseSyncTuning): PresenceSync — ⚠ undocumented
|
|
94
103
|
- `defaultConvexGameApi` (function): function defaultConvexGameApi(): ConvexGameApi — ⚠ undocumented
|
|
95
|
-
- `watchConvexQuery` (function): function watchConvexQuery<TArgs extends DefaultFunctionArgs, TResult, TView>(client:
|
|
104
|
+
- `watchConvexQuery` (function): function watchConvexQuery<TArgs extends DefaultFunctionArgs, TResult, TView>(client: ConvexGameClient, query: FunctionReference<"query", "public", TArgs, TResult>, args: TArgs, toView: (result: TResult) => TView, onChange: (view: TView) => void): () => void — ⚠ undocumented
|
|
96
105
|
|
|
97
106
|
## @jgengine/convex/hostedServer
|
|
98
107
|
|
|
@@ -114,27 +123,46 @@
|
|
|
114
123
|
|
|
115
124
|
- `DEFAULT_CONVEX_POSE_RULES` (const): const DEFAULT_CONVEX_POSE_RULES: PoseSyncRules — ⚠ undocumented
|
|
116
125
|
- `DEFAULT_MAX_SERVERS_PER_TICK` (const): const DEFAULT_MAX_SERVERS_PER_TICK: 32 — How many running servers one `tickActiveServers` transaction may hydrate, tick, and persist.
|
|
117
|
-
- `GameServerHelpers` (type): type GameServerHelpers = { loadSnapshot: ( ctx: JGMutationCtx, serverId: string, scope?: LoadSnapshotScope, ) => Promise<LoadedServerSnapshot | null>; applyCommand: (args: { gameId: string; loadedRevision: number;
|
|
126
|
+
- `GameServerHelpers` (type): type GameServerHelpers = { ensureServer: (ctx: JGMutationCtx, gameId: string) => Promise<ServerDoc>; loadSnapshot: ( ctx: JGMutationCtx, serverId: string, scope?: LoadSnapshotScope, ) => Promise<LoadedServerSnapshot | null>; applyCommand: (args: { gameId: string; loadedRevision: number; currentRevis… — 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.
|
|
118
127
|
- `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.
|
|
119
128
|
- `JGMutationCtx` (type): type JGMutationCtx = GenericMutationCtx<JGDataModel> — Mutation ctx accepted by {@link loadServerSnapshot} / {@link persistServerSnapshot}.
|
|
120
|
-
- `
|
|
129
|
+
- `JGQueryCtx` (type): type JGQueryCtx = GenericQueryCtx<JGDataModel> — Read-only context accepted by engine host queries.
|
|
130
|
+
- `JG_MAX_MEMBERS_PER_SERVER` (const): const JG_MAX_MEMBERS_PER_SERVER: 256 — Room-topology ceiling on `slotsPerServer`; shared membership uses indexed rows instead.
|
|
121
131
|
- `JG_RUNTIME_TICK_MS` (const): const JG_RUNTIME_TICK_MS: 1000 — ⚠ undocumented
|
|
122
132
|
- `JgAuthMode` (type): type JgAuthMode = "anonymous" | "required" — ⚠ undocumented
|
|
123
|
-
- `JgCronSpec` (type): type JgCronSpec = { name: string; intervalSeconds: number; functionKey: "tickActiveServers" | "flushDirtyServers" | "reapIdlePresence"; /** Which factory's exports the function lives in, i.e. which `convex/*.ts` file to reach it through
|
|
133
|
+
- `JgCronSpec` (type): type JgCronSpec = { name: string; intervalSeconds: number; functionKey: "tickActiveServers" | "flushDirtyServers" | "reapIdlePresence" | "pruneChatMessages" | "pruneClientErrors" | "pruneRateLimits"; /** Which factory's exports the function lives in, i.e. which `convex/*.ts` file to reach it through… — ⚠ undocumented
|
|
124
134
|
- `LoadSnapshotScope` (type): type LoadSnapshotScope = CommandScope — How much of a server to hydrate. Both fields default to "everything", which is a document read per member plus one per chunk — the cost that makes a large shared world unaffordable per mutation. Narrow them when the caller knows what it will touch: a command that only moves the actor's own state needs `players: [actorUserId]`, and a spatial edit needs only the chunk keys it writes (`chunkKeysInRadius` from `@jgengine/core/runtime/worldChunks`).
|
|
125
135
|
- `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.
|
|
126
136
|
- `MAX_CHUNKS_PER_QUERY` (const): const MAX_CHUNKS_PER_QUERY: 64 — How many chunk rows one `getChunks` call may return.
|
|
127
137
|
- `MatchmakingMode` (type): type MatchmakingMode = "auto" | "singleton" — How auto-match behaves when no `serverId` is supplied.
|
|
138
|
+
- `OnlinePlayer` (type): type OnlinePlayer = { serverId: string; userId: string; homeGameId?: string } — One active player delivered to an online-system batch.
|
|
128
139
|
- `PresenceListRow` (type): type PresenceListRow = { userId: string; sessionId?: string; kind?: string; label?: string; position: { x: number; y: number; z: number }; rotationY: number; rotationPitch: number; lastSeenAt: number; } — One member's pose as `list` reports it.
|
|
129
140
|
- `PresenceSyncResult` (type): type PresenceSyncResult = { pose: { x: number; y: number; z: number; rotationY: number; rotationPitch: number }; lastSeenAt: number; displaced: boolean; } — What `sync` hands back: the pose the server holds after clamping, and whether this session had been displaced.
|
|
130
141
|
- `RunCommandArgs` (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; externalId?: string; /** * How much of the server to hydrate before applying the command. Overrides whatever the * {@link CommandDef} declares through its own `scope`; omit both to hydrate the whole world. */ scope?: LoadSnap… — Arguments of the `runCommand` mutation, and of the equivalent {@link GameServerHelpers.runCommand} helper.
|
|
131
142
|
- `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).
|
|
132
143
|
- `ServerDoc` (type): type ServerDoc = DocumentByName<JGDataModel, "jgGameServers"> — A `jgGameServers` row — the server handle the persistence helpers load from and write back to.
|
|
133
144
|
- `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.
|
|
145
|
+
- `createClientErrorFunctions` (function): function createClientErrorFunctions(): { reportClientError: RegisteredMutation<"public", { kind?: string | undefined; stack?: string | undefined; componentStack?: string | undefined; url?: string | undefined; pathname?: string | undefined; userAgent?: string | undefined; agentPrompt?: string | undef… — Authenticated, bounded client diagnostics and bounded retention sweeps.
|
|
146
|
+
- `forEachOnlinePlayer` (function): function forEachOnlinePlayer(ctx: JGMutationCtx, options: { batchSize?: number; handler: FunctionReference<"mutation", "internal", { players: OnlinePlayer[]; nowMs: number }>; continuation: FunctionReference<"mutation", "internal", { cursor?: string | null; nowMs?: number }>; cursor?: string | null;… — Scan one bounded presence page and schedule its handler and continuation in separate transactions.
|
|
134
147
|
- `isListablePublicly` (function): function isListablePublicly(visibility: SessionVisibility | undefined): boolean — True when a server's `visibility` should surface in public listings / browse results.
|
|
135
148
|
- `loadServerSnapshot` (function): function loadServerSnapshot(ctx: JGMutationCtx, server: ServerDoc, runtime: GameRuntime, scope?: LoadSnapshotScope): 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. Pass a {@link LoadSnapshotScope} to bound the read instead of loading the whole world.
|
|
136
149
|
- `persistServerSnapshot` (function): function persistServerSnapshot(ctx: JGMutationCtx, server: ServerDoc, snapshot: GameRuntimeSnapshot, save: SaveConfig): Promise<boolean> — 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.
|
|
137
|
-
- `
|
|
150
|
+
- `rateLimit` (function): function rateLimit(ctx: JGMutationCtx, args: { key: string; windowMs: number; max: number; nowMs?: number }): Promise<{ ok: boolean; retryAfterMs: number }> — Consume one request in a fixed rate window; concurrent callers serialize on the indexed key.
|
|
151
|
+
- `recentChatMessages` (function): function recentChatMessages(ctx: { db: Pick<JGQueryCtx["db"], "query"> }, args: { serverId: string; channelId?: string; limit?: number }): Promise<{ _id: Id<"jgChatMessages">; _creationTime: number; authorName?: string | undefined; serverId: string; userId: string; channelId: string; body: string; a… — Bounded recent chat for a resolved server; authorization belongs to the calling query.
|
|
152
|
+
- `sendChatMessage` (function): function sendChatMessage(ctx: JGMutationCtx, args: { serverId: string; userId: string; body: string; channelId?: string; authorName?: string; maxBodyLength?: number; minIntervalMs?: number }): Promise<ChatSendOutcome> — Write validated chat in a host transaction after the caller resolves its actor and world.
|
|
153
|
+
- `validateSlotsPerServer` (function): function validateSlotsPerServer(slotsPerServer: number, topology?: "rooms" | "shared"): { ok: true; } | { ok: false; reason: string; } — Whether a `slotsPerServer` value is a capacity a single hosted server row can actually hold.
|
|
154
|
+
|
|
155
|
+
## @jgengine/convex/territory
|
|
156
|
+
|
|
157
|
+
- `loadTerritoryState` (function): function loadTerritoryState(ctx: JGQueryCtx | JGMutationCtx, args: { serverId: string; gameId: string; userId: string; chunkKeys: readonly string[] }): Promise<GameRuntimeSnapshot> — Trusted host hydration for claim/bootstrap work; the caller checks access before invoking it.
|
|
158
|
+
- `persistTerritoryState` (function): function persistTerritoryState(ctx: JGMutationCtx, snapshot: GameRuntimeSnapshot, nowMs = Date.now()): Promise<void> — Persist dirty territory chunks and profiles in the caller's mutation, without a shared-server write.
|
|
159
|
+
|
|
160
|
+
## @jgengine/convex/worldPresence
|
|
161
|
+
|
|
162
|
+
- `PresenceSnapshotWriter` (type): type PresenceSnapshotWriter = (presence: WorldPresenceRecord, nowMs: number) => Promise<void> — Persist game-owned last-position state when the host store requests a checkpoint.
|
|
163
|
+
- `WorldPresenceOptions` (interface): interface WorldPresenceOptions — World chunk size and host-owned pose, checkpoint and retention policies.
|
|
164
|
+
- `WorldPresenceRecord` (interface): interface WorldPresenceRecord extends PresenceResidentRow — Normalized actor identity and pose read from an indexed jgPoses row.
|
|
165
|
+
- `createWorldPresenceStore` (function): function createWorldPresenceStore(options: WorldPresenceOptions = {}): { active: (ctx: ReadContext, actorExternalId: string, serverId?: string | undefined) => Promise<WorldPresenceRecord | null>; ensure(ctx: JGMutationCtx, args: { serverId: string; actorExternalId: string; homeGameId: string; kind: … — Indexed presence storage for hosts that own authentication, bot ownership and spawn policy.
|
|
138
166
|
|
|
139
167
|
## @jgengine/core/multiplayer
|
|
140
168
|
|
|
@@ -168,7 +196,7 @@
|
|
|
168
196
|
- `browseSessions` (function): function browseSessions(listings: readonly SessionListing[], filter: MatchFilter = {}, options: BrowseOptions = {}): SessionListing[] — ⚠ undocumented
|
|
169
197
|
- `createFeedWriteGate` (function): function createFeedWriteGate(allowedActions: readonly string[] = []): FeedWriteGate — ⚠ undocumented
|
|
170
198
|
- `createLocalVoiceTransport` (function): function createLocalVoiceTransport(options?: { userId?: string }): { transport: VoiceTransport; participants(channelId: string): readonly VoiceParticipant[]; } — ⚠ undocumented
|
|
171
|
-
- `createPoseSyncGate` (function): function createPoseSyncGate(tuning: PoseSyncTuning): PoseSyncGate — ⚠ undocumented
|
|
199
|
+
- `createPoseSyncGate` (function): function createPoseSyncGate(tuning: PoseSyncTuning = DEFAULT_POSE_SYNC_TUNING): PoseSyncGate — ⚠ undocumented
|
|
172
200
|
- `createPushToTalk` (function): function createPushToTalk(config?: { mode?: PushToTalkMode; onChange?: (transmitting: boolean) => void; }): PushToTalk — ⚠ undocumented
|
|
173
201
|
- `findByJoinCode` (function): function findByJoinCode(listings: readonly SessionListing[], code: string): SessionListing | null — ⚠ undocumented
|
|
174
202
|
- `fromRuntimeObjectRow` (function): function fromRuntimeObjectRow(row: RuntimeObjectRow): SceneObject — Inverse of {@link toRuntimeObjectRow}: rebuild the live placed object a host persisted.
|
|
@@ -237,10 +265,11 @@
|
|
|
237
265
|
|
|
238
266
|
## @jgengine/core/multiplayer/poseSyncGate
|
|
239
267
|
|
|
268
|
+
- `DEFAULT_POSE_SYNC_TUNING` (const): const DEFAULT_POSE_SYNC_TUNING: PoseSyncTuning — Default ten-hertz client pose gate with a five-second heartbeat.
|
|
240
269
|
- `PlayerPose` (interface): interface PlayerPose — ⚠ undocumented
|
|
241
270
|
- `PoseSyncGate` (interface): interface PoseSyncGate — ⚠ undocumented
|
|
242
271
|
- `PoseSyncTuning` (interface): interface PoseSyncTuning — ⚠ undocumented
|
|
243
|
-
- `createPoseSyncGate` (function): function createPoseSyncGate(tuning: PoseSyncTuning): PoseSyncGate — ⚠ undocumented
|
|
272
|
+
- `createPoseSyncGate` (function): function createPoseSyncGate(tuning: PoseSyncTuning = DEFAULT_POSE_SYNC_TUNING): PoseSyncGate — ⚠ undocumented
|
|
244
273
|
|
|
245
274
|
## @jgengine/core/multiplayer/presenceContract
|
|
246
275
|
|
|
@@ -251,6 +280,7 @@
|
|
|
251
280
|
- `PresenceFeeds` (interface): interface PresenceFeeds<TRow, TLocation> — ⚠ undocumented
|
|
252
281
|
- `PresencePose` (interface): interface PresencePose — ⚠ undocumented
|
|
253
282
|
- `PresencePosition` (interface): interface PresencePosition — ⚠ undocumented
|
|
283
|
+
- `PresenceResidentRow` (interface): interface PresenceResidentRow — Resident identity used to suppress offline actors while their owner is online.
|
|
254
284
|
- `PresenceSession` (interface): interface PresenceSession<TGameId extends string = string> — ⚠ undocumented
|
|
255
285
|
- `PresenceTransport` (interface): interface PresenceTransport<TRow, TLocation, TGameId extends string = string> — Backend seam for multiplayer presence. Feeds are reactive data and change identity whenever any player's pose updates; actions MUST be identity-stable for the lifetime of a mounted session so join/leave lifecycle effects can depend on them without re-running per pose tick. The use* members are called as React hooks by consumers, so a mounted transport must never change identity — remount the subtree to switch backends.
|
|
256
286
|
- `createLocalPresenceTransport` (function): function createLocalPresenceTransport<TRow, TLocation, TGameId extends string = string>(): { transport: PresenceTransport<TRow, TLocation, TGameId>; actions: PresenceActions<TGameId>; } — ⚠ undocumented
|
|
@@ -292,7 +322,7 @@
|
|
|
292
322
|
- `DEFAULT_HEARTBEAT_INTERVAL_MS` (const): const DEFAULT_HEARTBEAT_INTERVAL_MS: 30000 — Default ping/pong interval; a socket that misses one round-trip is terminated.
|
|
293
323
|
- `DEFAULT_MAX_CONNECTIONS` (const): const DEFAULT_MAX_CONNECTIONS: 10000 — Default max concurrent sockets this server accepts before rejecting new ones.
|
|
294
324
|
- `DEFAULT_MAX_PAYLOAD_BYTES` (const): const DEFAULT_MAX_PAYLOAD_BYTES: 1048576 — Default per-message payload cap (bytes) — `ws` closes the socket with 1009 past this.
|
|
295
|
-
- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<
|
|
325
|
+
- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; role?: SnapshotViewer["role"]; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<Session… — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence.
|
|
296
326
|
- `GameHostOptions` (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; } — Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes.
|
|
297
327
|
- `GameSocketIoServer` (type): type GameSocketIoServer = { rewind: (args: { serverId: string; atMs: number }) => RewoundPosition[]; close: () => void; } — ⚠ undocumented
|
|
298
328
|
- `GameSocketIoServerOptions` (type): type GameSocketIoServerOptions = HostRouterOptions & { io: SocketIoLikeServer } — ⚠ undocumented
|
|
@@ -317,6 +347,7 @@
|
|
|
317
347
|
- `createGameWsServer` (function): function createGameWsServer(options: GameWsServerOptions): GameWsServer — ⚠ undocumented
|
|
318
348
|
- `createWorldGameServer` (function): function createWorldGameServer(options: WorldGameServerOptions): WorldGameServer — Build a {@link WorldGameServer} — one process hosting authoritative GameContext worlds over ws, ready for two-client play once {@link WorldGameServer.start} runs.
|
|
319
349
|
- `filePersistence` (function): function filePersistence(dir: string, now: () => number = Date.now): HostPersistence — ⚠ undocumented
|
|
350
|
+
- `fileWorldStore` (function): function fileWorldStore(path: string): HostedWorldStore — JSON-file implementation of the asynchronous hosted-world persistence seam.
|
|
320
351
|
- `installShutdownHook` (function): function installShutdownHook(shutdown: () => Promise<void> | void, options: InstallShutdownHookOptions = {}): ShutdownHook — Wires `SIGINT`/`SIGTERM` (or a custom signal list) to a clean-shutdown callback — e.g. `() => worldServer.close()` or `() => Promise.all([wsServer.close(), host.stop()])`. Bounded by `timeoutMs` so a stuck flush can't hang the process forever; idempotent — a second signal delivered mid-shutdown reuses the same in-flight run instead of flushing twice. Returns a {@link ShutdownHook} whose `remove()` uninstalls the listeners, for tests and embedders that want their own handling.
|
|
321
352
|
- `memoryPersistence` (function): function memoryPersistence(now?: () => number): HostPersistence — Creates an in-memory `HostPersistence` implementation, useful for tests and ephemeral hosts.
|
|
322
353
|
- `memoryWorldPersistence` (function): function memoryWorldPersistence(): WorldPersistence — Default {@link WorldPersistence}: an isolated in-memory {@link HostedWorldStore} per `gameId`/`serverId`, lost on process exit.
|
|
@@ -341,9 +372,13 @@
|
|
|
341
372
|
- `EditorManifest` (interface): interface EditorManifest — What the standalone editor loads on boot: the on-disk scene document (if any) and every model in the asset folder.
|
|
342
373
|
- `EditorManifestAsset` (interface): interface EditorManifestAsset — One placeable model the standalone editor lists — a stable id and a URL the dev server serves it from.
|
|
343
374
|
|
|
375
|
+
## @jgengine/node/fileWorldStore
|
|
376
|
+
|
|
377
|
+
- `fileWorldStore` (function): function fileWorldStore(path: string): HostedWorldStore — JSON-file implementation of the asynchronous hosted-world persistence seam.
|
|
378
|
+
|
|
344
379
|
## @jgengine/node/host
|
|
345
380
|
|
|
346
|
-
- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<
|
|
381
|
+
- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; role?: SnapshotViewer["role"]; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<Session… — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence.
|
|
347
382
|
- `GameHostOptions` (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; } — Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes.
|
|
348
383
|
- `HostChangeEvent` (type): type HostChangeEvent = { type: "server"; serverId: string; } | { type: "player"; serverId: string; userId: string; } | { type: "feed"; serverId: string; action: string; } — A change notification emitted by a `GameHost` for a server, player, or feed mutation.
|
|
349
384
|
- `createGameHost` (function): function createGameHost(options: GameHostOptions): GameHost — Creates a `GameHost` that runs game servers over the given persistence and runtimes.
|
|
@@ -410,6 +445,7 @@
|
|
|
410
445
|
- `SqlQueryable` (type): type SqlQueryable = { query: (text: string, params?: unknown[]) => Promise<SqlQueryResult>; } — ⚠ undocumented
|
|
411
446
|
- `ensureSchema` (function): function ensureSchema(pool: SqlQueryable): Promise<void> — ⚠ undocumented
|
|
412
447
|
- `sqlPersistence` (function): function sqlPersistence(pool: SqlPool, now: () => number = Date.now): HostPersistence — ⚠ undocumented
|
|
448
|
+
- `sqlWorldStore` (function): function sqlWorldStore(pool: SqlQueryable, worldId: string): HostedWorldStore — Create an asynchronous hosted-world store backed by one JSONB row.
|
|
413
449
|
|
|
414
450
|
## @jgengine/sql/sqlPersistence
|
|
415
451
|
|
|
@@ -420,6 +456,10 @@
|
|
|
420
456
|
- `ensureSchema` (function): function ensureSchema(pool: SqlQueryable): Promise<void> — ⚠ undocumented
|
|
421
457
|
- `sqlPersistence` (function): function sqlPersistence(pool: SqlPool, now: () => number = Date.now): HostPersistence — ⚠ undocumented
|
|
422
458
|
|
|
459
|
+
## @jgengine/sql/sqlWorldStore
|
|
460
|
+
|
|
461
|
+
- `sqlWorldStore` (function): function sqlWorldStore(pool: SqlQueryable, worldId: string): HostedWorldStore — Create an asynchronous hosted-world store backed by one JSONB row.
|
|
462
|
+
|
|
423
463
|
## @jgengine/ws
|
|
424
464
|
|
|
425
465
|
- `CommandAuthorize` (type): type CommandAuthorize = (args: { userId: string; op: HostCommandOp; serverId?: string; command?: string; }) => boolean | Promise<boolean> — Per-command authorization hook: return `false` to reject. Defaults to allow-all when omitted.
|
|
@@ -433,8 +473,9 @@
|
|
|
433
473
|
- `CommandRateLimit` (type): type CommandRateLimit = { count: number; perMs: number } — A sliding-window rate limit: at most `count` calls per `perMs` window.
|
|
434
474
|
- `CommandRateLimiter` (type): type CommandRateLimiter = { allow: (connection: object, op: HostCommandOp, atMs: number) => boolean; } — A composable per-connection/per-op sliding-window rate limiter.
|
|
435
475
|
- `DEFAULT_COMMAND_LIMITS` (const): const DEFAULT_COMMAND_LIMITS: CommandLimits — Recommended per-op limits a host can opt into via `limits: DEFAULT_COMMAND_LIMITS`. Rate limiting is off unless `limits` is set.
|
|
476
|
+
- `DEFAULT_GRACE_MS` (const): const DEFAULT_GRACE_MS: 15000 — Default reconnect grace period for hosted socket sessions.
|
|
436
477
|
- `DEFAULT_POSE_RULES` (const): const DEFAULT_POSE_RULES: PoseSyncRules — ⚠ undocumented
|
|
437
|
-
- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<
|
|
478
|
+
- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; role?: SnapshotViewer["role"]; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<Session… — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence.
|
|
438
479
|
- `GameHostOptions` (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; } — Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes.
|
|
439
480
|
- `HostChangeEvent` (type): type HostChangeEvent = | { type: "server"; serverId: string } | { type: "player"; serverId: string; userId: string } | { type: "feed"; serverId: string; action: string } — A change notification emitted by a `GameHost` for a server, player, or feed mutation.
|
|
440
481
|
- `HostCommandOp` (type): type HostCommandOp = "pose" | "runCommand" | "join" | "browse" | "voice" — A host-side op the command middleware pipeline can gate: pose sync, `runCommand`, join/joinByCode, browse, or voice join/leave/publish.
|
|
@@ -479,13 +520,13 @@
|
|
|
479
520
|
- `WorldGameHost` (interface): interface WorldGameHost extends GameHost — A {@link GameHost} whose worlds run on `HostedWorldSession`s; `tick` advances them and re-broadcasts on change.
|
|
480
521
|
- `WorldGameHostOptions` (interface): interface WorldGameHostOptions — Config for {@link createWorldGameHost}: how to resolve a hosted world's authoritative session per server.
|
|
481
522
|
- `WsAppearance` (type): type WsAppearance = Record<string, string | number | boolean> — Client-set cosmetic/state tags carried alongside a pose (skin, mount, emote, ...). Primitive values only.
|
|
482
|
-
- `WsBackend` (type): type WsBackend = GameBackend & { pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise<void>; browse: (args: { gameId: string; filter?: MatchFilter; limit?: number }) => Promise<SessionListing[]>; joinByCode: (args: {
|
|
523
|
+
- `WsBackend` (type): type WsBackend = GameBackend & { rtt: { sampleMs: number; smoothedMs: number }; pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise<void>; browse: (args: { gameId: string; filter?: MatchFilter; limit?: number }) => Promise<SessionListing[]>; joinByCode: (args: { ga… — ⚠ undocumented
|
|
483
524
|
- `WsBackendOptions` (type): type WsBackendOptions = { url?: string; pipe?: TransportPipeFactory; userId: string; token?: string; webSocketFactory?: (url: string) => WebSocket; reconnectDelayMs?: number; maxReconnectDelayMs?: number; rpcTimeoutMs?: number; poseTuning?: PoseSyncTuning; now?: () => number; setTimeoutFn?: typeof s… — ⚠ undocumented
|
|
484
525
|
- `WsBrowseResult` (type): type WsBrowseResult = SessionListing[] — ⚠ undocumented
|
|
485
526
|
- `WsChannel` (type): type WsChannel = "server" | "player" | "feed" | "presence" | "chat" | "voice" — ⚠ undocumented
|
|
486
527
|
- `WsChatMessage` (type): type WsChatMessage = { id: string; channelId: string; fromUserId: string; body: string; at: number; } — ⚠ undocumented
|
|
487
528
|
- `WsChatSync` (type): type WsChatSync = { subscribe: ( serverId: string, channelId: string, onChange: (messages: WsChatMessage[]) => void, ) => () => void; send: (serverId: string, channelId: string, body: string) => Promise<ChatSendOutcome>; } — ⚠ undocumented
|
|
488
|
-
- `WsClientMessage` (type): type WsClientMessage = | { v: 1; t: "
|
|
529
|
+
- `WsClientMessage` (type): type WsClientMessage = | { v: 1; t: "ping"; id: number; at: number } | { v: 1; t: "hello"; id: number; userId: string; token?: string } | { v: 1; t: "join"; id: number; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; role?: "player" | "spectator"; } | { v: 1; t: "jo… — ⚠ undocumented
|
|
489
530
|
- `WsDecodeFailure` (type): type WsDecodeFailure = { reason: string; id?: number; } — ⚠ undocumented
|
|
490
531
|
- `WsJoinByCodeResult` (type): type WsJoinByCodeResult = JoinServerResult | null — ⚠ undocumented
|
|
491
532
|
- `WsJoinResult` (type): type WsJoinResult = JoinServerResult — ⚠ undocumented
|
|
@@ -493,7 +534,7 @@
|
|
|
493
534
|
- `WsPresenceRow` (type): type WsPresenceRow = PresencePoseRow & { appearance?: WsAppearance } — ⚠ undocumented
|
|
494
535
|
- `WsPresenceSync` (type): type WsPresenceSync = { subscribe: (serverId: string, onChange: (rows: WsPresenceRow[]) => void) => () => void; /** `pose.appearance`, when provided, is forwarded to the host as-is and surfaces on every subscriber's presence row for that user. */ syncPose: (serverId: string, pose: WsPose) => void; } — ⚠ undocumented
|
|
495
536
|
- `WsRunCommandResult` (type): type WsRunCommandResult = TransportRunCommandResult — ⚠ undocumented
|
|
496
|
-
- `WsServerMessage` (type): type WsServerMessage = | { v: 1; t: "reply"; id: number; ok: true; result?: unknown } | { v: 1; t: "reply"; id: number; ok: false; reason: string } | WsUpdateMessage — ⚠ undocumented
|
|
537
|
+
- `WsServerMessage` (type): type WsServerMessage = | { v: 1; t: "pong"; id: number; at: number; serverAt: number } | { v: 1; t: "reply"; id: number; ok: true; result?: unknown } | { v: 1; t: "reply"; id: number; ok: false; reason: string } | WsUpdateMessage — ⚠ undocumented
|
|
497
538
|
- `WsUpdateMessage` (type): type WsUpdateMessage = | { v: 1; t: "update"; channel: "server"; serverId: string; data: GameRuntimeServerView | null } | { v: 1; t: "update"; channel: "player"; serverId: string; data: GameRuntimePlayerView | null } | { v: 1; t: "update"; channel: "feed"; serverId: string; action: string; data: unk… — ⚠ undocumented
|
|
498
539
|
- `WsVoiceParticipant` (type): type WsVoiceParticipant = { userId: string; streamId?: string; } — ⚠ undocumented
|
|
499
540
|
- `WsVoiceSync` (type): type WsVoiceSync = { subscribe: ( serverId: string, channelId: string, onChange: (participants: WsVoiceParticipant[]) => void, ) => () => void; join: (serverId: string, channelId: string, streamId?: string) => Promise<void>; leave: (serverId: string, channelId: string) => Promise<void>; publish: (se… — ⚠ undocumented
|
|
@@ -541,7 +582,7 @@
|
|
|
541
582
|
|
|
542
583
|
## @jgengine/ws/createWsBackend
|
|
543
584
|
|
|
544
|
-
- `WsBackend` (type): type WsBackend = GameBackend & { pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise<void>; browse: (args: { gameId: string; filter?: MatchFilter; limit?: number }) => Promise<SessionListing[]>; joinByCode: (args: {
|
|
585
|
+
- `WsBackend` (type): type WsBackend = GameBackend & { rtt: { sampleMs: number; smoothedMs: number }; pushFeedEntry: (args: { serverId: string; action: string; entry: unknown }) => Promise<void>; browse: (args: { gameId: string; filter?: MatchFilter; limit?: number }) => Promise<SessionListing[]>; joinByCode: (args: { ga… — ⚠ undocumented
|
|
545
586
|
- `WsBackendOptions` (type): type WsBackendOptions = { url?: string; pipe?: TransportPipeFactory; userId: string; token?: string; webSocketFactory?: (url: string) => WebSocket; reconnectDelayMs?: number; maxReconnectDelayMs?: number; rpcTimeoutMs?: number; poseTuning?: PoseSyncTuning; now?: () => number; setTimeoutFn?: typeof s… — ⚠ undocumented
|
|
546
587
|
- `WsChatSync` (type): type WsChatSync = { subscribe: ( serverId: string, channelId: string, onChange: (messages: WsChatMessage[]) => void, ) => () => void; send: (serverId: string, channelId: string, body: string) => Promise<ChatSendOutcome>; } — ⚠ undocumented
|
|
547
588
|
- `WsPresenceSync` (type): type WsPresenceSync = { subscribe: (serverId: string, onChange: (rows: WsPresenceRow[]) => void) => () => void; /** `pose.appearance`, when provided, is forwarded to the host as-is and surfaces on every subscriber's presence row for that user. */ syncPose: (serverId: string, pose: WsPose) => void; } — ⚠ undocumented
|
|
@@ -550,7 +591,7 @@
|
|
|
550
591
|
|
|
551
592
|
## @jgengine/ws/host
|
|
552
593
|
|
|
553
|
-
- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<
|
|
594
|
+
- `GameHost` (type): type GameHost = { joinServer: (args: { userId: string; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; role?: SnapshotViewer["role"]; }) => Promise<JoinServerResult>; browseServers: (args: { gameId: string; filter?: MatchFilter; limit?: number; }) => Promise<Session… — A transport-agnostic authoritative game server host that manages sessions, ticking, and persistence.
|
|
554
595
|
- `GameHostOptions` (type): type GameHostOptions = { runtimes?: GameRuntime[]; persistence: HostPersistence; tickMs?: number; slotsPerServer?: number; now?: () => number; createServerId?: () => string; allowedFeedActions?: readonly string[]; } — Configuration for {@link createGameHost}, including persistence, tick rate, and game runtimes.
|
|
555
596
|
- `HostChangeEvent` (type): type HostChangeEvent = | { type: "server"; serverId: string } | { type: "player"; serverId: string; userId: string } | { type: "feed"; serverId: string; action: string } — A change notification emitted by a `GameHost` for a server, player, or feed mutation.
|
|
556
597
|
- `OP_LEDGER_LIMIT` (const): const OP_LEDGER_LIMIT: 64 — Max recently-applied `runCommand` op IDs retained per (serverId, userId), oldest evicted first.
|
|
@@ -559,6 +600,7 @@
|
|
|
559
600
|
|
|
560
601
|
## @jgengine/ws/hostRouter
|
|
561
602
|
|
|
603
|
+
- `DEFAULT_GRACE_MS` (const): const DEFAULT_GRACE_MS: 15000 — Default reconnect grace period for hosted socket sessions.
|
|
562
604
|
- `DEFAULT_POSE_RULES` (const): const DEFAULT_POSE_RULES: PoseSyncRules — ⚠ undocumented
|
|
563
605
|
- `HostRouter` (type): type HostRouter = { connect: (transport: HostRouterTransport) => HostRouterConnection; rewind: (args: { serverId: string; atMs: number }) => RewoundPosition[]; close: () => void; } — ⚠ undocumented
|
|
564
606
|
- `HostRouterAuthenticate` (type): type HostRouterAuthenticate = (args: { userId: string; token?: string; }) => Promise<string | null> | string | null — ⚠ undocumented
|
|
@@ -612,14 +654,14 @@
|
|
|
612
654
|
- `WsBrowseResult` (type): type WsBrowseResult = SessionListing[] — ⚠ undocumented
|
|
613
655
|
- `WsChannel` (type): type WsChannel = "server" | "player" | "feed" | "presence" | "chat" | "voice" — ⚠ undocumented
|
|
614
656
|
- `WsChatMessage` (type): type WsChatMessage = { id: string; channelId: string; fromUserId: string; body: string; at: number; } — ⚠ undocumented
|
|
615
|
-
- `WsClientMessage` (type): type WsClientMessage = | { v: 1; t: "
|
|
657
|
+
- `WsClientMessage` (type): type WsClientMessage = | { v: 1; t: "ping"; id: number; at: number } | { v: 1; t: "hello"; id: number; userId: string; token?: string } | { v: 1; t: "join"; id: number; gameId: string; serverId?: string; attributes?: SessionAttributes; code?: string; role?: "player" | "spectator"; } | { v: 1; t: "jo… — ⚠ undocumented
|
|
616
658
|
- `WsDecodeFailure` (type): type WsDecodeFailure = { reason: string; id?: number; } — ⚠ undocumented
|
|
617
659
|
- `WsJoinByCodeResult` (type): type WsJoinByCodeResult = JoinServerResult | null — ⚠ undocumented
|
|
618
660
|
- `WsJoinResult` (type): type WsJoinResult = JoinServerResult — ⚠ undocumented
|
|
619
661
|
- `WsPose` (type): type WsPose = PlayerPose & { appearance?: WsAppearance } — ⚠ undocumented
|
|
620
662
|
- `WsPresenceRow` (type): type WsPresenceRow = PresencePoseRow & { appearance?: WsAppearance } — ⚠ undocumented
|
|
621
663
|
- `WsRunCommandResult` (type): type WsRunCommandResult = TransportRunCommandResult — ⚠ undocumented
|
|
622
|
-
- `WsServerMessage` (type): type WsServerMessage = | { v: 1; t: "reply"; id: number; ok: true; result?: unknown } | { v: 1; t: "reply"; id: number; ok: false; reason: string } | WsUpdateMessage — ⚠ undocumented
|
|
664
|
+
- `WsServerMessage` (type): type WsServerMessage = | { v: 1; t: "pong"; id: number; at: number; serverAt: number } | { v: 1; t: "reply"; id: number; ok: true; result?: unknown } | { v: 1; t: "reply"; id: number; ok: false; reason: string } | WsUpdateMessage — ⚠ undocumented
|
|
623
665
|
- `WsUpdateMessage` (type): type WsUpdateMessage = | { v: 1; t: "update"; channel: "server"; serverId: string; data: GameRuntimeServerView | null } | { v: 1; t: "update"; channel: "player"; serverId: string; data: GameRuntimePlayerView | null } | { v: 1; t: "update"; channel: "feed"; serverId: string; action: string; data: unk… — ⚠ undocumented
|
|
624
666
|
- `WsVoiceParticipant` (type): type WsVoiceParticipant = { userId: string; streamId?: string; } — ⚠ undocumented
|
|
625
667
|
|
|
@@ -4,6 +4,10 @@
|
|
|
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
|
+
## client-error-reporting — Accept authenticated bounded diagnostics with cooldown and retention sweeps.
|
|
8
|
+
|
|
9
|
+
- `createClientErrorFunctions` (function) · `import { createClientErrorFunctions } from "@jgengine/convex"`
|
|
10
|
+
|
|
7
11
|
## convex-load-server-snapshot — read a hosted server's runtime snapshot inside a host-written Convex mutation
|
|
8
12
|
|
|
9
13
|
- `loadServerSnapshot` (function) · `import { loadServerSnapshot } from "@jgengine/convex"`
|
|
@@ -16,6 +20,14 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
16
20
|
|
|
17
21
|
- `createConvexSaveBackend` (function) · `import { createConvexSaveBackend } from "@jgengine/convex"`
|
|
18
22
|
|
|
23
|
+
## host-chat-history — Read bounded channel history inside an authorized host query.
|
|
24
|
+
|
|
25
|
+
- `recentChatMessages` (function) · `import { recentChatMessages } from "@jgengine/convex"`
|
|
26
|
+
|
|
27
|
+
## host-chat-send — Validate, rate-limit and persist chat inside an authorized host mutation.
|
|
28
|
+
|
|
29
|
+
- `sendChatMessage` (function) · `import { sendChatMessage } from "@jgengine/convex"`
|
|
30
|
+
|
|
19
31
|
## host-join-code-gate — membership-or-code gate for private hosted servers
|
|
20
32
|
|
|
21
33
|
- `canJoinPrivateServer` (function) · `import { canJoinPrivateServer } from "@jgengine/convex"`
|
|
@@ -24,6 +36,19 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
24
36
|
|
|
25
37
|
- `isListablePublicly` (function) · `import { isListablePublicly } from "@jgengine/convex"`
|
|
26
38
|
|
|
39
|
+
## host-rate-limit — Enforce indexed per-key request windows inside authoritative mutations.
|
|
40
|
+
|
|
41
|
+
- `rateLimit` (function) · `import { rateLimit } from "@jgengine/convex"`
|
|
42
|
+
|
|
43
|
+
## hosted-world-persistence — Persist hosted authoritative world snapshots in a local file.
|
|
44
|
+
|
|
45
|
+
- `fileWorldStore` (function) · `import { fileWorldStore } from "@jgengine/node"`
|
|
46
|
+
- `sqlWorldStore` (function) · `import { sqlWorldStore } from "@jgengine/sql"`
|
|
47
|
+
|
|
48
|
+
## online-player-batches — Schedule bounded active-player or home-game work in separate transactions.
|
|
49
|
+
|
|
50
|
+
- `forEachOnlinePlayer` (function) · `import { forEachOnlinePlayer } from "@jgengine/convex"`
|
|
51
|
+
|
|
27
52
|
## placed-object-rehydrate — rebuild live placed objects, with their state and slot contents, from persisted snapshot rows
|
|
28
53
|
|
|
29
54
|
- `fromRuntimeObjectRow` (function) · `import { fromRuntimeObjectRow } from "@jgengine/core/multiplayer"`
|
|
@@ -31,3 +56,15 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
31
56
|
## 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
57
|
|
|
33
58
|
- `toRuntimeObjectRow` (function) · `import { toRuntimeObjectRow } from "@jgengine/core/multiplayer"`
|
|
59
|
+
|
|
60
|
+
## territory-host-hydration — Load an actor profile and bounded claim chunks for a host transaction.
|
|
61
|
+
|
|
62
|
+
- `loadTerritoryState` (function) · `import { loadTerritoryState } from "@jgengine/convex/territory"`
|
|
63
|
+
|
|
64
|
+
## territory-host-persistence — Persist dirty ownership chunks and profiles without rewriting the shared server.
|
|
65
|
+
|
|
66
|
+
- `persistTerritoryState` (function) · `import { persistTerritoryState } from "@jgengine/convex/territory"`
|
|
67
|
+
|
|
68
|
+
## world-presence-store — indexed neighborhood presence with pose throttling and physical expiry Call only after resolving the actor and their world access; browser arguments are not trusted identities.
|
|
69
|
+
|
|
70
|
+
- `createWorldPresenceStore` (function) · `import { createWorldPresenceStore } from "@jgengine/convex/worldPresence"`
|