@jgengine/convex 0.49.1 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -26,6 +26,164 @@ At publish, rename this heading to the new version and mirror the entries into
26
26
  `packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
27
27
  -->
28
28
 
29
+ ## 0.50.0
30
+
31
+ ### Migrate
32
+
33
+ - **Bump lockstep SDK packages to `^0.50.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.47.0`; `@jgengine/github` is `0.37.0`.
34
+ - declare the tour's steps in createOnboardingSystem({ steps: [{ id, text, detail, anchor, placement, done: { next: true } }] }) and mount <OnboardingCallout />; a coach-mark condition becomes done: { when } or { event }
35
+
36
+ ### Added
37
+
38
+ - An action's bindings take `locked: true`: players can't rebind it, saved overrides for it are ignored, and controls menus show it read-only. `isActionLocked(codes)` reads the flag.
39
+ - `persist.storage` in `defineGame` also takes any `SaveBackend`, such as `createConvexSaveBackend`, so a solo game's whole-world save can go to the cloud.
40
+ - Brightway Park design brief: core loop, session goal, long loop, second 0, the first 10 minutes and a content budget
41
+ - `OnboardingCallout` (`@jgengine/core/onboarding/onboardingCallout.view`): The onboarding's UI callout, for steps about the interface rather than the world: a step with an `anchor` (a CSS selector) gets a card beside that element with a spotlight around it, and a step that finishes on Next with no anchor gets a centred card. Also `OnboardingCalloutCard`, `OnboardingCalloutCardProps`, `OnboardingCalloutProps`, `OnboardingCalloutTheme`.
42
+ - `OnboardingCallout` (`@jgengine/react`): The onboarding's UI callout, for steps about the interface rather than the world: a step with an `anchor` (a CSS selector) gets a card beside that element with a spotlight around it, and a step that finishes on Next with no anchor gets a centred card. Also `OnboardingCalloutCard`, `OnboardingCalloutCardProps`, `OnboardingCalloutProps`, `OnboardingCalloutTheme`.
43
+ - Fernwild's Survivor's path takes a new survivor from the first tree to the Broodmother in 23 steps, with a star on the map at each target.
44
+ - Fernwild taming works from a new game with no cheats: punch a dodo out and feed it berries, club a phiomia and keep it under with narcotics. A club hit now lands its full torpor.
45
+ - Fernwild has 21 explorer notes, a Dossier tab of species met, tamed and notes read, and an always-on map baked from the island's ground with notes, obelisks, caves and tames marked.
46
+ - Fernwild danger rises with distance (wild levels capped at 5 by the beach, the full 30 from 1250 m), and the start beach holds only grazers.
47
+ - createSpoilage in @jgengine/core/inventory/spoilage; Fernwild meat spoils into spoiled meat and the timers survive a save.
48
+ - an onboarding step can set inArea: false so a teaching-area pointer sends the player past the area.
49
+ - Fernwild's PLAN.md design brief compares it with ARK beat by beat, and its from-zero proof plays 19 minutes from a new game; Fernwild is off the depth known-failing list.
50
+ - `createSpoilage` (`@jgengine/core/inventory/spoilage`): Perishables in an inventory, as ARK keeps them: each stack runs one timer, and every time it runs out one unit spoils and the timer starts again, so a big stack lasts longer than a small one. Also `SpoilRule`, `Spoilage`, `SpoiledUnits`, `SpoilingStack`.
51
+ - every game's PLAN.md carries a design brief (core, session and long loops, second 0, the first 10 minutes beat by beat, content budget, systems, onboarding); check games-design fails a game without a complete one
52
+ - games/scripts/depth-known-failing.json lists the 18 games that fail the brief, from-zero and depth rules today, with why; the list only shrinks
53
+ - every game has a from-zero proof (proofs/flows/from-zero.flow.ts) that boots a new game with no prepared state or save and plays its first 10 minutes through real input; it fails a start beyond the brief's second 0, nothing taught or unlocked by minute 2, or new things stopping at the brief's pace
54
+ - check games-depth plays that proof and measures systems touched, time to first unlock, unlocks per 10 minutes, content against the budget, the longest cycle and minutes of content; it blocks landing, and today's games are on the shrink-only known-failing list
55
+ - a game declares its new-game start as data (defineGame newGame: cash, kit, unlocked items, prebuilt pieces, systems that start running). The engine builds every new game from that declaration only, refuses start items handed out anywhere else, and exposes it on the definition for checks to read
56
+ - Fernwild declares its start: empty-handed, knowing only the Stone Pick and Torch engrams
57
+ - `NewGameConfig` (`@jgengine/core/game/newGame`): A game's new-game declaration, passed as `defineGame({ newGame })`: the {@link NewGameState} plus the hooks that apply its game-owned parts. Also `NewGameKit`, `NewGamePiece`, `NewGameState`.
58
+ - onboarding as data (createOnboardingSystem). A game declares its steps; each has text, can point at an entity, the nearest thing of a kind or a spot (on-screen pin or edge arrow, minimap marker, waypoint), completes from a game event, a condition, a measured amount or a Next press, can lock commands until done, and grants items, xp, currency or unlocks. Three styles: a separate tutorial level, a teaching start area, or hints blended into stage one. Skippable, saved per player, shown in the quest tracker and objective banner (OnboardingHud, OnboardingBanner, OnboardingPointer, onboardingLock)
59
+ - ctx.game.commands.gate, one place to refuse commands before they run
60
+ - Fernwild's first day: punch a tree for thatch, gather stone, craft a Stone Pick, tame a dodo, each pointing at the nearest tree, stones or dodo with an arrow and a minimap star; engram learning waits for the pick
61
+ - `Onboarding` (`@jgengine/core/onboarding/onboarding`): One player's run through an {@link OnboardingConfig}. Also `OnboardingConfig`, `OnboardingDone`, `OnboardingPoint`, `OnboardingSnapshot`, `OnboardingStep`, `OnboardingStepView`, `OnboardingStyle`, `OnboardingTarget`.
62
+ - `OnboardingBanner` (`@jgengine/core/onboarding/onboarding.view`): The step-done banner ("Gather stone" / "Next: Craft a Stone Pick") through `ObjectiveBannerHost`, near the top of the screen. Also `OnboardingHud`, `OnboardingHudProps`, `useOnboarding`.
63
+ - `OnboardingPointer` (`@jgengine/core/onboarding/OnboardingPointer.draw`): The onboarding's on-screen pointer, mounted in a game's `WorldOverlay`: a pin over the current step's target while it is in view, an edge arrow pointing the way with the distance while it is not (`WaypointMarkers` over the onboarding's waypoint).
64
+ - `ONBOARDING_MARK` (`@jgengine/core/onboarding/onboardingSystem`): The id of the waypoint and minimap marker the onboarding owns. Also `OnboardingRuntime`, `OnboardingSystemOptions`, `createOnboardingSystem`, `onboardingLock`, `onboardingOf`.
65
+ - repeat targeted tests under concurrent load with complete failure evidence
66
+ - A from-zero proof plays a new Scrap Signal game for ten minutes through real input: Bolt's quests one at a time, the crash chest's pistol, the whelps, the convoy's bruisers and red crate, Old Tusk and his whelps, the road to Breakwater Shelf and Captain Rusk. Scrap Signal is off the depth known-failing list for both from-zero and depth.
67
+ - Camp enemies carry four times their listed health and shield (one dial); bosses and Overdrive crews keep their own numbers; cleared camps refill after four minutes away.
68
+ - Ammo crates that restock, ammo packs sized to a camp fight, and kills that always drop ammo while you are nearly dry.
69
+ - Named targets behind side quests and quest-reward uniques.
70
+ - browser proof flows can move the mouse, look and aim with real pointer input again (flow.moveMouse, flow.look, flow.aimAt), restoring the pointer-lock look and crosshair aim lost when the JSON drive steps were replaced
71
+ - `FLOW_AIM_TOLERANCE_PX` (`@jgengine/core/play/flow`): How close {@link BrowserFlow.aimAt} must bring the target to the crosshair, in CSS pixels.
72
+ - withField(row, key, value) in @jgengine/core/data/withField sets an optional field, or leaves it out when value is undefined, so a cleared field is never stored as key: undefined.
73
+ - `definedOnly` (`@jgengine/core/data/definedOnly`): `value` without its undefined fields, so `{ ...defaults, ...definedOnly(overrides) }` never erases a default with an explicit `undefined`, and a record built from optional inputs can be written where an optional field must be absent rather than `undefined`. Also `DefinedOnly`.
74
+ - `withField` (`@jgengine/core/data/withField`): A copy of `row` with the optional field `key` set to `value`, or left out when `value` is `undefined`. Also `OptionalKey`.
75
+
76
+ ### Changed
77
+
78
+ - AGENTS.md settles two rules: an agent's own scratch files go in the session scratchpad (.scratch/ is only tool output), and a wait under 10 minutes runs in the foreground with a timeout while only dev servers and game launches run in the background
79
+ - Brightway Park keeps monthly books: eight-day months from 1 March, a report of every line (gate, rides, shops, awards, borrowing, wages, upkeep, restocking, repairs, research, marketing, interest, building), loan interest at each month's close, two red month-ends in a row close the park, and three marketing campaigns that bring more guests while they run
80
+ - Brightway Park has a design brief in its PLAN.md and a from-zero proof that plays the first 10 minutes of a new game
81
+ - Brightway Park's probe reports taught, unlocks, each content kind's seen and total, and one reading per system
82
+ - Brightway Park's lab opens with four short topics and the first-scenario hints now teach the lab, so the first unlock lands inside two minutes
83
+ - A new Brightway ride or shop that is still shut opens its window, where Connect it waits
84
+ - Brightway Park leaves the depth known-failing list
85
+ - Brightway Park restarts its calendar for every new park or scenario: 09:00 on 1 March, day one, one midnight a day, even when a scenario is started or replayed mid-way through another park
86
+ - Brightway Park's midnight uses the engine clock's cancel handle, so re-arming it after a reset cancels the old timer
87
+ - Brightway Park starts on an empty lot: a gate plaza, the gate shut, no guests until the player builds a path and a ride and opens the gate
88
+ - Guests trickle in and ramp with rating and appeal; the rating follows the park over the day instead of in seconds
89
+ - The open-park and staff proofs play from the empty lot by mouse and keyboard
90
+ - Brightway Park scenarios run for months: a new game starts Brightway Beginnings on the empty lot (120 guests and 400 stars by the end of May, Year 1), Star Attraction runs the whole first season and Full House two; each is won the moment its targets hold, unlocks the next, and the HUD shows days left; time runs at up to 8x
91
+ - Brightway Park's first scenario teaches itself: a card says the next step and rings the button that does it, with an arrow over the spot on the lot (lay a path from the gate, build a ride, connect it, open the gate), and at the first breakdown it says to hire a mechanic from Staff; a new proof plays from the empty lot to a natural breakdown and the hired mechanic fixing it
92
+ - Brightway Park's README states the award range and what the balance bot really measures, and jgengine.json describes the loan the park actually has
93
+ - Brightway's balance bot plays a new game the way a player starts one: the first scenario, the gate opened, a mechanic hired, and it prints how the scenario stands
94
+ - Brightway Park rides stay down until a mechanic fixes them: mechanics are hireable from the start and go straight to broken rides, the ride window shows reliability for the next run, the lab takes five times longer (34 topics over about thirteen park months, Dodgems now a topic), and litter bins catch litter
95
+ - Brightway Park's hint cards now run on the engine's onboarding system, keep up while the game is paused, and have a Hide hints button
96
+ - Brightway Park scenarios run for months: a new game starts Brightway Beginnings on the empty lot (120 guests and 400 stars by the end of May, Year 1), Star Attraction runs the whole first season and Full House two; each is won the moment its targets hold, unlocks the next, and the HUD shows days left; time runs at up to 8x
97
+ - drive's command line and help move to scripts/driveArgs.ts and its lockstep recorder to scripts/driveLockstep.ts, so drive-dev.ts is the run alone (1074 to 684 lines); the move drops four type casts
98
+ - New create-game-jg skill: a new game starts by agreeing its direction with the user (references, how faithful, breadth, look, scale, players, platforms, what done means), mapping every feature with one jgengine has call, and writing PLAN.md before jgengine create
99
+ - bun run drive prints the result, its warnings and failures by default; --verbose adds the progress, reuse and timing lines, and the GPU driver's shader precision warnings move there too
100
+ - drive --step: try flow calls on a live game page, with closest-match failures A --step runs one browser-flow call on a page of the game kept running (paused between commands) in the warm browser, so trying a click, wait or check answers in seconds instead of a full proof run. A failed click names the closest controls and whether one is covered; a failed probe check names missing probe keys and the closest real ones; see() names the closest page text; a misspelt f.call names the closest calls. The page work moves out of drive-dev.ts into drivePage.ts, shared by --flow --browser and --step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101
+ - Landing fails fast: lane:close runs bun run preflight (install, gen, type-checks and gates) first and stops at the first failure, before any test runs
102
+ - bun run preflight is that cheap check on its own; preflight --ship is gone
103
+ - verify:fast starts type-checks and gates before tests and stops starting tests after a failure; --cheap runs only the type-checks and gates
104
+ - bun run gen runs independent generators side by side, and gen barrels reads core's surface once instead of five times
105
+ - lane:close starts the queue for its PR in the background and names the one command that prints the outcome
106
+ - Flaky tests fixed: folder deletes retry on Windows (Bun ignores rm's maxRetries), ws grace-window tests use hand-fired timers, the CDP deadline test waits for the server's message, and scrap-signal's audio test restores fetch instead of deleting it
107
+ - game-making skills start from the PLAN.md design brief: milestones are the next 10 minutes of play proven by the from-zero flow, the greenfield contract folds into the brief, and the first tutorial step no longer has to be doable from starting inventory
108
+ - create-game-jg description back under the 15-word skill limit
109
+ - Changelog: locked bindings and SaveBackend storage
110
+ - Faster frames in every game: half-resolution ambient occlusion, a matrix pass that skips what stood still, and a cheaper per-frame scene walk
111
+ - Ambient occlusion and its denoise run on a quarter of the pixels; at 1080p that cut its GPU time from about 2-3.8 ms to 0.6-1.6 ms in every game that uses it, with no visible change in a side-by-side shot.
112
+ - The scene's matrix pass rebuilds only nodes whose position, rotation, scale, parent or world matrix changed; the shader warmup and other traverseVisible callers use a stack walk (visitVisible), 1.4 ms -> 0.12 ms on fernwild.
113
+ - Dev telemetry reads the GPU name once instead of every second; bun run perf names the engine or game function behind each hot three.js function, and a compile hitch names the draw that waited.
114
+ - bun run perf takes several games in one browser and ends with a table per game; a clean-tree run is cached per commit so --compare HEAD measures only the after side; the process exits once results are written.
115
+ - shoot, verify:fast, preflight and lane:close print only results, warnings, failures and summaries by default; --verbose prints progress, reuse, timing and every pass
116
+ - release --wait: wait on npm serving every package, not on the run's 5-minute check 0.49.0's core reached npm 7m12s after upload, 24 s after publish.yml's 300 s check gave up; release --wait reported a failure and a second release followed (~50 min). release --wait now polls the registry for every lockstep package (bounded at 20 minutes) instead of one npm view of core, and a run that failed only at its registry check keeps waiting on npm and then runs the same artifact check locally. publish.yml's check budget goes from 300 s to 900 s because 300 s was measured too short. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117
+ - The Rustflat Front's crews wake only after Old Tusk is down; Old Tusk has 2000 health, Captain Rusk 1800 plus a 400 shield.
118
+ - Bruiser Bash now ends at the convoy's red crate, and Old Tusk's quest starts by thinning his whelps.
119
+ - Games: the six strict compiler flags (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `noImplicitOverride`, `noPropertyAccessFromIndexSignature`, `noImplicitReturns`, `noFallthroughCasesInSwitch`) are now on for every in-repo game through `games/tsconfig.base.json`, and in llmville's own tsconfig. Each game's `check-types` (run by CI) now enforces them, so new strict errors cannot come back.
120
+ - ember-command, drift-foundry and brightway-park now pass the strictest type settings. Their catalogs name their ids, so a mistyped unit, building or research id is a type error, and save loading reads old saves without casts.
121
+ - New `@jgengine/core/data/untyped` (`isRecord`, `isFiniteNumber`, `recordOf`) narrows parsed JSON and old saves without casts. New `slotText`/`slotNumber` in `@jgengine/core/save/saveSlots` read a save slot's chips.
122
+ - `ModelConfig.dims`, `ModelBodySource.dims`, `DrivableVehicleOptions.modifiers`, `BuildingSurfaceMaterial.roughness`/`metalness` and the kit objective's `steps`/`deadline` accept an explicit `undefined`.
123
+ - scrap-signal now type-checks clean under the strict flags (unchecked index access, exact optional properties, no index-signature dot access): save reads go through named locals, optional fields are passed only when set, and catalog lookups say when a key can be missing.
124
+ - The shared legacy-save reader reads the envelope by key.
125
+ - harbor-heat now type-checks clean under the strict flags (unchecked index access, exact optional properties, no index-signature dot access).
126
+ - harbor-heat reads scene meta through the engine's typed meta readers, so a wrong value in the scene file fails naming the marker instead of silently falling back to a default.
127
+ - harbor-heat's save reader uses the engine's shared record check instead of its own copy, and drops casts it no longer needs.
128
+ - deepward now type-checks clean under the strict flags (unchecked index access, exact optional properties, no index-signature dot access).
129
+ - deepward's save reader checks stored fields through named raw shapes and the engine's shared record check, instead of reading an open record by dot.
130
+ - deepward's probe now reports its own named readings, so its proof flows are checked against the real probe keys instead of any string.
131
+ - odd-orbit now type-checks clean under the strict flags (unchecked index access, exact optional properties, no index-signature dot access).
132
+ - odd-orbit's save checker names every stored field it reads, so a misspelled field is a type error instead of a silent `undefined`.
133
+ - A career rung with no pay value now fails loudly instead of paying `NaN`.
134
+ - New engine helper `definedFields` (`@jgengine/core/data/definedFields`): passes maybe-missing values on as optional fields, leaving out the undefined ones, so code stays correct under exact optional property types.
135
+ - llmville: first pass toward the strict type flags (517 errors down to 303). Builder session, agent asset requests, dev endpoints, generated assets and rack commands read untyped input through the engine's record check instead of dot access; map decor and procedural props pass optional transforms through `definedFields`.
136
+ - llmville: the game package, Convex backend, dev-server bridges and scripts now pass the strict compiler flags (strict-flag errors 303 → 173; the rest are in `src/`).
137
+ - llmville: command and save readers use bracket reads, optional fields that can be unset say so with `| undefined`, and Convex calls only pass optional fields that are set.
138
+ - llmville: the whole game now passes the strict compiler flags (strict-flag errors 173 → 0).
139
+ - llmville: component props and view-model fields that can be unset say so with `| undefined`; list reads check for a missing entry; fixed lists (avatars, builder materials, room templates) are typed as never empty.
140
+ - llmville: the error-report payload only sends fields that are set, and the toast theme no longer needs a cast.
141
+ - Nine games (apex-nights, beacon-bastion, briarcrown, hold-the-line, orbit-watch, outpost-zero, red-frontier, saltwick, the-sunken-crown) now pass the strictest type settings, ahead of those settings turning on for every game.
142
+ - Games read a placed object's authored settings through `metaNumber`, `metaText` and `requireMetaText` (`@jgengine/core/editor/markerMeta`), which fail loudly on a wrong value instead of casting it.
143
+ - `CompassProps.center`, the kit vitals objective's `id`/`goal`, and kit buttons' `cue` accept an explicit `undefined`.
144
+ - Every engine package (assets, convex, editor, github, jgengine, navbake, node, rapier, react, sql) now type-checks with the strict flags core, shell and ws already had. 400 errors fixed with guards and real types, no new casts.
145
+ - definedOnly in @jgengine/core/data/definedOnly is public, and its result keeps always-defined fields required; only fields that may be undefined become optional.
146
+ - Scripts, the UI registry, the dev, web and desktop apps and the host examples now type-check with the same strict flags as the engine packages. About 1000 errors fixed with guards and real types, no new casts.
147
+ - optional fields on registry slot props (ActionBarSlot, ItemGridSlot, BuffChip), EditorAppProps, the ws host's listOpenServers args and a few core option types accept an explicit undefined.
148
+
149
+ ### Fixed
150
+
151
+ - Beacon Bastion's pacing test counts only gold the keep could still have spent: the bounty from the last kill, paid after the final chance to buy, no longer fails a keep that spent everything it could. A keep that stops buying still fails the test.
152
+ - Brightway Park's status card no longer overlaps the goal card at 1600x900; it keeps a steady height as guest thoughts come and go, with mood and cleanliness side by side
153
+ - Brightway's goal hint no longer says to lay a path when the only ride is broken down
154
+ - Fernwild's Harvest Stones prompt no longer shows when you aim at a tree.
155
+ - Fernwild's dehydration test steps whole seconds so it finishes inside CI's test timeout.
156
+ - Fernwild no longer freezes for about a second when you press Play: the island's 33k trees and rocks are placed while the title loads (worst click 940 ms -> 510 ms, menu shows ~0.5 s later)
157
+ - drive --playtest judges a softlock only after the player first acts; a quiet title or idle start no longer counts
158
+ - the capability boot probe presses Play (the game's capture.play) and gives a system 300 frames from there, not from the menu
159
+ - closing a lane again with -m adds the new lines to its change note instead of replacing it
160
+ - the automatic worktree cleanup now removes a lane that never committed once it has sat untouched for an hour with no changed or new files, and its dry run lists only what a real run would take
161
+ - waypoint pins and edge arrows near a screen edge keep their label on screen
162
+ - world-anchored floating text no longer re-renders every frame while nothing floats
163
+ - the worktree cleanup leaves a tree a running process holds whole and reports it in use, instead of half-deleting it and stopping, so the trees after it still go
164
+ - bun run worktree new runs the cleanup from the new tree's own code, not the main checkout's possibly older copy
165
+ - Rockets burst on the first thing they hit instead of flying through it.
166
+ - Chest and crate drops land two metres out on the opener's side, so picking one up no longer puts the camera inside the chest.
167
+ - on Windows a new worktree shares the main checkout's installed packages instead of installing 713 of its own, so it is ready in about 10s instead of 30s to 2 minutes
168
+ - a worktree whose bun.lock moved on gets its own install instead of writing into the shared one
169
+ - the 17 aim and look checks removed with the old drive steps are restored, and the 12 removed drive-step assertion checks now live as browser flow tests (failing expect stops the run, strict value matching, a page error, a timed-out wait)
170
+ - A new game made with `--no-editor` now gets the starter sun angle; it was written in the editor's shape, which `sky()` ignored.
171
+ - the casts and non-null assertions the type-escape baseline was raised to accept (boot sequence, asset prefetch, frame breakdown, perf panel, dev asset plugin, perf scripts) are replaced by real checks.
172
+ - Convex inserts and query results no longer write field: undefined for optional columns.
173
+ - the workspace catalog JSON scanner throws a clear error on truncated JSON instead of reading past the end.
174
+ - script readers of changelog headings, PNG headers and screenshot pixels no longer read past the end of their input.
175
+
176
+ ### Removed
177
+
178
+ - createCoachMarkSequence, CoachMark and CoachMarkHost. UI tours are onboarding steps now: give a step an anchor (CSS selector) and placement, or finish it on Next, and OnboardingCallout shows it as a card with a spotlight, Next and Skip, saved with the game
179
+ - `@jgengine/core/ui` no longer exports `CoachMarkPlacement`, `CoachMarkSequence`, `CoachMarkSequenceOptions`, `CoachMarkSnapshot`, `CoachMarkStep`, `CoachMarkView`, `createCoachMarkSequence`.
180
+ - `@jgengine/core/ui/coachMarks` no longer exports `CoachMarkPlacement`, `CoachMarkSequence`, `CoachMarkSequenceOptions`, `CoachMarkSnapshot`, `CoachMarkStep`, `CoachMarkView`, `createCoachMarkSequence`.
181
+ - `@jgengine/core/ui/coachMarks.view` no longer exports `CoachMark`, `CoachMarkHost`, `CoachMarkHostProps`, `CoachMarkProps`, `CoachMarkTheme`, `useCoachMarks`.
182
+ - `@jgengine/react` no longer exports `CoachMark`, `CoachMarkHost`, `CoachMarkHostProps`, `CoachMarkProps`, `CoachMarkTheme`, `useCoachMarks`.
183
+ - the jgengine plan command; the skill replaces it, and jgengine has stays as the lookup
184
+ - the progression-curve seam from games-framework; a game's own level numbers, unlock order and tutorial beats stay in the game at any length (games/AGENTS.md mechanism signal)
185
+ - system checkboxes from the game READMEs' progression sections; progression is written as what the player gets over hours
186
+
29
187
  ## 0.49.1
30
188
 
31
189
  ### Migrate
@@ -13,8 +13,8 @@ export type ConvexBackendOptions<TRawPresenceRow extends {
13
13
  client: ConvexReactClient;
14
14
  gameId: string;
15
15
  userId: string;
16
- api?: ConvexGameApi;
17
- poseTuning?: PoseSyncTuning;
16
+ api?: ConvexGameApi | undefined;
17
+ poseTuning?: PoseSyncTuning | undefined;
18
18
  presence?: {
19
19
  functions: ConvexPresenceFunctions;
20
20
  mapRow: (row: TRawPresenceRow) => TPresenceRow;
@@ -15,23 +15,23 @@ export type ConvexGameApi = {
15
15
  runtime: {
16
16
  joinServer: FunctionReference<"mutation", "public", {
17
17
  gameId: string;
18
- serverId?: string;
18
+ serverId?: string | undefined;
19
19
  mode?: string;
20
20
  visibility?: "public" | "private";
21
21
  joinCode?: string;
22
- externalId?: string;
22
+ externalId?: string | undefined;
23
23
  sessionId?: string;
24
24
  }, JoinServerOutcome>;
25
25
  leaveServer: FunctionReference<"mutation", "public", {
26
26
  serverId: string;
27
- externalId?: string;
27
+ externalId?: string | undefined;
28
28
  sessionId?: string;
29
29
  }, null>;
30
30
  runCommand: FunctionReference<"mutation", "public", {
31
31
  serverId: string;
32
32
  command: string;
33
33
  input: unknown;
34
- externalId?: string;
34
+ externalId?: string | undefined;
35
35
  }, {
36
36
  ok: true;
37
37
  } | {
@@ -42,7 +42,7 @@ export type ConvexGameApi = {
42
42
  serverId: string;
43
43
  action: string;
44
44
  entry: unknown;
45
- externalId?: string;
45
+ externalId?: string | undefined;
46
46
  }, null>;
47
47
  getServer: FunctionReference<"query", "public", {
48
48
  serverId: string;
@@ -55,13 +55,13 @@ export type ConvexGameApi = {
55
55
  getFeed: FunctionReference<"query", "public", {
56
56
  serverId: string;
57
57
  action: string;
58
- externalId?: string;
58
+ externalId?: string | undefined;
59
59
  }, unknown[]>;
60
60
  };
61
61
  presence: {
62
62
  list: FunctionReference<"query", "public", {
63
63
  serverId: string;
64
- externalId?: string;
64
+ externalId?: string | undefined;
65
65
  viewerChunkKey?: string;
66
66
  }, PresencePoseRow[]>;
67
67
  sync: FunctionReference<"mutation", "public", {
@@ -76,7 +76,7 @@ export type ConvexGameApi = {
76
76
  sessionId?: string;
77
77
  kind?: string;
78
78
  label?: string;
79
- externalId?: string;
79
+ externalId?: string | undefined;
80
80
  }, {
81
81
  pose: {
82
82
  x: number;
@@ -98,13 +98,13 @@ export type ConvexGameApi = {
98
98
  messages: FunctionReference<"query", "public", {
99
99
  serverId: string;
100
100
  channelId: string;
101
- externalId?: string;
101
+ externalId?: string | undefined;
102
102
  }, ChatMessage[]>;
103
103
  sendMessage: FunctionReference<"mutation", "public", {
104
104
  serverId: string;
105
105
  channelId: string;
106
106
  body: string;
107
- externalId?: string;
107
+ externalId?: string | undefined;
108
108
  }, ChatSendOutcome>;
109
109
  };
110
110
  leaderboard: {
@@ -53,9 +53,9 @@ export declare function createHostedGameServerFunctions(options: {
53
53
  tickMs?: number;
54
54
  }): {
55
55
  getHostedServer: import("convex/server").RegisteredQuery<"public", {
56
- externalId?: string | undefined;
56
+ externalId?: string;
57
57
  gameId: string;
58
- serverId?: string | undefined;
58
+ serverId?: string;
59
59
  }, Promise<{
60
60
  serverId: string;
61
61
  gameId: string;
@@ -64,24 +64,24 @@ export declare function createHostedGameServerFunctions(options: {
64
64
  memberUserIds: string[];
65
65
  } | null>>;
66
66
  joinHostedServer: import("convex/server").RegisteredMutation<"public", {
67
- externalId?: string | undefined;
67
+ externalId?: string;
68
68
  gameId: string;
69
- serverId?: string | undefined;
69
+ serverId?: string;
70
70
  }, Promise<{
71
71
  serverId: string;
72
72
  isNew: boolean;
73
73
  }>>;
74
74
  leaveHostedServer: import("convex/server").RegisteredMutation<"public", {
75
- externalId?: string | undefined;
75
+ externalId?: string;
76
76
  gameId: string;
77
- serverId?: string | undefined;
77
+ serverId?: string;
78
78
  }, Promise<null>>;
79
79
  runHostedCommand: import("convex/server").RegisteredMutation<"public", {
80
80
  command: string;
81
- externalId?: string | undefined;
81
+ externalId?: string;
82
82
  gameId: string;
83
83
  input: any;
84
- serverId?: string | undefined;
84
+ serverId?: string;
85
85
  }, Promise<{
86
86
  ok: false;
87
87
  reason: string;
@@ -89,10 +89,10 @@ export declare function createHostedGameServerFunctions(options: {
89
89
  ok: true;
90
90
  }>>;
91
91
  submitHostedInput: import("convex/server").RegisteredMutation<"public", {
92
- externalId?: string | undefined;
92
+ externalId?: string;
93
93
  frame: any;
94
94
  gameId: string;
95
- serverId?: string | undefined;
95
+ serverId?: string;
96
96
  }, Promise<null>>;
97
97
  tickHostedWorlds: import("convex/server").RegisteredMutation<"internal", {}, Promise<{
98
98
  ticked: number;
@@ -7,7 +7,7 @@ export declare function randomConvexPlayerId(): string;
7
7
  export declare function resolveConvexMultiplayer(args: {
8
8
  game: GameDefinition;
9
9
  gameId: string;
10
- url?: string;
10
+ url?: string | undefined;
11
11
  client?: ConvexReactClient;
12
12
  api?: ConvexGameApi;
13
13
  userId?: string;
package/dist/server.d.ts CHANGED
@@ -16,13 +16,13 @@ export { JG_MAX_MEMBERS_PER_SERVER, canJoinPrivateServer, isListablePublicly, va
16
16
  export declare function jgengineTables(): {
17
17
  jgGameServers: import("convex/server").TableDefinition<import("convex/values").VObject<{
18
18
  createdAt: number;
19
- dirtyAt?: number | undefined;
19
+ dirtyAt?: number;
20
20
  gameId: string;
21
- joinCode?: string | undefined;
22
- lastSavedAt?: number | undefined;
23
- memberCount?: number | undefined;
21
+ joinCode?: string;
22
+ lastSavedAt?: number;
23
+ memberCount?: number;
24
24
  memberUserIds: string[];
25
- mode?: string | undefined;
25
+ mode?: string;
26
26
  modeConfig?: any;
27
27
  revision: number;
28
28
  save: "none" | {
@@ -34,9 +34,9 @@ export declare function jgengineTables(): {
34
34
  slotsPerServer: number;
35
35
  status: "closed" | "open" | "running";
36
36
  tickAnchorMs: number;
37
- topology?: "rooms" | "shared" | undefined;
37
+ topology?: "rooms" | "shared";
38
38
  updatedAt: number;
39
- visibility?: "private" | "public" | undefined;
39
+ visibility?: "private" | "public";
40
40
  }, {
41
41
  gameId: import("convex/values").VString<string, "required">;
42
42
  status: import("convex/values").VUnion<"closed" | "open" | "running", [import("convex/values").VLiteral<"open", "required">, import("convex/values").VLiteral<"running", "required">, import("convex/values").VLiteral<"closed", "required">], "required", never>;
@@ -75,7 +75,7 @@ export declare function jgengineTables(): {
75
75
  jgServerCapacity: import("convex/server").TableDefinition<import("convex/values").VObject<{
76
76
  gameId: string;
77
77
  memberCount: number;
78
- mode?: string | undefined;
78
+ mode?: string;
79
79
  serverId: GenericId<"jgGameServers">;
80
80
  slotsPerServer: number;
81
81
  status: "closed" | "open" | "running";
@@ -92,7 +92,7 @@ export declare function jgengineTables(): {
92
92
  jgServerMembers: import("convex/server").TableDefinition<import("convex/values").VObject<{
93
93
  joinedAt: number;
94
94
  serverId: GenericId<"jgGameServers">;
95
- sessionIds?: string[] | undefined;
95
+ sessionIds?: string[];
96
96
  userId: string;
97
97
  }, {
98
98
  serverId: import("convex/values").VId<GenericId<"jgGameServers">, "required">;
@@ -133,11 +133,11 @@ export declare function jgengineTables(): {
133
133
  }, {}, {}>;
134
134
  jgPlayerProfiles: import("convex/server").TableDefinition<import("convex/values").VObject<{
135
135
  createdAt: number;
136
- dirtyAt?: number | undefined;
136
+ dirtyAt?: number;
137
137
  gameId: string;
138
138
  playerState: any;
139
139
  revision: number;
140
- sessionServerId?: GenericId<"jgGameServers"> | undefined;
140
+ sessionServerId?: GenericId<"jgGameServers">;
141
141
  sessionState?: any;
142
142
  updatedAt: number;
143
143
  userId: string;
@@ -156,7 +156,7 @@ export declare function jgengineTables(): {
156
156
  }, {}, {}>;
157
157
  jgWorldChunks: import("convex/server").TableDefinition<import("convex/values").VObject<{
158
158
  chunkKey: string;
159
- dirtyAt?: number | undefined;
159
+ dirtyAt?: number;
160
160
  serverId: GenericId<"jgGameServers">;
161
161
  snapshot: any;
162
162
  updatedAt: number;
@@ -173,7 +173,7 @@ export declare function jgengineTables(): {
173
173
  jgLeaderboardRows: import("convex/server").TableDefinition<import("convex/values").VObject<{
174
174
  gameId: string;
175
175
  scope: "global" | "profile" | "server";
176
- serverId?: GenericId<"jgGameServers"> | undefined;
176
+ serverId?: GenericId<"jgGameServers">;
177
177
  stat: string;
178
178
  updatedAt: number;
179
179
  userId: string;
@@ -204,17 +204,17 @@ export declare function jgengineTables(): {
204
204
  by_server_and_action: ["serverId", "action", "_creationTime"];
205
205
  }, {}, {}>;
206
206
  jgPoses: import("convex/server").TableDefinition<import("convex/values").VObject<{
207
- chunkKey?: string | undefined;
208
- homeGameId?: string | undefined;
209
- kind?: string | undefined;
210
- label?: string | undefined;
211
- lastWorldSnapshotAt?: number | undefined;
212
- ownerActorId?: string | undefined;
213
- revokedAt?: number | undefined;
207
+ chunkKey?: string;
208
+ homeGameId?: string;
209
+ kind?: string;
210
+ label?: string;
211
+ lastWorldSnapshotAt?: number;
212
+ ownerActorId?: string;
213
+ revokedAt?: number;
214
214
  rotationPitch: number;
215
215
  rotationY: number;
216
216
  serverId: string;
217
- sessionId?: string | undefined;
217
+ sessionId?: string;
218
218
  updatedAt: number;
219
219
  userId: string;
220
220
  x: number;
@@ -255,7 +255,7 @@ export declare function jgengineTables(): {
255
255
  }, {}, {}>;
256
256
  jgChatMessages: import("convex/server").TableDefinition<import("convex/values").VObject<{
257
257
  at: number;
258
- authorName?: string | undefined;
258
+ authorName?: string;
259
259
  body: string;
260
260
  channelId: string;
261
261
  serverId: string;
@@ -307,13 +307,13 @@ export declare function jgengineHostedTables(): {
307
307
  declare const schemaForTypes: import("convex/server").SchemaDefinition<{
308
308
  jgGameServers: import("convex/server").TableDefinition<import("convex/values").VObject<{
309
309
  createdAt: number;
310
- dirtyAt?: number | undefined;
310
+ dirtyAt?: number;
311
311
  gameId: string;
312
- joinCode?: string | undefined;
313
- lastSavedAt?: number | undefined;
314
- memberCount?: number | undefined;
312
+ joinCode?: string;
313
+ lastSavedAt?: number;
314
+ memberCount?: number;
315
315
  memberUserIds: string[];
316
- mode?: string | undefined;
316
+ mode?: string;
317
317
  modeConfig?: any;
318
318
  revision: number;
319
319
  save: "none" | {
@@ -325,9 +325,9 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
325
325
  slotsPerServer: number;
326
326
  status: "closed" | "open" | "running";
327
327
  tickAnchorMs: number;
328
- topology?: "rooms" | "shared" | undefined;
328
+ topology?: "rooms" | "shared";
329
329
  updatedAt: number;
330
- visibility?: "private" | "public" | undefined;
330
+ visibility?: "private" | "public";
331
331
  }, {
332
332
  gameId: import("convex/values").VString<string, "required">;
333
333
  status: import("convex/values").VUnion<"closed" | "open" | "running", [import("convex/values").VLiteral<"open", "required">, import("convex/values").VLiteral<"running", "required">, import("convex/values").VLiteral<"closed", "required">], "required", never>;
@@ -366,7 +366,7 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
366
366
  jgServerCapacity: import("convex/server").TableDefinition<import("convex/values").VObject<{
367
367
  gameId: string;
368
368
  memberCount: number;
369
- mode?: string | undefined;
369
+ mode?: string;
370
370
  serverId: GenericId<"jgGameServers">;
371
371
  slotsPerServer: number;
372
372
  status: "closed" | "open" | "running";
@@ -383,7 +383,7 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
383
383
  jgServerMembers: import("convex/server").TableDefinition<import("convex/values").VObject<{
384
384
  joinedAt: number;
385
385
  serverId: GenericId<"jgGameServers">;
386
- sessionIds?: string[] | undefined;
386
+ sessionIds?: string[];
387
387
  userId: string;
388
388
  }, {
389
389
  serverId: import("convex/values").VId<GenericId<"jgGameServers">, "required">;
@@ -424,11 +424,11 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
424
424
  }, {}, {}>;
425
425
  jgPlayerProfiles: import("convex/server").TableDefinition<import("convex/values").VObject<{
426
426
  createdAt: number;
427
- dirtyAt?: number | undefined;
427
+ dirtyAt?: number;
428
428
  gameId: string;
429
429
  playerState: any;
430
430
  revision: number;
431
- sessionServerId?: GenericId<"jgGameServers"> | undefined;
431
+ sessionServerId?: GenericId<"jgGameServers">;
432
432
  sessionState?: any;
433
433
  updatedAt: number;
434
434
  userId: string;
@@ -447,7 +447,7 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
447
447
  }, {}, {}>;
448
448
  jgWorldChunks: import("convex/server").TableDefinition<import("convex/values").VObject<{
449
449
  chunkKey: string;
450
- dirtyAt?: number | undefined;
450
+ dirtyAt?: number;
451
451
  serverId: GenericId<"jgGameServers">;
452
452
  snapshot: any;
453
453
  updatedAt: number;
@@ -464,7 +464,7 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
464
464
  jgLeaderboardRows: import("convex/server").TableDefinition<import("convex/values").VObject<{
465
465
  gameId: string;
466
466
  scope: "global" | "profile" | "server";
467
- serverId?: GenericId<"jgGameServers"> | undefined;
467
+ serverId?: GenericId<"jgGameServers">;
468
468
  stat: string;
469
469
  updatedAt: number;
470
470
  userId: string;
@@ -495,17 +495,17 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
495
495
  by_server_and_action: ["serverId", "action", "_creationTime"];
496
496
  }, {}, {}>;
497
497
  jgPoses: import("convex/server").TableDefinition<import("convex/values").VObject<{
498
- chunkKey?: string | undefined;
499
- homeGameId?: string | undefined;
500
- kind?: string | undefined;
501
- label?: string | undefined;
502
- lastWorldSnapshotAt?: number | undefined;
503
- ownerActorId?: string | undefined;
504
- revokedAt?: number | undefined;
498
+ chunkKey?: string;
499
+ homeGameId?: string;
500
+ kind?: string;
501
+ label?: string;
502
+ lastWorldSnapshotAt?: number;
503
+ ownerActorId?: string;
504
+ revokedAt?: number;
505
505
  rotationPitch: number;
506
506
  rotationY: number;
507
507
  serverId: string;
508
- sessionId?: string | undefined;
508
+ sessionId?: string;
509
509
  updatedAt: number;
510
510
  userId: string;
511
511
  x: number;
@@ -546,7 +546,7 @@ declare const schemaForTypes: import("convex/server").SchemaDefinition<{
546
546
  }, {}, {}>;
547
547
  jgChatMessages: import("convex/server").TableDefinition<import("convex/values").VObject<{
548
548
  at: number;
549
- authorName?: string | undefined;
549
+ authorName?: string;
550
550
  body: string;
551
551
  channelId: string;
552
552
  serverId: string;
@@ -626,7 +626,7 @@ export type RunCommandOutcome = {
626
626
  };
627
627
  /** Arguments of the `runCommand` mutation, and of the equivalent {@link GameServerHelpers.runCommand} helper. */
628
628
  export type ConvexRunCommandArgs = RunCommandArgs & {
629
- externalId?: string;
629
+ externalId?: string | undefined;
630
630
  /**
631
631
  * How much of the server to hydrate before applying the command. Overrides whatever the
632
632
  * {@link CommandDef} declares through its own `scope`; omit both to hydrate the whole world.
@@ -723,42 +723,42 @@ export declare function createGameServerFunctions(options?: {
723
723
  allowedFeedActions?: readonly string[];
724
724
  }): {
725
725
  joinServer: import("convex/server").RegisteredMutation<"public", {
726
- externalId?: string | undefined;
726
+ externalId?: string;
727
727
  gameId: string;
728
- joinCode?: string | undefined;
729
- mode?: string | undefined;
728
+ joinCode?: string;
729
+ mode?: string;
730
730
  modeConfig?: any;
731
- serverId?: GenericId<"jgGameServers"> | undefined;
732
- sessionId?: string | undefined;
733
- visibility?: "private" | "public" | undefined;
731
+ serverId?: GenericId<"jgGameServers">;
732
+ sessionId?: string;
733
+ visibility?: "private" | "public";
734
734
  }, Promise<EnsureJoinedOutcome>>;
735
735
  leaveServer: import("convex/server").RegisteredMutation<"public", {
736
- externalId?: string | undefined;
736
+ externalId?: string;
737
737
  serverId: GenericId<"jgGameServers">;
738
- sessionId?: string | undefined;
738
+ sessionId?: string;
739
739
  }, Promise<null>>;
740
740
  runCommand: import("convex/server").RegisteredMutation<"public", {
741
741
  command: string;
742
- externalId?: string | undefined;
742
+ externalId?: string;
743
743
  input: any;
744
744
  scope?: {
745
- chunkKeys?: string[] | undefined;
746
- players?: string[] | undefined;
747
- } | undefined;
745
+ chunkKeys?: string[];
746
+ players?: string[];
747
+ };
748
748
  serverId: GenericId<"jgGameServers">;
749
749
  }, Promise<RunCommandOutcome>>;
750
750
  flushSave: import("convex/server").RegisteredMutation<"public", {
751
- externalId?: string | undefined;
751
+ externalId?: string;
752
752
  serverId: GenericId<"jgGameServers">;
753
753
  }, Promise<boolean>>;
754
754
  getServer: import("convex/server").RegisteredQuery<"public", {
755
- externalId?: string | undefined;
755
+ externalId?: string;
756
756
  serverId: GenericId<"jgGameServers">;
757
757
  }, Promise<{
758
758
  _id: GenericId<"jgGameServers">;
759
759
  gameId: string;
760
760
  status: "closed" | "open" | "running";
761
- mode: string | undefined;
761
+ mode?: string;
762
762
  memberUserIds: string[];
763
763
  slotsPerServer: number;
764
764
  save: "none" | {
@@ -773,30 +773,30 @@ export declare function createGameServerFunctions(options?: {
773
773
  updatedAt: number;
774
774
  } | null>>;
775
775
  getServerMeta: import("convex/server").RegisteredQuery<"public", {
776
- externalId?: string | undefined;
776
+ externalId?: string;
777
777
  serverId: GenericId<"jgGameServers">;
778
- }, Promise<{
778
+ }, Promise<import("@jgengine/core/data/definedOnly").DefinedOnly<{
779
779
  _id: GenericId<"jgGameServers">;
780
780
  gameId: string;
781
781
  status: "closed" | "open" | "running";
782
782
  mode: string | undefined;
783
783
  memberCount: number;
784
784
  slotsPerServer: number;
785
- } | null>>;
785
+ }> | null>>;
786
786
  getServerCapacity: import("convex/server").RegisteredQuery<"public", {
787
- externalId?: string | undefined;
787
+ externalId?: string;
788
788
  serverId: GenericId<"jgGameServers">;
789
- }, Promise<{
789
+ }, Promise<import("@jgengine/core/data/definedOnly").DefinedOnly<{
790
790
  _id: GenericId<"jgGameServers">;
791
791
  gameId: string;
792
792
  status: "closed" | "open" | "running";
793
793
  mode: string | undefined;
794
794
  memberCount: number;
795
795
  slotsPerServer: number;
796
- } | null>>;
796
+ }> | null>>;
797
797
  getChunks: import("convex/server").RegisteredQuery<"public", {
798
798
  chunkKeys: string[];
799
- externalId?: string | undefined;
799
+ externalId?: string;
800
800
  serverId: GenericId<"jgGameServers">;
801
801
  }, Promise<{
802
802
  chunkKey: string;
@@ -804,7 +804,7 @@ export declare function createGameServerFunctions(options?: {
804
804
  updatedAt: number;
805
805
  }[]>>;
806
806
  getPlayerProfile: import("convex/server").RegisteredQuery<"public", {
807
- externalId?: string | undefined;
807
+ externalId?: string;
808
808
  gameId: string;
809
809
  }, Promise<{
810
810
  userId: string;
@@ -815,27 +815,28 @@ export declare function createGameServerFunctions(options?: {
815
815
  } | null>>;
816
816
  getFeed: import("convex/server").RegisteredQuery<"public", {
817
817
  action: string;
818
- externalId?: string | undefined;
818
+ externalId?: string;
819
819
  serverId: GenericId<"jgGameServers">;
820
820
  }, Promise<any[]>>;
821
821
  pushFeedEntry: import("convex/server").RegisteredMutation<"public", {
822
822
  action: string;
823
823
  entry: any;
824
- externalId?: string | undefined;
824
+ externalId?: string;
825
825
  serverId: GenericId<"jgGameServers">;
826
826
  }, Promise<null>>;
827
827
  listOpenServers: import("convex/server").RegisteredQuery<"public", {
828
- externalId?: string | undefined;
828
+ externalId?: string;
829
829
  gameId: string;
830
- limit?: number | undefined;
831
- }, Promise<{
830
+ limit?: number;
831
+ }, Promise<({
832
832
  _id: GenericId<"jgGameServers">;
833
833
  status: "closed" | "open" | "running";
834
834
  memberCount: number;
835
835
  slotsPerServer: number;
836
- mode: string | undefined;
837
836
  updatedAt: number;
838
- }[]>>;
837
+ } & {
838
+ mode?: string;
839
+ })[]>>;
839
840
  tickActiveServers: import("convex/server").RegisteredMutation<"internal", {}, Promise<{
840
841
  ticked: number;
841
842
  saved: number;
@@ -850,18 +851,18 @@ export declare function createLeaderboardFunctions(options?: {
850
851
  auth?: JgAuthMode;
851
852
  }): {
852
853
  getTop: import("convex/server").RegisteredQuery<"public", {
853
- externalId?: string | undefined;
854
+ externalId?: string;
854
855
  gameId: string;
855
- limit?: number | undefined;
856
+ limit?: number;
856
857
  scope: "global" | "profile" | "server";
857
- serverId?: GenericId<"jgGameServers"> | undefined;
858
+ serverId?: GenericId<"jgGameServers">;
858
859
  stat: string;
859
860
  }, Promise<{
860
861
  userId: string;
861
862
  value: number;
862
863
  }[]>>;
863
864
  getProfile: import("convex/server").RegisteredQuery<"public", {
864
- externalId?: string | undefined;
865
+ externalId?: string;
865
866
  gameId: string;
866
867
  userId: string;
867
868
  }, Promise<Record<string, number>>>;
@@ -869,7 +870,7 @@ export declare function createLeaderboardFunctions(options?: {
869
870
  entries: {
870
871
  by: number;
871
872
  scope: "global" | "profile" | "server";
872
- serverId?: GenericId<"jgGameServers"> | undefined;
873
+ serverId?: GenericId<"jgGameServers">;
873
874
  stat: string;
874
875
  userId: string;
875
876
  }[];
@@ -925,7 +926,7 @@ export declare function createPresenceFunctions(options?: {
925
926
  resolveSpawn?: (ctx: JGMutationCtx, args: {
926
927
  serverId: string;
927
928
  userId: string;
928
- kind?: string;
929
+ kind?: string | undefined;
929
930
  }) => Promise<{
930
931
  x: number;
931
932
  y: number;
@@ -937,28 +938,28 @@ export declare function createPresenceFunctions(options?: {
937
938
  } | null;
938
939
  }): {
939
940
  list: import("convex/server").RegisteredQuery<"public", {
940
- externalId?: string | undefined;
941
+ externalId?: string;
941
942
  serverId: string;
942
- viewerChunkKey?: string | undefined;
943
+ viewerChunkKey?: string;
943
944
  }, Promise<PresenceListRow[]>>;
944
945
  sync: import("convex/server").RegisteredMutation<"public", {
945
- externalId?: string | undefined;
946
- kind?: string | undefined;
947
- label?: string | undefined;
946
+ externalId?: string;
947
+ kind?: string;
948
+ label?: string;
948
949
  pose?: {
949
950
  rotationPitch: number;
950
951
  rotationY: number;
951
952
  x: number;
952
953
  y: number;
953
954
  z: number;
954
- } | undefined;
955
+ };
955
956
  serverId: string;
956
- sessionId?: string | undefined;
957
+ sessionId?: string;
957
958
  }, Promise<PresenceSyncResult | null>>;
958
959
  leave: import("convex/server").RegisteredMutation<"public", {
959
- externalId?: string | undefined;
960
+ externalId?: string;
960
961
  serverId: string;
961
- sessionId?: string | undefined;
962
+ sessionId?: string;
962
963
  }, Promise<null>>;
963
964
  reapIdlePresence: import("convex/server").RegisteredMutation<"internal", {}, Promise<{
964
965
  reaped: number;
@@ -1033,7 +1034,7 @@ export declare function recentChatMessages(ctx: {
1033
1034
  _creationTime: number;
1034
1035
  _id: GenericId<"jgChatMessages">;
1035
1036
  at: number;
1036
- authorName?: string | undefined;
1037
+ authorName?: string;
1037
1038
  body: string;
1038
1039
  channelId: string;
1039
1040
  serverId: string;
@@ -1045,19 +1046,19 @@ export declare function recentChatMessages(ctx: {
1045
1046
  */
1046
1047
  export declare function createClientErrorFunctions(): {
1047
1048
  reportClientError: import("convex/server").RegisteredMutation<"public", {
1048
- agentPrompt?: string | undefined;
1049
- componentStack?: string | undefined;
1050
- kind?: string | undefined;
1049
+ agentPrompt?: string;
1050
+ componentStack?: string;
1051
+ kind?: string;
1051
1052
  message: string;
1052
- pathname?: string | undefined;
1053
- stack?: string | undefined;
1054
- url?: string | undefined;
1055
- userAgent?: string | undefined;
1053
+ pathname?: string;
1054
+ stack?: string;
1055
+ url?: string;
1056
+ userAgent?: string;
1056
1057
  }, Promise<{
1057
1058
  ok: boolean;
1058
1059
  reason: string;
1059
1060
  } | {
1060
- reason?: undefined;
1061
+ reason?: never;
1061
1062
  ok: boolean;
1062
1063
  }>>;
1063
1064
  pruneClientErrors: import("convex/server").RegisteredMutation<"internal", {}, Promise<{
@@ -1078,18 +1079,18 @@ export declare function createChatFunctions(options?: {
1078
1079
  }): {
1079
1080
  messages: import("convex/server").RegisteredQuery<"public", {
1080
1081
  channelId: string;
1081
- externalId?: string | undefined;
1082
+ externalId?: string;
1082
1083
  serverId: string;
1083
1084
  }, Promise<ChatMessage[]>>;
1084
1085
  recent: import("convex/server").RegisteredQuery<"public", {
1085
1086
  channelId: string;
1086
- externalId?: string | undefined;
1087
+ externalId?: string;
1087
1088
  serverId: string;
1088
1089
  }, Promise<ChatMessage[]>>;
1089
1090
  sendMessage: import("convex/server").RegisteredMutation<"public", {
1090
1091
  body: string;
1091
1092
  channelId: string;
1092
- externalId?: string | undefined;
1093
+ externalId?: string;
1093
1094
  serverId: string;
1094
1095
  }, Promise<ChatSendOutcome>>;
1095
1096
  pruneChatMessages: import("convex/server").RegisteredMutation<"internal", {}, Promise<{
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { defineSchema, defineTable, internalMutationGeneric, mutationGeneric, queryGeneric, } from "convex/server";
2
2
  import { ConvexError, v } from "convex/values";
3
3
  import { validateChatMessage } from "@jgengine/core/chat/chat";
4
+ import { definedOnly } from "@jgengine/core/data/definedOnly";
4
5
  import { createFeedWriteGate, validateFeedWrite, } from "@jgengine/core/multiplayer/feedWriteGate";
5
6
  import { DEFAULT_POSE_SYNC_RULES, decidePoseSync, isPresenceExpired, pickReusablePresence, resolveActivePresence, spawnPresenceState, } from "@jgengine/core/multiplayer/presenceModel";
6
7
  import { SERVER_ONLY_COMMAND_REASON, commandScopeEscape, scopeWithActor } from "@jgengine/core/commands/commandRunner";
@@ -298,10 +299,10 @@ async function ensureServerCapacity(ctx, server) {
298
299
  const existing = await serverCapacity(ctx, server);
299
300
  if (existing)
300
301
  return existing;
301
- const id = await ctx.db.insert("jgServerCapacity", {
302
+ const id = await ctx.db.insert("jgServerCapacity", definedOnly({
302
303
  serverId: server._id, gameId: server.gameId, status: server.status, mode: server.mode,
303
304
  memberCount: server.memberCount ?? server.memberUserIds.length, slotsPerServer: server.slotsPerServer,
304
- });
305
+ }));
305
306
  return (await ctx.db.get("jgServerCapacity", id));
306
307
  }
307
308
  /**
@@ -387,7 +388,7 @@ export async function applyLeaderboardIncrements(ctx, gameId, entries) {
387
388
  await ctx.db.patch(existing._id, { value: existing.value + entry.by, updatedAt: now });
388
389
  }
389
390
  else {
390
- await ctx.db.insert("jgLeaderboardRows", {
391
+ await ctx.db.insert("jgLeaderboardRows", definedOnly({
391
392
  gameId,
392
393
  stat: entry.stat,
393
394
  scope: entry.scope,
@@ -395,7 +396,7 @@ export async function applyLeaderboardIncrements(ctx, gameId, entries) {
395
396
  userId: entry.userId,
396
397
  value: entry.by,
397
398
  updatedAt: now,
398
- });
399
+ }));
399
400
  }
400
401
  }
401
402
  }
@@ -475,7 +476,8 @@ export async function persistServerSnapshot(ctx, server, snapshot, save) {
475
476
  await ctx.db.insert("jgWorldChunks", {
476
477
  serverId: server._id,
477
478
  chunkKey: chunk.chunkKey,
478
- ...chunkPatch,
479
+ snapshot: chunk.snapshot,
480
+ updatedAt: chunk.updatedAt,
479
481
  });
480
482
  }
481
483
  }
@@ -582,10 +584,10 @@ export function createGameServerFunctions(options) {
582
584
  const serverId = await ctx.db.insert("jgGameServers", {
583
585
  gameId: args.gameId,
584
586
  status: "running",
585
- mode: args.mode,
587
+ ...(args.mode === undefined ? {} : { mode: args.mode }),
586
588
  modeConfig: args.modeConfig,
587
589
  visibility: args.visibility ?? "public",
588
- joinCode: args.joinCode,
590
+ ...(args.joinCode === undefined ? {} : { joinCode: args.joinCode }),
589
591
  topology: shared ? "shared" : "rooms",
590
592
  memberCount: 0,
591
593
  memberUserIds: [],
@@ -930,7 +932,7 @@ export function createGameServerFunctions(options) {
930
932
  _id: server._id,
931
933
  gameId: server.gameId,
932
934
  status: server.status,
933
- mode: server.mode,
935
+ ...(server.mode === undefined ? {} : { mode: server.mode }),
934
936
  memberUserIds: server.memberUserIds,
935
937
  slotsPerServer: server.slotsPerServer,
936
938
  save: server.save,
@@ -958,7 +960,7 @@ export function createGameServerFunctions(options) {
958
960
  memberCount: v.number(),
959
961
  slotsPerServer: v.number(),
960
962
  }), v.null()),
961
- handler: (ctx, args) => withMember(ctx, mode, args, null, async (server) => ({
963
+ handler: (ctx, args) => withMember(ctx, mode, args, null, async (server) => definedOnly({
962
964
  _id: server._id,
963
965
  gameId: server.gameId,
964
966
  status: server.status,
@@ -983,10 +985,10 @@ export function createGameServerFunctions(options) {
983
985
  const member = await ctx.db.query("jgServerMembers").withIndex("by_server_and_user", q => q.eq("serverId", args.serverId).eq("userId", actor)).unique();
984
986
  if (!member)
985
987
  return null;
986
- return { _id: capacity.serverId, gameId: capacity.gameId, status: capacity.status, mode: capacity.mode, memberCount: capacity.memberCount, slotsPerServer: capacity.slotsPerServer };
988
+ return definedOnly({ _id: capacity.serverId, gameId: capacity.gameId, status: capacity.status, mode: capacity.mode, memberCount: capacity.memberCount, slotsPerServer: capacity.slotsPerServer });
987
989
  }
988
990
  const server = await requireServerMember(ctx, args.serverId, actor);
989
- return server ? { _id: server._id, gameId: server.gameId, status: server.status, mode: server.mode, memberCount: server.memberUserIds.length, slotsPerServer: server.slotsPerServer } : null;
991
+ return server ? definedOnly({ _id: server._id, gameId: server.gameId, status: server.status, mode: server.mode, memberCount: server.memberUserIds.length, slotsPerServer: server.slotsPerServer }) : null;
990
992
  },
991
993
  });
992
994
  /**
@@ -1125,7 +1127,7 @@ export function createGameServerFunctions(options) {
1125
1127
  return Promise.all(rows
1126
1128
  .filter((row) => isListablePublicly(row.visibility))
1127
1129
  .slice(0, limit)
1128
- .map(async (row) => ({
1130
+ .map(async (row) => definedOnly({
1129
1131
  _id: row._id,
1130
1132
  status: row.status,
1131
1133
  memberCount: (await serverCapacity(ctx, row))?.memberCount ?? row.memberCount ?? row.memberUserIds.length,
@@ -1327,7 +1329,7 @@ export function createPresenceFunctions(options) {
1327
1329
  }
1328
1330
  return rows
1329
1331
  .filter((row) => row.revokedAt === undefined && row.updatedAt >= threshold)
1330
- .map((row) => ({
1332
+ .map((row) => definedOnly({
1331
1333
  userId: row.userId,
1332
1334
  sessionId: row.sessionId,
1333
1335
  kind: row.kind,
@@ -1550,7 +1552,7 @@ export async function sendChatMessage(ctx, args) {
1550
1552
  const gate = await rateLimit(ctx, { key: JSON.stringify(["chat", args.serverId, args.userId]), windowMs: args.minIntervalMs ?? 2000, max: 1 });
1551
1553
  if (!gate.ok)
1552
1554
  return { ok: false, reason: "rate_limited" };
1553
- await ctx.db.insert("jgChatMessages", { serverId: args.serverId, channelId: args.channelId ?? "global", userId: args.userId, body: validation.text, authorName: args.authorName, at: Date.now() });
1555
+ await ctx.db.insert("jgChatMessages", definedOnly({ serverId: args.serverId, channelId: args.channelId ?? "global", userId: args.userId, body: validation.text, authorName: args.authorName, at: Date.now() }));
1554
1556
  return { ok: true };
1555
1557
  }
1556
1558
  /**
@@ -1664,8 +1666,8 @@ export function createChatFunctions(options) {
1664
1666
  .withIndex("by_server_channel", (q) => q.eq("serverId", args.serverId).eq("channelId", args.channelId))
1665
1667
  .order("desc")
1666
1668
  .take(historyLimit * 2 + 16);
1667
- for (let i = historyLimit * 2; i < recent.length; i += 1) {
1668
- await ctx.db.delete(recent[i]._id);
1669
+ for (const stale of recent.slice(historyLimit * 2)) {
1670
+ await ctx.db.delete(stale._id);
1669
1671
  }
1670
1672
  return { ok: true };
1671
1673
  },
@@ -18,7 +18,7 @@ export interface WorldPresenceRecord extends PresenceResidentRow {
18
18
  rotationY: number;
19
19
  rotationPitch: number;
20
20
  lastSeenAt: number;
21
- lastWorldSnapshotAt?: number;
21
+ lastWorldSnapshotAt?: number | undefined;
22
22
  }
23
23
  /** World chunk size and host-owned pose, checkpoint and retention policies. */
24
24
  export interface WorldPresenceOptions {
@@ -1,3 +1,4 @@
1
+ import { definedOnly } from "@jgengine/core/data/definedOnly";
1
2
  import { DEFAULT_POSE_SYNC_RULES, decidePoseSync, shouldPersistWorldSnapshot } from "@jgengine/core/multiplayer/presenceModel";
2
3
  import { chunkKeyAt, chunkKeysAround, parseChunkKey, DEFAULT_CHUNK_SIZE } from "@jgengine/core/runtime/worldChunks";
3
4
  function record(row) {
@@ -47,7 +48,7 @@ export function createWorldPresenceStore(options = {}) {
47
48
  label: args.label ?? existing?.label, x: args.position.x, y: args.position.y, z: args.position.z,
48
49
  rotationY: 0, rotationPitch: 0, chunkKey: chunkKeyAt([args.position.x, args.position.y, args.position.z], size),
49
50
  updatedAt: nowMs, revokedAt: undefined };
50
- const id = existing ? existing._id : await ctx.db.insert("jgPoses", patch);
51
+ const id = existing ? existing._id : await ctx.db.insert("jgPoses", definedOnly(patch));
51
52
  if (existing)
52
53
  await ctx.db.patch(id, patch);
53
54
  return record((await ctx.db.get("jgPoses", id)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/convex",
3
- "version": "0.49.1",
3
+ "version": "0.50.0",
4
4
  "description": "Convex adapters for JGengine: game transport, presence transport, and backend wiring over @jgengine/core.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "check-types": "tsgo --noEmit -p tsconfig.json"
31
31
  },
32
32
  "dependencies": {
33
- "@jgengine/core": "^0.49.1"
33
+ "@jgengine/core": "^0.50.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "convex": "^1.31.2",
@@ -7,7 +7,7 @@ description: Route game work to the smallest useful JGengine guidance set.
7
7
 
8
8
  This skill decides what to load; it is not an engine manual. Package map, commands, layering, and license live in the JGengine README (repo-root [`README.md`](../../../README.md) in the monorepo, [jgengine.com](https://jgengine.com) from a created project); open it when you need one of those facts.
9
9
 
10
- A created project installs only `jgengine`, `jgengine-editor`, `jgengine-verify`, `game-design`, and `jgengine-ui`. Run `npx jgengine skills -p --all` before loading any other domain named below. Those projects do not ship `api.md`; read exact signatures from the `.d.ts` files under `node_modules/@jgengine/<pkg>/dist/`.
10
+ A created project installs only `jgengine`, `create-game-jg`, `jgengine-editor`, `jgengine-verify`, `game-design`, and `jgengine-ui`. Run `npx jgengine skills -p --all` before loading any other domain named below. Those projects do not ship `api.md`; read exact signatures from the `.d.ts` files under `node_modules/@jgengine/<pkg>/dist/`.
11
11
 
12
12
  Created projects pin their scaffolding CLI as a local devDependency, separately from SDK versions. Run `npx jgengine doctor` after SDK upgrades to check CLI compatibility. Skill installation preserves newer or differing unversioned copies; update the project CLI first, and use `skills -p --force` only when replacement is intended. Each packaged skill install records its CLI and SDK versions in `.jgengine-version.json`.
13
13
 
@@ -27,7 +27,7 @@ finished solution.
27
27
 
28
28
  ## 1. Establish the target
29
29
 
30
- Before building a game or a big feature, plan it: `npx jgengine plan <game> --brief "<brief>"` writes `PLAN.md`; list every feature the brief implies under `## Features` and run `npx jgengine plan <game>` again to fill each feature's engine owner and the gaps; then order `## Milestones`, one PR each, and tick each as it lands. For greenfield builds, also write `src/art-direction.md` before any code.
30
+ A new game or a big new feature starts with the `create-game-jg` skill: agree the direction with the user, map every feature with `jgengine has`, write `PLAN.md` with its design brief, then `jgengine create` and build it one milestone per PR, each the next 10 minutes of play. Load it before anything below.
31
31
 
