@jgengine/shell 0.19.0 → 0.22.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.
Files changed (215) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +113 -15
  5. package/dist/Shell3dPresentation.d.ts +2 -2
  6. package/dist/Shell3dPresentation.js +49 -7
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +11 -1
  10. package/dist/ShellHudPresentation.js +20 -6
  11. package/dist/audio/audioEngine.d.ts +16 -0
  12. package/dist/audio/audioEngine.js +178 -89
  13. package/dist/audio/audioWire.js +2 -2
  14. package/dist/audio/synthEngine.d.ts +7 -1
  15. package/dist/audio/synthEngine.js +103 -38
  16. package/dist/camera/GameCameraRig.js +2 -1
  17. package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
  18. package/dist/camera/GameFirstPersonCamera.js +17 -13
  19. package/dist/camera/cameraRigs.js +8 -2
  20. package/dist/camera/chaseHeading.d.ts +9 -0
  21. package/dist/camera/chaseHeading.js +10 -0
  22. package/dist/camera/customMuzzle.d.ts +14 -0
  23. package/dist/camera/customMuzzle.js +68 -0
  24. package/dist/camera/index.d.ts +1 -0
  25. package/dist/camera/index.js +1 -0
  26. package/dist/camera/rigResolve.d.ts +3 -0
  27. package/dist/camera/rigResolve.js +11 -0
  28. package/dist/combat/ProjectileModels.d.ts +31 -0
  29. package/dist/combat/ProjectileModels.js +70 -0
  30. package/dist/commandSink.d.ts +1 -2
  31. package/dist/commandSink.js +3 -2
  32. package/dist/defineGame.d.ts +12 -5
  33. package/dist/defineGame.js +25 -10
  34. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  35. package/dist/devtools/DevtoolsOverlay.js +14 -168
  36. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  37. package/dist/devtools/DevtoolsRuntime.js +61 -0
  38. package/dist/devtools/agentBridge.js +1 -1
  39. package/dist/devtools/devtoolsReports.d.ts +68 -0
  40. package/dist/devtools/devtoolsReports.js +100 -0
  41. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  42. package/dist/devtools/latencyInstrumentation.js +11 -0
  43. package/dist/devtools/playtestProbe.d.ts +4 -0
  44. package/dist/devtools/playtestProbe.js +22 -0
  45. package/dist/drivers/FrameDriver.js +25 -21
  46. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  47. package/dist/drivers/HudOnlyDriver.js +32 -14
  48. package/dist/environment/EnvironmentScene.js +19 -15
  49. package/dist/environment/terrainGroundColor.d.ts +5 -4
  50. package/dist/environment/terrainGroundColor.js +19 -8
  51. package/dist/inputSink.d.ts +6 -6
  52. package/dist/inputSink.js +60 -21
  53. package/dist/lazyShell3dPresentation.d.ts +9 -0
  54. package/dist/lazyShell3dPresentation.js +24 -0
  55. package/dist/materialOverride.d.ts +42 -0
  56. package/dist/materialOverride.js +95 -36
  57. package/dist/multiplayer.d.ts +33 -3
  58. package/dist/multiplayer.js +105 -22
  59. package/dist/pointer/pointerService.d.ts +2 -0
  60. package/dist/pointer/pointerService.js +12 -2
  61. package/dist/postfx/PostProcessing.d.ts +10 -0
  62. package/dist/postfx/PostProcessing.js +9 -41
  63. package/dist/postfx/gradeShader.d.ts +1 -1
  64. package/dist/postfx/gradeShader.js +30 -2
  65. package/dist/postfx/shadowPrepass.d.ts +3 -0
  66. package/dist/postfx/shadowPrepass.js +18 -0
  67. package/dist/presentationRecovery.d.ts +75 -0
  68. package/dist/presentationRecovery.js +204 -0
  69. package/dist/presentationRecovery3d.d.ts +4 -0
  70. package/dist/presentationRecovery3d.js +32 -0
  71. package/dist/presentationResolve.d.ts +8 -1
  72. package/dist/presentationResolve.js +5 -2
  73. package/dist/render/CascadedShadows.d.ts +7 -2
  74. package/dist/render/CascadedShadows.js +98 -20
  75. package/dist/render/EnvironmentLighting.js +74 -35
  76. package/dist/render/HairCards.d.ts +7 -0
  77. package/dist/render/HairCards.js +23 -0
  78. package/dist/render/SceneModels.js +51 -77
  79. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  80. package/dist/render/authoredSurfaceMaterial.js +108 -0
  81. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  82. package/dist/render/materialAppearanceSignals.js +58 -0
  83. package/dist/render/materialAsset.d.ts +98 -0
  84. package/dist/render/materialAsset.js +454 -0
  85. package/dist/render/measureBounds.js +16 -3
  86. package/dist/render/measureCollisionMesh.js +8 -1
  87. package/dist/render/modelAssets.d.ts +8 -0
  88. package/dist/render/modelAssets.js +43 -0
  89. package/dist/render/modelBindPose.d.ts +5 -0
  90. package/dist/render/modelBindPose.js +35 -0
  91. package/dist/render/modelLoad.d.ts +3 -0
  92. package/dist/render/modelLoad.js +9 -0
  93. package/dist/render/modelNodeVisibility.d.ts +6 -0
  94. package/dist/render/modelNodeVisibility.js +18 -0
  95. package/dist/render/modelRender.d.ts +18 -1
  96. package/dist/render/modelRender.js +81 -3
  97. package/dist/render/projectedOverdraw.d.ts +35 -0
  98. package/dist/render/projectedOverdraw.js +204 -0
  99. package/dist/render/resolveModel.d.ts +12 -2
  100. package/dist/render/resolveModel.js +21 -4
  101. package/dist/render/rigNode.d.ts +26 -0
  102. package/dist/render/rigNode.js +32 -0
  103. package/dist/render/spriteRender.d.ts +4 -0
  104. package/dist/render/spriteRender.js +5 -0
  105. package/dist/render/staticShapeInstances.d.ts +34 -0
  106. package/dist/render/staticShapeInstances.js +78 -0
  107. package/dist/render/useFootIk.js +76 -11
  108. package/dist/render/useModelAnimation.d.ts +28 -2
  109. package/dist/render/useModelAnimation.js +194 -69
  110. package/dist/render/useModelInstance.d.ts +25 -0
  111. package/dist/render/useModelInstance.js +43 -0
  112. package/dist/scatter/scatterModels.d.ts +3 -2
  113. package/dist/scatter/scatterModels.js +15 -7
  114. package/dist/scene/AuthoredScene.d.ts +14 -6
  115. package/dist/scene/AuthoredScene.js +30 -9
  116. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  117. package/dist/scene/GrassFieldRenderer.js +20 -2
  118. package/dist/settings/SettingsMenu.js +11 -18
  119. package/dist/settings/SettingsRuntime.d.ts +5 -0
  120. package/dist/settings/SettingsRuntime.js +29 -2
  121. package/dist/shellControlSuspension.d.ts +18 -0
  122. package/dist/shellControlSuspension.js +28 -0
  123. package/dist/shellInputPublication.d.ts +28 -0
  124. package/dist/shellInputPublication.js +49 -0
  125. package/dist/shellPointerInput.d.ts +18 -0
  126. package/dist/shellPointerInput.js +49 -0
  127. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  128. package/dist/structures/BuildingKitBatch.js +34 -59
  129. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  130. package/dist/structures/BuildingRenderBudget.js +54 -0
  131. package/dist/structures/GeneratedBuilding.js +13 -8
  132. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  133. package/dist/structures/buildingKitMaterials.js +70 -0
  134. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  135. package/dist/structures/buildingSpatialBatch.js +45 -0
  136. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  137. package/dist/terrain/CarvedTerrain.js +2 -2
  138. package/dist/terrain/GrassField.d.ts +3 -1
  139. package/dist/terrain/GrassField.js +61 -6
  140. package/dist/terrain/grassGeometry.d.ts +13 -0
  141. package/dist/terrain/grassGeometry.js +57 -1
  142. package/dist/terrain/grassMaterial.js +2 -2
  143. package/dist/terrain/index.d.ts +1 -0
  144. package/dist/terrain/index.js +1 -0
  145. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  146. package/dist/terrain/terrainDetailMaterial.js +7 -7
  147. package/dist/terrain/terrainMath.d.ts +2 -0
  148. package/dist/terrain/terrainMath.js +2 -0
  149. package/dist/terrain/terrainPaintColor.d.ts +11 -0
  150. package/dist/terrain/terrainPaintColor.js +47 -0
  151. package/dist/touch/TouchControlsOverlay.js +4 -4
  152. package/dist/useShellMultiplayerSync.js +4 -0
  153. package/dist/vfx/ParticleField.d.ts +23 -20
  154. package/dist/vfx/ParticleField.js +168 -50
  155. package/dist/visibility/CullingProvider.js +2 -62
  156. package/dist/visibility/cullingDriver.d.ts +9 -0
  157. package/dist/visibility/cullingDriver.js +68 -0
  158. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  159. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  160. package/dist/weather/DustField.d.ts +2 -1
  161. package/dist/weather/DustField.js +17 -3
  162. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  163. package/dist/weather/FireSpreadLayer.js +5 -11
  164. package/dist/weather/LightningStrike.d.ts +4 -1
  165. package/dist/weather/LightningStrike.js +13 -17
  166. package/dist/weather/RainField.d.ts +2 -1
  167. package/dist/weather/RainField.js +17 -3
  168. package/dist/weather/RainImpactField.d.ts +16 -0
  169. package/dist/weather/RainImpactField.js +89 -0
  170. package/dist/weather/SnowField.d.ts +2 -1
  171. package/dist/weather/SnowField.js +17 -3
  172. package/dist/weather/WeatherLayer.d.ts +11 -2
  173. package/dist/weather/WeatherLayer.js +30 -6
  174. package/dist/weather/index.d.ts +2 -0
  175. package/dist/weather/index.js +2 -0
  176. package/dist/weather/weatherMath.d.ts +2 -0
  177. package/dist/weather/weatherMath.js +8 -0
  178. package/dist/weather/weatherShelter.d.ts +18 -0
  179. package/dist/weather/weatherShelter.js +75 -0
  180. package/dist/weather/weatherUniforms.d.ts +40 -1
  181. package/dist/weather/weatherUniforms.js +29 -9
  182. package/dist/world/AuthoredFlocks.d.ts +22 -0
  183. package/dist/world/AuthoredFlocks.js +87 -0
  184. package/dist/world/StaticObjectModels.d.ts +8 -0
  185. package/dist/world/StaticObjectModels.js +75 -0
  186. package/dist/world/WorldHud.d.ts +14 -6
  187. package/dist/world/WorldHud.js +25 -12
  188. package/dist/world/WorldParticles.d.ts +10 -23
  189. package/dist/world/WorldParticles.js +209 -61
  190. package/dist/world/WorldScene.d.ts +3 -1
  191. package/dist/world/WorldScene.js +41 -26
  192. package/dist/world/staticObjectBatches.d.ts +35 -0
  193. package/dist/world/staticObjectBatches.js +163 -0
  194. package/dist/world/worldBarSamples.d.ts +37 -33
  195. package/dist/world/worldBarSamples.js +117 -75
  196. package/package.json +4 -4
  197. package/skills/ce-handoff/SKILL.md +1 -1
  198. package/skills/game-design/SKILL.md +2 -0
  199. package/skills/jgengine/SKILL.md +23 -2
  200. package/skills/jgengine/api.md +28 -17
  201. package/skills/jgengine/recipes/minimal-game.md +7 -2
  202. package/skills/jgengine-ui/SKILL.md +3 -1
  203. package/skills/jgengine-ui/api.md +219 -70
  204. package/skills/jgengine-ui/capabilities.md +119 -1
  205. package/skills/jgengine-ui/reference-primitives.md +93 -3
  206. package/skills/jgengine-ui/reference.md +2 -0
  207. package/skills/jgengine-world/SKILL.md +12 -3
  208. package/skills/jgengine-world/api.md +201 -58
  209. package/skills/jgengine-world/capabilities.md +116 -3
  210. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  211. package/skills/jgengine-world/recipes/controls.md +4 -0
  212. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  213. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  214. package/skills/jgengine-world/reference.md +167 -5
  215. package/skills/jgengine-world/visibility.md +28 -1