32
32
  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:
33
33
 
@@ -38,7 +38,7 @@ Treat the pitch as a unique composition of needs, not a genre to fill in — nev
38
38
  - HUD/menu states and **custom** UI art direction (compose shipped building blocks + HudTheme; no unskinned stock face)
39
39
  - one observable completion scenario
40
40
 
41
- Pick the dimension first: `create` defaults to a 3D world (terrain, sky, player model, editor scene); a grid, card, tile, or other board game with no 3D scene takes `create --template 2d` (a `board` world drawn by GameUI over `@jgengine/core/puzzle/cellGrid`). `create --template 3d|2d|builder` is the one way to start a game, in this monorepo too (`bun run jgengine create "<Title>"` lands in `games/`); never copy `games/*`. The game's `jgengine.json` says what it is and uses (template, editor, `@jgengine` packages, and in-repo its adoption debt and boot probes). Change what a game uses by editing it, then run `jgengine sync`, which also brings a game up to the running engine: its `@jgengine` versions and its template's shared files (wiring, page, tooling scripts, AGENTS.md). A shared file the game rewrote goes in its `owns` list so sync leaves it alone.
41
+ Start a game only through `create-game-jg` (it picks the template and runs `jgengine create`); never copy `games/*`. The game's `jgengine.json` says what it is and uses (template, editor, `@jgengine` packages, and in-repo its adoption debt and boot probes). Change what a game uses by editing it, then run `jgengine sync`, which also brings a game up to the running engine: its `@jgengine` versions and its template's shared files (wiring, page, tooling scripts, AGENTS.md). A shared file the game rewrote goes in its `owns` list so sync leaves it alone.
42
42
 