@@ -1,14 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useFrame, useThree } from "@react-three/fiber";
3
3
  import { useEffect, useRef, useState, useSyncExternalStore } from "react";
4
- import { devtools, instrumentLatency, } from "@jgengine/core/devtools/devtools";
5
- import { fallbackSeamsSnapshot } from "@jgengine/core/devtools/fallbackSeams";
6
- import { textureErrorsSnapshot } from "@jgengine/core/devtools/textureErrors";
7
- import { MOVEMENT_TUNING } from "@jgengine/core/movement/movementModel";
4
+ import { devtools, } from "@jgengine/core/devtools/devtools";
8
5
  import { heavyModels } from "../render/modelLoad.js";
9
- import { collisionDebug } from "./collisionDebug.js";
10
- import { readStoredOverrides } from "./devtoolsOverrides.js";
11
- import { diagnose } from "./perfDiagnose.js";
6
+ import { buildLeanReport } from "./devtoolsReports.js";
12
7
  import { PerfPanel } from "./PerfPanel.js";
13
8
  import { LogsPanel } from "./LogsPanel.js";
14
9
  import { NetPanel } from "./NetPanel.js";
@@ -17,20 +12,12 @@ import { ColPanel } from "./ColPanel.js";
17
12
  import { TunePanel } from "./TunePanel.js";