43
43
  For an existing game asked to pick up a new engine release ("jgengine published a new version, grab it"), run `npx jgengine@latest sync` in the project (or at its workspace root): it moves every `@jgengine` pin and catalog entry to that release, refreshes the template's files, and prints every Migrate step and Adopt-worthy addition in between. Install, migrate oldest-first, then work the Adopt lists instead of reconstructing the changelog by hand.
44
44
 
@@ -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.49.1" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
9
+ - `VERSION` (const): const VERSION: "0.50.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
10
10
 
11
11
  ## @jgengine/core/authoring
12
12
 
@@ -277,6 +277,7 @@
277
277
  ## @jgengine/core/devtools/frameBreakdown
278
278
 
279
279
  - `DrawCost` (interface): interface DrawCost — One object or material's share of the draws: totals while gathering, per-frame averages in a summary.
280
+ - `FrameBreakdown` (interface): interface FrameBreakdown — A running breakdown, as `installFrameBreakdown` returns it and `window.__jgFrameBreakdown` holds it.
280
281
  - `FrameBreakdownRecord` (interface): interface FrameBreakdownRecord — One frame, as the hooks saw it. GPU times arrive a few frames late; `gpu` is null until they do.
281
282
  - `FrameBreakdownSummary` (interface): interface FrameBreakdownSummary — A window of frames, summarized for a person or a diff.
282
283
  - `HITCH_MS` (const): const HITCH_MS: 33 — Where each frame goes: the record of one frame (CPU per system, GPU per render pass, draws, shader compiles, uploads, heap) and the summary of a window of them (percentiles, the costliest systems, passes, objects and materials, and every hitch with what caused it). This file is the arithmetic; `frameBreakdown.draw.ts` fills the records from a live renderer.
@@ -286,9 +287,6 @@
286
287
 
287
288
  ## @jgengine/core/devtools/frameBreakdown.draw
288
289
 
289
- - `BreakdownRenderer` (interface): interface BreakdownRenderer — What the breakdown needs from a three.js WebGLRenderer.
290
- - `BreakdownStore` (interface): interface BreakdownStore — What it needs from the r3f store: the subscriber list and the subscribe call.
291
- - `FrameBreakdown` (interface): interface FrameBreakdown — A running breakdown.
292
290
  - `measureComposerPasses` (function): function measureComposerPasses(composer: { passes: readonly object[] }): void — Time each post pass of a composer by name, CPU and GPU. Cheap when no breakdown is installed; call it each frame before `composer.render` so passes added later are timed too.
293
291
 
294
292
  ## @jgengine/core/devtools/frameSampler.view
@@ -438,7 +436,7 @@
438
436
 