18
13
  export { applyStoredDevtoolsOverrides, persistDevtoolsOverrides } from "./devtoolsOverrides.js";
19
14
  const REFRESH_MS = 250;
20
- /** @internal */
21
- export function withDevtoolsLatency(multiplayer) {
22
- return {
23
- ...multiplayer,
24
- backend: {
25
- ...instrumentLatency(multiplayer.backend, ["pushFeedEntry"]),
26
- transport: instrumentLatency(multiplayer.backend.transport, ["joinServer", "leaveServer", "runCommand"]),
27
- },
28
- };
29
- }
15
+ export { withDevtoolsLatency } from "./latencyInstrumentation.js";
30
16
  const RENDER_SAMPLE_MS = 500;
31
17
  const RENDER_PROBE_FRAME_PRIORITY = -10;
32
18
  /** @internal */
33
19
  export function DevtoolsRendererProbe() {
20
+ useEffect(() => devtools.probes.register("heavyModels", () => heavyModels()), []);
34
21
  const gl = useThree((state) => state.gl);
35
22
  const lastSampleAt = useRef(0);
36
23
  useEffect(() => {
@@ -66,166 +53,25 @@ const TABS = [
66
53
  { id: "net", label: "Net" },
67
54
  { id: "keys", label: "Keys" },
68
55
  ];
69
- function roundMs(value, digits = 1) {
70
- const scale = 10 ** digits;
71
- return Math.round(value * scale) / scale;
72
- }
73
- function compactLongFrame(event) {
74
- return {
75
- at: event.at,
76
- frameMs: roundMs(event.frameMs),
77
- simMs: roundMs(event.simMs),
78
- outsideMs: roundMs(event.outsideMs),
79
- culprit: event.culprit,
80
- reason: event.reason,
81
- phases: event.phases.slice(0, 4).map((phase) => ({
82
- name: phase.name,
83
- ms: roundMs(phase.ms),
84
- })),
85
- probes: event.probes,
86
- render: event.render,
87
- };
88
- }
89
- function summarizeLongFrames(events) {
90
- if (events.length === 0)
91
- return null;
92
- const byCulprit = new Map();
93
- let maxFrameMs = 0;
94
- let maxDraws = 0;
95
- let maxGeometries = 0;
96
- for (const event of events) {
97
- byCulprit.set(event.culprit, (byCulprit.get(event.culprit) ?? 0) + 1);
98
- maxFrameMs = Math.max(maxFrameMs, event.frameMs);
99
- if (event.render !== null) {
100
- maxDraws = Math.max(maxDraws, event.render.drawCalls);
101
- maxGeometries = Math.max(maxGeometries, event.render.geometries);
102
- }
103
- }
104
- const culprits = [...byCulprit.entries()]
105
- .sort((a, b) => b[1] - a[1])
106
- .map(([culprit, count]) => ({ culprit, count }));
107
- return {
108
- count: events.length,
109
- culprits,
110
- maxFrameMs: roundMs(maxFrameMs),
111
- maxDrawCalls: maxDraws > 0 ? maxDraws : undefined,
112
- maxGeometries: maxGeometries > 0 ? maxGeometries : undefined,
113
- };
114
- }
115
- /** @internal */
116
- export function buildLeanReport(playable) {
117
- const snap = devtools.snapshot();
118
- const frame = snap.frame;
119
- const longs = snap.longFrames;
120
- const why = frame !== null ? diagnose(frame, longs) : null;
121
- const changedControls = snap.controls
122
- .filter((control) => !Object.is(control.value, control.initial))
123
- .map((control) => ({ name: control.name, value: control.value, initial: control.initial }));
124
- const enabledDiscovered = snap.discovered
125
- .filter((entry) => entry.enabled)
126
- .map((entry) => ({ id: entry.id, value: entry.value }));
127
- const hotLogs = snap.logs.filter((entry) => entry.level === "warn" || entry.level === "error").slice(-20);
128
- return {
129
- game: playable.game.name,
130
- at: snap.at,
131
- why,
132
- frame: frame === null
133
- ? null
134
- : {
135
- fps: roundMs(frame.fps, 1),
136
- avgFrameMs: roundMs(frame.avgFrameMs),
137
- p95FrameMs: roundMs(frame.p95FrameMs),
138
- maxFrameMs: roundMs(frame.maxFrameMs),
139
- avgSimMs: roundMs(frame.avgSimMs),
140
- maxSimMs: roundMs(frame.maxSimMs),
141
- avgOutsideMs: roundMs(frame.avgOutsideMs),
142
- maxOutsideMs: roundMs(frame.maxOutsideMs),
143
- longFrames: frame.longFrames,
144
- samples: frame.samples,
145
- phases: frame.phases.slice(0, 8).map((phase) => ({
146
- name: phase.name,
147
- avgMs: roundMs(phase.avgMs),
148
- maxMs: roundMs(phase.maxMs),
149
- pctOfSim: Math.round(phase.pctOfSim),
150
- })),
151
- },
152
- render: snap.render,
153
- latency: snap.latency,
154
- longFrameSummary: summarizeLongFrames(longs),
155
- longFrames: longs.slice(-6).map(compactLongFrame),
156
- probes: snap.probes,
157
- logs: hotLogs.length > 0 ? hotLogs : undefined,
158
- controls: changedControls.length > 0 ? changedControls : undefined,
159
- discovered: enabledDiscovered.length > 0 ? enabledDiscovered : undefined,
160
- discoveredCount: snap.discovered.length,
161
- };
162
- }
163
- /** @internal */
164
- export function buildFullReport(playable) {
165
- return { game: playable.game.name, ...devtools.snapshot() };
166
- }
56
+ export { buildLeanReport, buildFullReport } from "./devtoolsReports.js";
167
57
  /** @internal */
168
58
  export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
169
59
  const [tab, setTab] = useState("perf");
170
60
  const [, setTick] = useState(0);
171
61
  const [copied, setCopied] = useState(false);
172
62
  useSyncExternalStore(devtools.signal.subscribe, devtools.signal.version, devtools.signal.version);
173
- useEffect(() => {
174
- devtools.logs.captureConsole();
175
- globalThis.__JG_DEVTOOLS = {
176
- snapshot: () => buildLeanReport(playable),
177
- snapshotFull: () => buildFullReport(playable),
178
- controls: devtools.controls,
179
- discover: devtools.discover,
180
- frame: devtools.frame,
181
- profile: devtools.profile,
182
- collisionDebug,
183
- };
184
- }, [playable]);
185
- useEffect(() => {
186
- devtools.discover.scanTable("game", playable);
187
- devtools.discover.scanTable("engine", { movement: MOVEMENT_TUNING });
188
- }, [playable]);
189
- useEffect(() => {
190
- const stored = readStoredOverrides(playable.game.name);
191
- if (stored === null)
192
- return;
193
- const appliedEnables = new Set();
194
- const appliedValues = new Set();
195
- const applyLateRegistrations = () => {
196
- const discovered = new Map(devtools.discover.list().map((entry) => [entry.id, entry]));
197
- const needEnable = stored.enabled.filter((id) => !appliedEnables.has(id) && discovered.get(id)?.enabled === false);
198
- const needValue = Object.entries(stored.values).filter(([name]) => !appliedValues.has(name) && devtools.controls.get(name) !== null);
199
- if (needEnable.length === 0 && needValue.length === 0)
200
- return;
201
- for (const id of needEnable)
202
- appliedEnables.add(id);
203
- for (const [name] of needValue)
204
- appliedValues.add(name);
205
- devtools.overrides.apply({ enabled: needEnable, values: Object.fromEntries(needValue) });
206
- };
207
- applyLateRegistrations();
208
- return devtools.signal.subscribe(applyLateRegistrations);
209
- }, [playable]);
210
- useEffect(() => {
211
- const disposers = [
212
- devtools.probes.register("entities", () => ctx.scene.entity.list().length),
213
- devtools.probes.register("objects", () => ctx.scene.object.list().length),
214
- devtools.probes.register("fallbacks", () => fallbackSeamsSnapshot()),
215
- devtools.probes.register("textureErrors", () => textureErrorsSnapshot()),
216
- devtools.probes.register("heavyModels", () => heavyModels()),
217
- ];
218
- return () => {
219
- for (const dispose of disposers)
220
- dispose();
221
- };
222
- }, [ctx]);
223
63
  useEffect(() => {
224
64
  if (!open)
225
65
  return;
226
- const interval = setInterval(() => setTick((current) => current + 1), REFRESH_MS);
227
- return () => clearInterval(interval);
228
- }, [open]);
66
+ let lastRefresh = 0;
67
+ return ctx.subscribe(() => {
68
+ const now = performance.now();
69
+ if (now - lastRefresh < REFRESH_MS)
70
+ return;
71
+ lastRefresh = now;
72
+ setTick((current) => current + 1);
73
+ });
74
+ }, [ctx, open]);
229
75
  if (!open)
230
76
  return null;
231
77
  const copyReport = () => {
@@ -0,0 +1,7 @@
1
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
2
+ import type { PlayableGame } from "../registry.js";
3
+ /** Keeps saved overrides and diagnostic registration live while the visual panel is closed. @internal */
4
+ export declare function DevtoolsRuntime({ ctx, playable }: {
5
+ ctx: GameContext;
6
+ playable: PlayableGame;
7
+ }): null;
@@ -0,0 +1,61 @@
1
+ import { useEffect } from "react";
2
+ import { devtools } from "@jgengine/core/devtools/devtools";
3
+ import { fallbackSeamsSnapshot } from "@jgengine/core/devtools/fallbackSeams";
4
+ import { textureErrorsSnapshot } from "@jgengine/core/devtools/textureErrors";
5
+ import { MOVEMENT_TUNING } from "@jgengine/core/movement/movementModel";
6
+ import { collisionDebug } from "./collisionDebug.js";
7
+ import { readStoredOverrides } from "./devtoolsOverrides.js";
8
+ import { buildLeanReport, buildFullReport } from "./devtoolsReports.js";
9
+ /** Keeps saved overrides and diagnostic registration live while the visual panel is closed. @internal */
10
+ export function DevtoolsRuntime({ ctx, playable }) {
11
+ useEffect(() => {
12
+ devtools.logs.captureConsole();
13
+ globalThis.__JG_DEVTOOLS = {
14
+ snapshot: () => buildLeanReport(playable),
15
+ snapshotFull: () => buildFullReport(playable),
16
+ controls: devtools.controls,
17
+ discover: devtools.discover,
18
+ frame: devtools.frame,
19
+ profile: devtools.profile,
20
+ collisionDebug,
21
+ };
22
+ }, [playable]);
23
+ useEffect(() => {
24
+ devtools.discover.scanTable("game", playable);
25
+ devtools.discover.scanTable("engine", { movement: MOVEMENT_TUNING });
26
+ }, [playable]);
27
+ useEffect(() => {
28
+ const stored = readStoredOverrides(playable.game.name);
29
+ if (stored === null)
30
+ return;
31
+ const appliedEnables = new Set();
32
+ const appliedValues = new Set();
33
+ const applyLateRegistrations = () => {
34
+ const discovered = new Map(devtools.discover.list().map((entry) => [entry.id, entry]));
35
+ const needEnable = stored.enabled.filter((id) => !appliedEnables.has(id) && discovered.get(id)?.enabled === false);
36
+ const needValue = Object.entries(stored.values).filter(([name]) => !appliedValues.has(name) && devtools.controls.get(name) !== null);
37
+ if (needEnable.length === 0 && needValue.length === 0)
38
+ return;
39
+ for (const id of needEnable)
40
+ appliedEnables.add(id);
41
+ for (const [name] of needValue)
42
+ appliedValues.add(name);
43
+ devtools.overrides.apply({ enabled: needEnable, values: Object.fromEntries(needValue) });
44
+ };
45
+ applyLateRegistrations();
46
+ return devtools.signal.subscribe(applyLateRegistrations);
47
+ }, [playable]);
48
+ useEffect(() => {
49
+ const disposers = [
50
+ devtools.probes.register("entities", () => ctx.scene.entity.list().length),
51
+ devtools.probes.register("objects", () => ctx.scene.object.list().length),
52
+ devtools.probes.register("fallbacks", () => fallbackSeamsSnapshot()),
53
+ devtools.probes.register("textureErrors", () => textureErrorsSnapshot()),
54
+ ];
55
+ return () => {
56
+ for (const dispose of disposers)
57
+ dispose();
58
+ };
59
+ }, [ctx]);
60
+ return null;
61
+ }
@@ -2,7 +2,7 @@ import { devtools } from "@jgengine/core/devtools/devtools";
2
2
  import { getSaveEndpoint } from "@jgengine/core/devtools/saveEndpoint";
3
3
  import { isHudAnchor, listActiveHudLayouts, } from "@jgengine/core/ui/hudLayout";
4
4
  import { listHudPanelTypes, resolvePanelResize, resizePanelSize, } from "@jgengine/core/ui/hudDocument";
5
- import { buildFullReport, buildLeanReport } from "./DevtoolsOverlay.js";
5
+ import { buildFullReport, buildLeanReport } from "./devtoolsReports.js";
6
6
  /**
7
7
  * @internal
8
8
  * Resolves true once `isLive()` reports a mounted editor host, polling every `intervalMs` up to
@@ -0,0 +1,68 @@
1
+ import { type DevtoolsSnapshot } from "@jgengine/core/devtools/devtools";
2
+ import type { PlayableGame } from "../registry.js";
3
+ /** @internal */
4
+ export declare function buildLeanReport(playable: PlayableGame): {
5
+ game: string;
6
+ at: number;
7
+ why: string | null;
8
+ frame: {
9
+ fps: number;
10
+ avgFrameMs: number;
11
+ p95FrameMs: number;
12
+ maxFrameMs: number;
13
+ avgSimMs: number;
14
+ maxSimMs: number;
15
+ avgOutsideMs: number;
16
+ maxOutsideMs: number;
17
+ longFrames: number;
18
+ samples: number;
19
+ phases: {
20
+ name: string;
21
+ avgMs: number;
22
+ maxMs: number;
23
+ pctOfSim: number;
24
+ }[];
25
+ } | null;
26
+ render: import("@jgengine/core/devtools/types").RenderSample | null;
27
+ latency: import("@jgengine/core/devtools/types").LatencyStats | null;
28
+ longFrameSummary: {
29
+ count: number;
30
+ culprits: {
31
+ culprit: string;
32
+ count: number;
33
+ }[];
34
+ maxFrameMs: number;
35
+ maxDrawCalls: number | undefined;
36
+ maxGeometries: number | undefined;
37
+ } | null;
38
+ longFrames: {
39
+ at: number;
40
+ frameMs: number;
41
+ simMs: number;
42
+ outsideMs: number;
43
+ culprit: string;
44
+ reason: string;
45
+ phases: {
46
+ name: string;
47
+ ms: number;
48
+ }[];
49
+ probes: Record<string, unknown>;
50
+ render: import("@jgengine/core/devtools/types").RenderSample | null;
51
+ }[];
52
+ probes: Record<string, unknown>;
53
+ logs: import("@jgengine/core/devtools/types").DevtoolsLogEntry[] | undefined;
54
+ controls: {
55
+ name: string;
56
+ value: unknown;
57
+ initial: unknown;
58
+ }[] | undefined;
59
+ discovered: {
60
+ id: string;
61
+ value: unknown;
62
+ }[] | undefined;
63
+ discoveredCount: number;
64
+ };
65
+ /** @internal */
66
+ export declare function buildFullReport(playable: PlayableGame): DevtoolsSnapshot & {
67
+ game: string;
68
+ };
@@ -0,0 +1,100 @@
1
+ import { devtools } from "@jgengine/core/devtools/devtools";
2
+ import { diagnose } from "./perfDiagnose.js";
3
+ function roundMs(value, digits = 1) {
4
+ const scale = 10 ** digits;
5
+ return Math.round(value * scale) / scale;
6
+ }
7
+ function compactLongFrame(event) {
8
+ return {
9
+ at: event.at,
10
+ frameMs: roundMs(event.frameMs),
11
+ simMs: roundMs(event.simMs),
12
+ outsideMs: roundMs(event.outsideMs),
13
+ culprit: event.culprit,
14
+ reason: event.reason,
15
+ phases: event.phases.slice(0, 4).map((phase) => ({
16
+ name: phase.name,
17
+ ms: roundMs(phase.ms),
18
+ })),
19
+ probes: event.probes,
20
+ render: event.render,
21
+ };
22
+ }
23
+ function summarizeLongFrames(events) {
24
+ if (events.length === 0)
25
+ return null;
26
+ const byCulprit = new Map();
27
+ let maxFrameMs = 0;
28
+ let maxDraws = 0;
29
+ let maxGeometries = 0;
30
+ for (const event of events) {
31
+ byCulprit.set(event.culprit, (byCulprit.get(event.culprit) ?? 0) + 1);
32
+ maxFrameMs = Math.max(maxFrameMs, event.frameMs);
33
+ if (event.render !== null) {
34
+ maxDraws = Math.max(maxDraws, event.render.drawCalls);
35
+ maxGeometries = Math.max(maxGeometries, event.render.geometries);
36
+ }
37
+ }
38
+ const culprits = [...byCulprit.entries()]
39
+ .sort((a, b) => b[1] - a[1])
40
+ .map(([culprit, count]) => ({ culprit, count }));
41
+ return {
42
+ count: events.length,
43
+ culprits,
44
+ maxFrameMs: roundMs(maxFrameMs),
45
+ maxDrawCalls: maxDraws > 0 ? maxDraws : undefined,
46
+ maxGeometries: maxGeometries > 0 ? maxGeometries : undefined,
47
+ };
48
+ }
49
+ /** @internal */
50
+ export function buildLeanReport(playable) {
51
+ const snap = devtools.snapshot();
52
+ const frame = snap.frame;
53
+ const longs = snap.longFrames;
54
+ const why = frame !== null ? diagnose(frame, longs) : null;
55
+ const changedControls = snap.controls
56
+ .filter((control) => !Object.is(control.value, control.initial))
57
+ .map((control) => ({ name: control.name, value: control.value, initial: control.initial }));
58
+ const enabledDiscovered = snap.discovered
59
+ .filter((entry) => entry.enabled)
60
+ .map((entry) => ({ id: entry.id, value: entry.value }));
61
+ const hotLogs = snap.logs.filter((entry) => entry.level === "warn" || entry.level === "error").slice(-20);
62
+ return {
63
+ game: playable.game.name,
64
+ at: snap.at,
65
+ why,
66
+ frame: frame === null
67
+ ? null
68
+ : {
69
+ fps: roundMs(frame.fps, 1),
70
+ avgFrameMs: roundMs(frame.avgFrameMs),
71
+ p95FrameMs: roundMs(frame.p95FrameMs),
72
+ maxFrameMs: roundMs(frame.maxFrameMs),
73
+ avgSimMs: roundMs(frame.avgSimMs),
74
+ maxSimMs: roundMs(frame.maxSimMs),
75
+ avgOutsideMs: roundMs(frame.avgOutsideMs),
76
+ maxOutsideMs: roundMs(frame.maxOutsideMs),
77
+ longFrames: frame.longFrames,
78
+ samples: frame.samples,
79
+ phases: frame.phases.slice(0, 8).map((phase) => ({
80
+ name: phase.name,
81
+ avgMs: roundMs(phase.avgMs),
82
+ maxMs: roundMs(phase.maxMs),
83
+ pctOfSim: Math.round(phase.pctOfSim),
84
+ })),
85
+ },
86
+ render: snap.render,
87
+ latency: snap.latency,
88
+ longFrameSummary: summarizeLongFrames(longs),
89
+ longFrames: longs.slice(-6).map(compactLongFrame),
90
+ probes: snap.probes,
91
+ logs: hotLogs.length > 0 ? hotLogs : undefined,
92
+ controls: changedControls.length > 0 ? changedControls : undefined,
93
+ discovered: enabledDiscovered.length > 0 ? enabledDiscovered : undefined,
94
+ discoveredCount: snap.discovered.length,
95
+ };
96
+ }
97
+ /** @internal */
98
+ export function buildFullReport(playable) {
99
+ return { game: playable.game.name, ...devtools.snapshot() };
100
+ }
@@ -0,0 +1,3 @@
1
+ import type { ShellMultiplayer } from "../multiplayer.js";
2
+ /** @internal */
3
+ export declare function withDevtoolsLatency(multiplayer: ShellMultiplayer): ShellMultiplayer;
@@ -0,0 +1,11 @@
1
+ import { instrumentLatency } from "@jgengine/core/devtools/devtools";
2
+ /** @internal */
3
+ export function withDevtoolsLatency(multiplayer) {
4
+ return {
5
+ ...multiplayer,
6
+ backend: {
7
+ ...instrumentLatency(multiplayer.backend, ["pushFeedEntry"]),
8
+ transport: instrumentLatency(multiplayer.backend.transport, ["joinServer", "leaveServer", "runCommand"]),
9
+ },
10
+ };
11
+ }
@@ -0,0 +1,4 @@
1
+ type Probe = () => Record<string, number>;
2
+ /** @internal Attach a live capture probe for one shell boot; cleanup retires retained readers. */
3
+ export declare function attachPlaytestProbe(host: object, read: Probe): () => void;
4
+ export {};
@@ -0,0 +1,22 @@
1
+ /** @internal Attach a live capture probe for one shell boot; cleanup retires retained readers. */
2
+ export function attachPlaytestProbe(host, read) {
3
+ const target = host;
4
+ let active = true;
5
+ const probe = () => {
6
+ if (!active)
7
+ return {};
8
+ try {
9
+ const value = read();
10
+ return value !== null && typeof value === "object" ? value : {};
11
+ }
12
+ catch {
13
+ return {};
14
+ }
15
+ };
16
+ target.__jgProbe = probe;
17
+ return () => {
18
+ active = false;
19
+ if (target.__jgProbe === probe)
20
+ delete target.__jgProbe;
21
+ };
22
+ }
@@ -29,12 +29,12 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
29
29
  const repeatFiredAtRef = useRef(new Map());
30
30
  const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
31
31
  const hotbarId = useMemo(() => hotbarIdFor(playable), [playable]);
32
- const movementTuning = useMemo(() => resolvePlayerMovementTuning({
33
- collision: playable.collision,
34
- movement: playable.movement,
35
- physics: playable.game.physics,
36
- world: playable.game.world,
37
- }), [playable]);
32
+ const movementTuning = useMemo(() => ({ ...resolvePlayerMovementTuning({
33
+ collision: playable.collision,
34
+ movement: playable.movement,
35
+ physics: playable.game.physics,
36
+ world: playable.game.world,
37
+ }), authoritativeStep: true }), [playable]);
38
38
  const autoPickupRadius = useMemo(() => {
39
39
  const cfg = playable.worldItem?.autoPickup;
40
40
  if (cfg === undefined || cfg === false)
@@ -106,7 +106,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
106
106
  inputSink.send(frame);
107
107
  };
108
108
  if (gateRef.current || !playControlsActive(ctx)) {
109
- ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
109
+ ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS), { reset: true });
110
110
  ctx.input.publishAnalog(null);
111
111
  sendInput();
112
112
  return;
@@ -115,7 +115,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
115
115
  try {
116
116
  let endPhase = devtools.profile.begin("time+input");
117
117
  const dt = Math.min(rawDt, 0.05);
118
- ctx.input.publish(heldActionsFor(tracker, tracker.actions()));
118
+ ctx.input.publish(heldActionsFor(tracker, tracker.actions()), {});
119
119
  ctx.input.publishPointer(pointerAxisRef.current);
120
120
  ctx.input.publishAnalog(analogRef.current);
121
121
  sendInput();
@@ -124,32 +124,35 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
124
124
  yawRef.current = steerYaw(yawRef.current, turnInput, TURN_SPEED, dt);
125
125
  endPhase();
126
126
  const playerId = ctx.player.possession.active(ctx.player.userId);
127
- ctx.sim.advance(dt, (stepDt) => {
128
- const gameDt = ctx.time.advance(stepDt);
129
- ctx.sim.runStages("beforeMovement", stepDt);
127
+ ctx.sim.advance(dt, (stepDt, _tick, gameDt) => {
128
+ ctx.input.beginStep();
129
+ const movementDt = serverAuthoritative ? stepDt : gameDt;
130
+ if (!serverAuthoritative)
131
+ ctx.sim.runStages("beforeMovement", stepDt);
130
132
  const player = ctx.scene.entity.get(playerId);
131
133
  // Server-authoritative sessions still run the deterministic local step as a prediction; the
132
134
  // replicated pose is the confirmation that the transport reconciles on the next world diff.
133
- if (player !== null && drivesPose) {
135
+ if (player !== null && drivesPose && movementDt > 0) {
134
136
  const endPose = devtools.profile.begin("pose");
135
- stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog() }, stepDt, movementTuning, yawRef.current, pitchRef.current);
137
+ stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog() }, movementDt, movementTuning, yawRef.current, pitchRef.current);
136
138
  const predicted = ctx.scene.entity.get(playerId);
137
139
  if (predicted !== null) {
138
- predictionRef.current.record({ held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog(), tick: ctx.sim.tick() }, stepDt, predicted.position);
140
+ predictionRef.current.record({ held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog(), tick: ctx.sim.tick() }, movementDt, predicted.position);
139
141
  }
140
142
  endPose();
141
143
  }
142
- if (drivesPose && !serverAuthoritative) {
144
+ if (drivesPose && !serverAuthoritative && movementDt > 0) {
143
145
  const table = localPlayers(ctx);
144
146
  const seats = table.slots();
145
147
  for (let index = 1; index < seats.length; index += 1) {
146
148
  const seat = table.local(seats[index].slotId);
147
149
  if (seat === null)
148
150
  continue;
149
- stepPlayerMovement(ctx, seat.userId, { held: seat.input.held(), pointer: null, analog: seat.input.analog() }, stepDt, movementTuning, seatCameraYaw(ctx, seat.userId));
151
+ stepPlayerMovement(ctx, seat.userId, { held: seat.input.held(), pointer: null, analog: seat.input.analog() }, movementDt, movementTuning, seatCameraYaw(ctx, seat.userId));
150
152
  }
151
153
  }
152
- ctx.sim.runStages("afterMovement", stepDt);
154
+ if (!serverAuthoritative)
155
+ ctx.sim.runStages("afterMovement", stepDt);
153
156
  if (autoPickupRadius !== null && !serverAuthoritative) {
154
157
  const endPickup = devtools.profile.begin("pickup");
155
158
  const self = ctx.scene.entity.get(playerId);
@@ -166,18 +169,19 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
166
169
  });
167
170
  advanceBehaviors(ctx, gameDt);
168
171
  }
169
- ctx.sim.runStages("afterTick", stepDt);
170
- });
172
+ if (!serverAuthoritative)
173
+ ctx.sim.runStages("afterTick", stepDt);
174
+ }, { advanceTime: !serverAuthoritative });
171
175
  endPhase = devtools.profile.begin("actions");