439
437
  - `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
440
438
  - `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
441
- - `VERSION` (const): const VERSION: "0.49.1" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
439
+ - `VERSION` (const): const VERSION: "0.50.0" — Installed `@jgengine/core` semver - compare against {@link CHANGELOG} keys when migrating.
442
440
 
443
441
  ## @jgengine/core/runtime/adapter
444
442
 
@@ -576,7 +574,7 @@
576
574
  ## @jgengine/core/runtime/hostPersistence
577
575
 
578
576
  - `FEED_RING_LIMIT` (const): const FEED_RING_LIMIT: 20 — ⚠ undocumented
579
- - `GameServerRecord` (type): type GameServerRecord = { serverId: string; gameId: string; status: GameServerStatus; mode?: string; modeConfig?: unknown; label?: string; visibility?: SessionVisibility; joinCode?: string; tags?: string[]; memberUserIds: string[]; slotsPerServer: number; save: SaveConfig; serverState: RuntimeServer… — ⚠ undocumented
577
+ - `GameServerRecord` (type): type GameServerRecord = { serverId: string; gameId: string; status: GameServerStatus; mode?: string | undefined; modeConfig?: unknown; label?: string; visibility?: SessionVisibility | undefined; joinCode?: string | undefined; tags?: string[]; memberUserIds: string[]; slotsPerServer: number; save: Sa… — ⚠ undocumented
580
578
  - `GameServerStatus` (type): type GameServerStatus = "open" | "running" | "closed" — ⚠ undocumented · used by `statusAfterLeave` (@jgengine/core/runtime/hostPolicy): Status after a leave: empty rooms reopen; non-empty rooms keep their current status.
581
579
  - `HostPersistence` (type): type HostPersistence = { savePlan?: (plan: ServerPersistPlan) => Promise<void>; resetScenario?: (reset: NormalizedScenarioReset) => Promise<void>; loadServer: (serverId: string) => Promise<GameServerRecord | null>; saveServer: (record: GameServerRecord) => Promise<void>; listServers: (gameId: string… — ⚠ undocumented · used by `memoryPersistence` (@jgengine/node): Creates an in-memory `HostPersistence` implementation, useful for tests and ephemeral hosts.
582
580
  - `LEADERBOARD_PENDING_KEY` (const): const LEADERBOARD_PENDING_KEY: "leaderboardPending" — ⚠ undocumented
@@ -750,7 +748,7 @@
750
748
  - `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
751
749
  - `GameRuntimePlayerView` (type): type GameRuntimePlayerView = { userId: string; gameId: string; playerState: unknown; updatedAt: number; } — ⚠ undocumented
752
750
  - `GameRuntimeServerView` (type): type GameRuntimeServerView = { serverId: string; gameId: string; revision: number; memberUserIds: string[]; serverState: unknown | WorldSyncFrame; updatedAt: number; } — ⚠ undocumented
753
- - `GameRuntimeTransport` (type): type GameRuntimeTransport = { /** * Join a server. `sessionId` names this client session (one tab, one mounted view) so the host keeps * the user a member while any of their sessions is still live. */ joinServer: (args: { gameId: string; serverId?: string; role?: MultiplayerRole; sessionId?: string … — ⚠ undocumented · used by `useServerSession` (@jgengine/core/session/useServerSession.view): Joins a host and exposes a retryable blocking state until membership is confirmed.
751
+ - `GameRuntimeTransport` (type): type GameRuntimeTransport = { /** * Join a server. `sessionId` names this client session (one tab, one mounted view) so the host keeps * the user a member while any of their sessions is still live. */ joinServer: (args: { gameId: string; serverId?: string | undefined; role?: MultiplayerRole; session… — ⚠ undocumented · used by `useServerSession` (@jgengine/core/session/useServerSession.view): Joins a host and exposes a retryable blocking state until membership is confirmed.
754
752
  - `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.
755
753
  - `JoinServerResult` (type): type JoinServerResult = { serverId: string; isNew: boolean; resumeTicket?: ResumeTicket; } — ⚠ undocumented
756
754
  - `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
@@ -5,14 +5,14 @@
5
5
  ## @jgengine/convex
6
6
 
7
7
  - `ConvexBackend` (type): type ConvexBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = LiveGameBackend<TPresenceRow, TPresenceLocation, TGameId> & { leaderboard: ConvexLeaderboardReads; } — ⚠ undocumented
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
8
+ - `ConvexBackendOptions` (type): type ConvexBackendOptions<TRawPresenceRow extends { actorExternalId: string }, TPresenceRow> = { client: ConvexReactClient; gameId: string; userId: string; api?: ConvexGameApi | undefined; poseTuning?: PoseSyncTuning | undefined; presence?: { functions: ConvexPresenceFunctions; mapRow: (row: TRawPre… — ⚠ undocumented
9
9
  - `ConvexChatFunctions` (interface): interface ConvexChatFunctions { messages: FunctionReference<"query">; sendMessage: FunctionReference<"mutation"> } — ⚠ undocumented · used by `createConvexChatTransport`: Wires a game's Convex chat functions into the engine's ChatTransport contract: one live query per subscribed channel (the channel's recent h…
10
- - `ConvexGameApi` (type): type ConvexGameApi = { runtime: { joinServer: FunctionReference< "mutation", "public", { gameId: string; serverId?: string; mode?: string; visibility?: "public" | "private"; joinCode?: string; externalId?: string; sessionId?: string; }, JoinServerOutcome >; leaveServer: FunctionReference<"mutation",… — ⚠ undocumented
10
+ - `ConvexGameApi` (type): type ConvexGameApi = { runtime: { joinServer: FunctionReference< "mutation", "public", { gameId: string; serverId?: string | undefined; mode?: string; visibility?: "public" | "private"; joinCode?: string; externalId?: string | undefined; sessionId?: string; }, JoinServerOutcome >; leaveServer: Funct… — ⚠ undocumented
11
11
  - `ConvexGameClient` (type): type ConvexGameClient = Pick<ConvexReactClient, "mutation" | "watchQuery"> — Structural client seam avoids requiring the engine and game to share a class instance type.
12
12
  - `ConvexGameTransportConfig` (type): type ConvexGameTransportConfig = { gameId: string; userId?: string; } — ⚠ undocumented
13
13
  - `ConvexLeaderboardReads` (type): type ConvexLeaderboardReads = ReturnType<typeof createConvexLeaderboardReads> — ⚠ undocumented
14
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
- - `ConvexRunCommandArgs` (type): type ConvexRunCommandArgs = RunCommandArgs & { 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?: LoadSnapshotScope; /** * Trusted hos… — Arguments of the `runCommand` mutation, and of the equivalent {@link GameServerHelpers.runCommand} helper.
15
+ - `ConvexRunCommandArgs` (type): type ConvexRunCommandArgs = RunCommandArgs & { externalId?: string | undefined; /** * 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?: LoadSnapshotScope; /** *… — Arguments of the `runCommand` mutation, and of the equivalent {@link GameServerHelpers.runCommand} helper.
16
16
  - `ConvexSaveBackendOptions` (interface): interface ConvexSaveBackendOptions — Config for {@link createConvexSaveBackend} — the Convex client, optional function refs (defaults to the `saves.*` convention), and an optional key `namespace`.
17
17
  - `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.
18
18
  - `DEFAULT_CONVEX_POSE_RULES` (const): const DEFAULT_CONVEX_POSE_RULES: PoseSyncRules — ⚠ undocumented
@@ -62,7 +62,7 @@
62
62
  - `randomConvexPlayerId` (function): function randomConvexPlayerId(): string — ⚠ undocumented
63
63
  - `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.
64
64
  - `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.
65
- - `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
65
+ - `resolveConvexMultiplayer` (function): function resolveConvexMultiplayer(args: { game: GameDefinition; gameId: string; url?: string | undefined; client?: ConvexReactClient; api?: ConvexGameApi; userId?: string; force?: boolean; feedActions?: string[]; poseTuning?: PoseSyncTuning; }): MultiplayerSession | null — ⚠ undocumented
66
66
  - `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.
67
67
  - `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.
68
68
  - `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
@@ -87,13 +87,13 @@
87
87
  ## @jgengine/convex/createConvexBackend
88
88
 
89
89
  - `ConvexBackend` (type): type ConvexBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = LiveGameBackend<TPresenceRow, TPresenceLocation, TGameId> & { leaderboard: ConvexLeaderboardReads; } — ⚠ undocumented
90
- - `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
90
+ - `ConvexBackendOptions` (type): type ConvexBackendOptions<TRawPresenceRow extends { actorExternalId: string }, TPresenceRow> = { client: ConvexReactClient; gameId: string; userId: string; api?: ConvexGameApi | undefined; poseTuning?: PoseSyncTuning | undefined; presence?: { functions: ConvexPresenceFunctions; mapRow: (row: TRawPre… — ⚠ undocumented
91
91
  - `ConvexLeaderboardReads` (type): type ConvexLeaderboardReads = ReturnType<typeof createConvexLeaderboardReads> — ⚠ undocumented
92
92
  - `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
93
93
 
94
94
  ## @jgengine/convex/createConvexGameTransport
95
95
 
96
- - `ConvexGameApi` (type): type ConvexGameApi = { runtime: { joinServer: FunctionReference< "mutation", "public", { gameId: string; serverId?: string; mode?: string; visibility?: "public" | "private"; joinCode?: string; externalId?: string; sessionId?: string; }, JoinServerOutcome >; leaveServer: FunctionReference<"mutation",… — ⚠ undocumented
96
+ - `ConvexGameApi` (type): type ConvexGameApi = { runtime: { joinServer: FunctionReference< "mutation", "public", { gameId: string; serverId?: string | undefined; mode?: string; visibility?: "public" | "private"; joinCode?: string; externalId?: string | undefined; sessionId?: string; }, JoinServerOutcome >; leaveServer: Funct… — ⚠ undocumented
97
97
  - `ConvexGameClient` (type): type ConvexGameClient = Pick<ConvexReactClient, "mutation" | "watchQuery"> — Structural client seam avoids requiring the engine and game to share a class instance type.
98
98
  - `ConvexGameTransportConfig` (type): type ConvexGameTransportConfig = { gameId: string; userId?: string; } — ⚠ undocumented
99
99
  - `createConvexChatSync` (function): function createConvexChatSync(client: ConvexGameClient, api: ConvexGameApi, config: ConvexGameTransportConfig, serverId: string): ChatSync — ⚠ undocumented
@@ -119,11 +119,11 @@
119
119
  ## @jgengine/convex/resolveConvexMultiplayer
120
120
 
121
121
  - `randomConvexPlayerId` (function): function randomConvexPlayerId(): string — ⚠ undocumented
122
- - `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
122
+ - `resolveConvexMultiplayer` (function): function resolveConvexMultiplayer(args: { game: GameDefinition; gameId: string; url?: string | undefined; client?: ConvexReactClient; api?: ConvexGameApi; userId?: string; force?: boolean; feedActions?: string[]; poseTuning?: PoseSyncTuning; }): MultiplayerSession | null — ⚠ undocumented
123
123
 
124
124
  ## @jgengine/convex/server
125
125
 
126
- - `ConvexRunCommandArgs` (type): type ConvexRunCommandArgs = RunCommandArgs & { 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?: LoadSnapshotScope; /** * Trusted hos… — Arguments of the `runCommand` mutation, and of the equivalent {@link GameServerHelpers.runCommand} helper.
126
+ - `ConvexRunCommandArgs` (type): type ConvexRunCommandArgs = RunCommandArgs & { externalId?: string | undefined; /** * 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?: LoadSnapshotScope; /** *… — Arguments of the `runCommand` mutation, and of the equivalent {@link GameServerHelpers.runCommand} helper.
127
127
  - `DEFAULT_CONVEX_POSE_RULES` (const): const DEFAULT_CONVEX_POSE_RULES: PoseSyncRules — ⚠ undocumented
128
128
  - `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.
129
129
  - `EnsureJoinedOutcome` (type): type EnsureJoinedOutcome = | { ok: true; serverId: GenericId<"jgGameServers">; isNew: boolean } | { ok: false; reason: "full" | "closed" | "unauthorized" } — Outcome of a join: the server joined and whether the player's profile was created, or why it was refused.
@@ -200,7 +200,7 @@
200
200
  - `createFeedWriteGate` (function): function createFeedWriteGate(allowedActions: readonly string[] = []): FeedWriteGate — ⚠ undocumented
201
201
  - `createLocalVoiceTransport` (function): function createLocalVoiceTransport(options?: { userId?: string }): { transport: VoiceTransport; participants(channelId: string): readonly VoiceParticipant[]; } — ⚠ undocumented
202
202
  - `createPoseSyncGate` (function): function createPoseSyncGate(tuning: PoseSyncTuning = DEFAULT_POSE_SYNC_TUNING): PoseSyncGate — ⚠ undocumented
203
- - `createPushToTalk` (function): function createPushToTalk(config?: { mode?: PushToTalkMode; onChange?: (transmitting: boolean) => void; }): PushToTalk — ⚠ undocumented
203
+ - `createPushToTalk` (function): function createPushToTalk(config?: { mode?: PushToTalkMode | undefined; onChange?: (transmitting: boolean) => void; }): PushToTalk — ⚠ undocumented
204
204
  - `findByJoinCode` (function): function findByJoinCode(listings: readonly SessionListing[], code: string): SessionListing | null — ⚠ undocumented
205
205
  - `fromRuntimeObjectRow` (function): function fromRuntimeObjectRow(row: RuntimeObjectRow): SceneObject — Inverse of {@link toRuntimeObjectRow}: rebuild the live placed object a host persisted.
206
206
  - `normalizeJoinCode` (function): function normalizeJoinCode(code: string): string — ⚠ undocumented
@@ -286,7 +286,7 @@
286
286
  - `DEFAULT_FEED_ACTIONS` (const): const DEFAULT_FEED_ACTIONS: string[] — ⚠ undocumented
287
287
  - `PeerShellFactories` (interface): interface PeerShellFactories — Alternate peer factories for hosts that provide their own WebRTC implementation or transport pipe.
288
288
  - `ResolvePeerShellMultiplayerArgs` (interface): interface ResolvePeerShellMultiplayerArgs — Peer bootstrap options; passing a playable hosts its authoritative GameContext world.
289
- - `ResolveShellMultiplayerArgs` (type): type ResolveShellMultiplayerArgs = { game: GameDefinition; gameId: string; url?: string | undefined; userId?: string; force?: boolean; feedActions?: string[]; } — ⚠ undocumented
289
+ - `ResolveShellMultiplayerArgs` (type): type ResolveShellMultiplayerArgs = { game: GameDefinition; gameId: string; url?: string | undefined; userId?: string | undefined; force?: boolean; feedActions?: string[]; } — ⚠ undocumented
290
290
  - `ShellMultiplayer` (type): type ShellMultiplayer = MultiplayerSession — ⚠ undocumented · used by `resolvePeerShellMultiplayer`: Opens a peer session.
291
291
  - `randomPlayerId` (function): function randomPlayerId(): string — ⚠ undocumented
292
292
  - `resolvePeerShellMultiplayer` (function): function resolvePeerShellMultiplayer(args: ResolvePeerShellMultiplayerArgs): Promise<ShellMultiplayer & { close: () => Promise<void> }> — Opens a peer session. With `playable`, one host runs the game loop and guests mirror its shared world. Await `close()` to finish authoritative persistence. Same-origin rooms use BroadcastChannel signaling.
@@ -319,7 +319,7 @@
319
319
  - `ActivePresenceResolution` (interface): interface ActivePresenceResolution<T> { active: T | null; extras: T[] } — ⚠ undocumented
320
320
  - `DEFAULT_POSE_SYNC_RULES` (const): const DEFAULT_POSE_SYNC_RULES: PoseSyncRules — Canonical client-authoritative pose-sync tuning shared by every host transport (WS, Convex).
321
321
  - `DEFAULT_SPAWN_ORIGIN` (const): const DEFAULT_SPAWN_ORIGIN: { readonly x: 0; readonly y: 0; readonly z: 0; } — ⚠ undocumented
322
- - `IncomingPose` (interface): interface IncomingPose { position: {x: number; z: number; y?: number}; rotationY?: number; rotationPitch?: number; appearance?: Record<string, string> } — ⚠ undocumented
322
+ - `IncomingPose` (interface): interface IncomingPose { position: {x: number; z: number; y?: number}; rotationY?: number; rotationPitch?: number | undefined; appearance?: Record<string, string> } — ⚠ undocumented
323
323
  - `PoseSyncDecision` (interface): interface PoseSyncDecision { position: {x: number; y: number; z: number}; rotationY: number; rotationPitch: number; appearance?: Record<string, string> | undefined; changed: boolean; refreshKeepAlive: boolean } — ⚠ undocumented
324
324
  - `PoseSyncRules` (interface): interface PoseSyncRules { maxSpeed: number; minIntervalMs?: number; maxVerticalOffset: number; floorY?: number; minElapsedSec: number; maxElapsedSec: number; keepAliveRefreshMs: number } — ⚠ undocumented · used by `DEFAULT_POSE_SYNC_RULES`: Canonical client-authoritative pose-sync tuning shared by every host transport (WS, Convex).
325
325
  - `PresencePoseState` (interface): interface PresencePoseState { position: {x: number; y: number; z: number}; rotationY: number; rotationPitch?: number; lastSeenAtMs?: number; appearance?: Record<string, string> | undefined } — ⚠ undocumented
@@ -354,7 +354,7 @@
354
354
  - `VoiceRoute` (interface): interface VoiceRoute { fromUserId: string; channelId: string; gain: number } — ⚠ undocumented
355
355
  - `VoiceTransport` (interface): interface VoiceTransport — Signaling seam for voice: who is in a channel and which media stream descriptor they published. The media plane (WebRTC, SFU, or anything else that moves audio bytes) stays behind this seam, host-supplied — the engine never touches it. subscribers delivers the channel roster on every change, starting with the current roster.
356
356
  - `createLocalVoiceTransport` (function): function createLocalVoiceTransport(options?: { userId?: string }): { transport: VoiceTransport; participants(channelId: string): readonly VoiceParticipant[]; } — ⚠ undocumented
357
- - `createPushToTalk` (function): function createPushToTalk(config?: { mode?: PushToTalkMode; onChange?: (transmitting: boolean) => void; }): PushToTalk — ⚠ undocumented
357
+ - `createPushToTalk` (function): function createPushToTalk(config?: { mode?: PushToTalkMode | undefined; onChange?: (transmitting: boolean) => void; }): PushToTalk — ⚠ undocumented
358
358
 
359
359
  ## @jgengine/core/runtime/worldProjection
360
360