172
176
  if (tracker.wasPressed("tabTarget")) {
173
177
  if (ctx.game.commands.has("target.cycle"))
174
- ctx.game.commands.run("target.cycle", {});
178
+ commandSink.run("target.cycle", {});
175
179
  else
176
180
  ctx.scene.entity.cycleTarget(playerId, { filter: "hostile" });
177
181
  }
178
182
  if (tracker.wasPressed("clearTarget")) {
179
183
  if (ctx.game.commands.has("target.clear"))
180
- ctx.game.commands.run("target.clear", {});
184
+ commandSink.run("target.clear", {});
181
185
  else
182
186
  ctx.scene.entity.setTarget(playerId, null);
183
187
  }
@@ -1,9 +1,14 @@
1
1
  import type { GameContext } from "@jgengine/core/runtime/gameContext";
2
2
  import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
3
3
  import type { ActionStateTracker } from "@jgengine/core/input/actionBindings";
4
+ import type { ShellMultiplayer } from "../multiplayer.js";
4
5
  import type { PlayableGame } from "../registry.js";
5
- export declare function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, }: {
6
+ export declare function HudOnlyDriver({ ctx, multiplayer, serverIdRef, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, }: {
6
7
  ctx: GameContext;
8
+ multiplayer: ShellMultiplayer | null;
9
+ serverIdRef: {
10
+ current: string | null;
11
+ };
7
12
  playable: PlayableGame;
8
13
  tracker: ActionStateTracker<string>;
9
14
  pointerAxisRef: {