@jgengine/core 0.18.1 → 0.21.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 (404) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/ai/decisionGraph.d.ts +50 -4
  3. package/dist/ai/decisionGraph.js +159 -23
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/ai/groupAssist.d.ts +10 -0
  7. package/dist/ai/groupAssist.js +11 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/threat.d.ts +8 -0
  11. package/dist/ai/threat.js +10 -0
  12. package/dist/anim/animGraph.d.ts +43 -1
  13. package/dist/anim/animGraph.js +326 -10
  14. package/dist/anim/boneTexture.d.ts +55 -0
  15. package/dist/anim/boneTexture.js +43 -0
  16. package/dist/anim/footPlacement.d.ts +57 -0
  17. package/dist/anim/footPlacement.js +67 -0
  18. package/dist/anim/locomotionGraph.d.ts +18 -1
  19. package/dist/anim/locomotionGraph.js +42 -3
  20. package/dist/audio/audioFalloff.d.ts +6 -0
  21. package/dist/audio/doppler.d.ts +21 -0
  22. package/dist/audio/doppler.js +32 -0
  23. package/dist/audio/engineLayers.d.ts +83 -0
  24. package/dist/audio/engineLayers.js +115 -0
  25. package/dist/audio/voiceAllocator.d.ts +46 -0
  26. package/dist/audio/voiceAllocator.js +76 -0
  27. package/dist/combat/abilityKit.d.ts +20 -0
  28. package/dist/combat/abilityKit.js +41 -4
  29. package/dist/combat/death.d.ts +11 -3
  30. package/dist/combat/death.js +3 -3
  31. package/dist/combat/magazine.d.ts +9 -0
  32. package/dist/combat/magazine.js +24 -2
  33. package/dist/combat/projectiles.d.ts +94 -2
  34. package/dist/combat/projectiles.js +282 -61
  35. package/dist/combat/regenShield.d.ts +14 -0
  36. package/dist/combat/regenShield.js +23 -7
  37. package/dist/combat/shotOrigin.d.ts +6 -0
  38. package/dist/combat/shotOrigin.js +6 -0
  39. package/dist/combat/weaponFeedback.d.ts +36 -0
  40. package/dist/combat/weaponFeedback.js +42 -0
  41. package/dist/combat/weaponHandling.d.ts +161 -0
  42. package/dist/combat/weaponHandling.js +202 -0
  43. package/dist/combat/weaponPresentation.d.ts +115 -0
  44. package/dist/combat/weaponPresentation.js +96 -0
  45. package/dist/crafting/recipeCatalog.d.ts +16 -0
  46. package/dist/crafting/recipeCatalog.js +55 -0
  47. package/dist/devtools/warnOnce.d.ts +8 -0
  48. package/dist/devtools/warnOnce.js +18 -0
  49. package/dist/economy/auctionBook.d.ts +1 -1
  50. package/dist/economy/auctionBook.js +11 -1
  51. package/dist/economy/listingBook.d.ts +1 -1
  52. package/dist/economy/listingBook.js +11 -1
  53. package/dist/economy/resourceLedger.js +3 -3
  54. package/dist/economy/shopStock.js +7 -6
  55. package/dist/economy/techTree.js +7 -6
  56. package/dist/economy/unlockPoints.js +3 -6
  57. package/dist/editor/commandApply.js +89 -16
  58. package/dist/editor/commands.d.ts +38 -1
  59. package/dist/editor/commands.js +320 -3
  60. package/dist/editor/creatorStorage.d.ts +59 -0
  61. package/dist/editor/creatorStorage.js +164 -0
  62. package/dist/editor/document.d.ts +4 -0
  63. package/dist/editor/document.js +119 -18
  64. package/dist/editor/environment.js +8 -0
  65. package/dist/editor/index.d.ts +2 -0
  66. package/dist/editor/liveSync.d.ts +4 -1
  67. package/dist/editor/liveSync.js +26 -10
  68. package/dist/editor/materialAuthoring.d.ts +14 -0
  69. package/dist/editor/materialAuthoring.js +38 -0
  70. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  71. package/dist/editor/modelNodeVisibility.js +29 -0
  72. package/dist/editor/movementCatalog.d.ts +90 -0
  73. package/dist/editor/movementCatalog.js +144 -0
  74. package/dist/editor/simulation.d.ts +60 -0
  75. package/dist/editor/simulation.js +202 -0
  76. package/dist/editor/staticPrefab.d.ts +17 -0
  77. package/dist/editor/staticPrefab.js +51 -0
  78. package/dist/editor/types.d.ts +15 -0
  79. package/dist/faction/reputation.d.ts +6 -0
  80. package/dist/faction/reputation.js +11 -0
  81. package/dist/game/cameraConfig.d.ts +54 -2
  82. package/dist/game/clipRoles.d.ts +2 -1
  83. package/dist/game/clipRoles.js +7 -3
  84. package/dist/game/contentValidation.d.ts +69 -0
  85. package/dist/game/contentValidation.js +118 -0
  86. package/dist/game/controlGate.d.ts +8 -0
  87. package/dist/game/controlGate.js +43 -6
  88. package/dist/game/defineGame.d.ts +32 -3
  89. package/dist/game/dialogueGraph.d.ts +20 -0
  90. package/dist/game/dialogueGraph.js +107 -0
  91. package/dist/game/events.d.ts +11 -0
  92. package/dist/game/keyValueStore.d.ts +17 -4
  93. package/dist/game/keyValueStore.js +35 -23
  94. package/dist/game/lootModifiers.d.ts +17 -0
  95. package/dist/game/lootModifiers.js +26 -0
  96. package/dist/game/lootTable.d.ts +4 -0
  97. package/dist/game/lootTable.js +16 -0
  98. package/dist/game/narrativeAuthoring.d.ts +67 -0
  99. package/dist/game/narrativeAuthoring.js +122 -0
  100. package/dist/game/playableGame.d.ts +134 -35
  101. package/dist/game/quest.d.ts +15 -0
  102. package/dist/game/quest.js +209 -63
  103. package/dist/game/questCatalog.d.ts +50 -0
  104. package/dist/game/questCatalog.js +205 -0
  105. package/dist/game/saveStore.d.ts +6 -5
  106. package/dist/game/saveStore.js +57 -57
  107. package/dist/game/systemRuntime.d.ts +2 -2
  108. package/dist/game/systemRuntime.js +84 -33
  109. package/dist/game/toasts.d.ts +8 -0
  110. package/dist/game/toasts.js +7 -0
  111. package/dist/game/vfxInstance.d.ts +9 -0
  112. package/dist/game/vfxInstance.js +12 -1
  113. package/dist/game/viewports.d.ts +43 -0
  114. package/dist/game/viewports.js +46 -0
  115. package/dist/game/worldItem.d.ts +5 -1
  116. package/dist/game/worldItem.js +0 -1
  117. package/dist/gameplay.d.ts +3 -0
  118. package/dist/gameplay.js +3 -0
  119. package/dist/input/actionBindings.d.ts +15 -1
  120. package/dist/input/actionBindings.js +14 -3
  121. package/dist/input/actionContexts.d.ts +25 -1
  122. package/dist/input/actionContexts.js +82 -25
  123. package/dist/input/axisInput.d.ts +4 -0
  124. package/dist/input/axisInput.js +4 -0
  125. package/dist/input/axisShaper.d.ts +73 -0
  126. package/dist/input/axisShaper.js +105 -0
  127. package/dist/input/gamepadModel.d.ts +36 -2
  128. package/dist/input/gamepadModel.js +69 -30
  129. package/dist/input/haptics.d.ts +50 -0
  130. package/dist/input/haptics.js +74 -0
  131. package/dist/input/inputBuffer.d.ts +40 -0
  132. package/dist/input/inputBuffer.js +59 -0
  133. package/dist/input/lookChannel.d.ts +10 -0
  134. package/dist/input/lookChannel.js +6 -0
  135. package/dist/input/pointer.d.ts +2 -0
  136. package/dist/input/pointer.js +4 -0
  137. package/dist/interaction/promptRegistry.d.ts +5 -4
  138. package/dist/interaction/promptRegistry.js +8 -2
  139. package/dist/inventory/inventoryModel.js +9 -6
  140. package/dist/inventory/shapedGrid.d.ts +48 -9
  141. package/dist/inventory/shapedGrid.js +69 -10
  142. package/dist/item/affix.d.ts +6 -0
  143. package/dist/item/affix.js +6 -0
  144. package/dist/item/durability.js +4 -0
  145. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  146. package/dist/item/itemInstanceRegistry.js +111 -7
  147. package/dist/item/itemgen.d.ts +1 -1
  148. package/dist/item/itemgen.js +1 -1
  149. package/dist/item/modularItem.js +7 -1
  150. package/dist/material/appearanceSignals.d.ts +53 -0
  151. package/dist/material/appearanceSignals.js +47 -0
  152. package/dist/material/hairCards.d.ts +47 -0
  153. package/dist/material/hairCards.js +136 -0
  154. package/dist/material/materialAsset.d.ts +162 -0
  155. package/dist/material/materialAsset.js +382 -0
  156. package/dist/meta/changelog.d.ts +1 -1
  157. package/dist/meta/changelog.js +444 -1
  158. package/dist/movement/characterController.d.ts +5 -2
  159. package/dist/movement/characterController.js +50 -23
  160. package/dist/movement/freeFlight.d.ts +2 -2
  161. package/dist/movement/freeFlight.js +3 -3
  162. package/dist/movement/glideModel.d.ts +3 -0
  163. package/dist/movement/glideModel.js +6 -0
  164. package/dist/movement/grappleSwing.d.ts +2 -0
  165. package/dist/movement/grappleSwing.js +5 -1
  166. package/dist/movement/movementModel.d.ts +63 -1
  167. package/dist/movement/movementModel.js +136 -26
  168. package/dist/movement/movementProbe.d.ts +51 -0
  169. package/dist/movement/movementProbe.js +119 -0
  170. package/dist/movement/playerMovement.d.ts +48 -2
  171. package/dist/movement/playerMovement.js +493 -87
  172. package/dist/movement/poseState.d.ts +2 -0
  173. package/dist/movement/poseState.js +29 -1
  174. package/dist/movement/solidObstacles.d.ts +11 -1
  175. package/dist/movement/solidObstacles.js +48 -3
  176. package/dist/movement/terrainGrade.d.ts +13 -0
  177. package/dist/movement/terrainGrade.js +33 -0
  178. package/dist/movement/voxelController.d.ts +2 -2
  179. package/dist/movement/voxelController.js +10 -10
  180. package/dist/nav/navFromEnvironment.d.ts +9 -3
  181. package/dist/nav/navFromEnvironment.js +18 -12
  182. package/dist/nav/navGrid.d.ts +19 -1
  183. package/dist/nav/navGrid.js +26 -10
  184. package/dist/nav/navMesh.d.ts +43 -2
  185. package/dist/nav/navMesh.js +534 -128
  186. package/dist/nav/solidRoute.d.ts +50 -0
  187. package/dist/nav/solidRoute.js +144 -0
  188. package/dist/physics/aircraftDynamics.d.ts +390 -0
  189. package/dist/physics/aircraftDynamics.js +704 -0
  190. package/dist/physics/ballisticSweep.d.ts +10 -3
  191. package/dist/physics/ballisticSweep.js +53 -29
  192. package/dist/physics/boatDynamics.d.ts +116 -0
  193. package/dist/physics/boatDynamics.js +160 -0
  194. package/dist/physics/drivableVehicle.d.ts +22 -8
  195. package/dist/physics/drivableVehicle.js +4 -2
  196. package/dist/physics/flightDynamics.d.ts +21 -4
  197. package/dist/physics/flightDynamics.js +28 -5
  198. package/dist/physics/forceVolume.d.ts +41 -0
  199. package/dist/physics/forceVolume.js +88 -0
  200. package/dist/physics/handlingProbe.d.ts +249 -0
  201. package/dist/physics/handlingProbe.js +606 -0
  202. package/dist/physics/kinematicVehicle.d.ts +40 -5
  203. package/dist/physics/kinematicVehicle.js +17 -2
  204. package/dist/physics/physicsBackend.d.ts +12 -0
  205. package/dist/physics/physicsBackendConformance.js +34 -0
  206. package/dist/physics/physicsWorldBackend.d.ts +1 -0
  207. package/dist/physics/physicsWorldBackend.js +52 -3
  208. package/dist/physics/spatialGrid.d.ts +1 -1
  209. package/dist/physics/spatialGrid.js +8 -9
  210. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  211. package/dist/physics/vehicleBackendLink.js +102 -0
  212. package/dist/physics/vehicleDynamics.d.ts +375 -0
  213. package/dist/physics/vehicleDynamics.js +737 -0
  214. package/dist/physics/vehicleObstacles.d.ts +3 -2
  215. package/dist/physics/vehicleObstacles.js +29 -5
  216. package/dist/physics/worldColliders.d.ts +1 -1
  217. package/dist/physics/worldColliders.js +35 -3
  218. package/dist/puzzle/cellGrid.d.ts +15 -9
  219. package/dist/puzzle/cellGrid.js +13 -9
  220. package/dist/render/lookPreset.d.ts +9 -2
  221. package/dist/render/lookPreset.js +29 -2
  222. package/dist/render/postProcessing.d.ts +30 -0
  223. package/dist/render/postProcessing.js +1 -1
  224. package/dist/runtime/cameraDirector.d.ts +54 -3
  225. package/dist/runtime/cameraDirector.js +187 -0
  226. package/dist/runtime/commandDispatch.d.ts +14 -0
  227. package/dist/runtime/commandDispatch.js +19 -0
  228. package/dist/runtime/commandInput.d.ts +37 -0
  229. package/dist/runtime/commandInput.js +58 -0
  230. package/dist/runtime/commandRunner.d.ts +27 -0
  231. package/dist/runtime/commandRunner.js +31 -3
  232. package/dist/runtime/context/combat.d.ts +12 -4
  233. package/dist/runtime/context/combat.js +179 -11
  234. package/dist/runtime/context/deathLoot.d.ts +11 -8
  235. package/dist/runtime/context/deathLoot.js +17 -16
  236. package/dist/runtime/context/scene.d.ts +2 -0
  237. package/dist/runtime/context/scene.js +13 -21
  238. package/dist/runtime/descriptors/baseline.js +34 -3
  239. package/dist/runtime/descriptors/features.d.ts +2 -1
  240. package/dist/runtime/descriptors/features.js +18 -0
  241. package/dist/runtime/gameContext.js +86 -13
  242. package/dist/runtime/gameContextTypes.d.ts +33 -3
  243. package/dist/runtime/gameRuntime.d.ts +3 -1
  244. package/dist/runtime/gameRuntime.js +4 -0
  245. package/dist/runtime/headlessRunner.d.ts +3 -1
  246. package/dist/runtime/headlessRunner.js +11 -11
  247. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  248. package/dist/runtime/hostedGameRunner.js +156 -10
  249. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  250. package/dist/runtime/hostedWorldSession.js +154 -16
  251. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  252. package/dist/runtime/inputRecorder.d.ts +1 -1
  253. package/dist/runtime/inputRecorder.js +21 -1
  254. package/dist/runtime/inputSnapshot.d.ts +22 -4
  255. package/dist/runtime/inputSnapshot.js +41 -3
  256. package/dist/runtime/localPlayers.d.ts +73 -0
  257. package/dist/runtime/localPlayers.js +119 -0
  258. package/dist/runtime/runtimeSave.d.ts +10 -6
  259. package/dist/runtime/runtimeSave.js +11 -5
  260. package/dist/runtime/simContext.d.ts +8 -3
  261. package/dist/runtime/simContext.js +7 -3
  262. package/dist/runtime/simSnapshot.js +2 -0
  263. package/dist/runtime/transport.d.ts +9 -0
  264. package/dist/runtime/transport.js +7 -1
  265. package/dist/runtime/worldReplication.d.ts +2 -0
  266. package/dist/runtime/worldReplication.js +1 -1
  267. package/dist/scene/assetCatalog.d.ts +2 -0
  268. package/dist/scene/behaviorRuntime.d.ts +38 -7
  269. package/dist/scene/behaviorRuntime.js +293 -30
  270. package/dist/scene/behaviors.d.ts +35 -2
  271. package/dist/scene/behaviors.js +29 -0
  272. package/dist/scene/entityStore.d.ts +14 -3
  273. package/dist/scene/entityStore.js +45 -9
  274. package/dist/scene/mount.d.ts +9 -0
  275. package/dist/scene/mount.js +35 -0
  276. package/dist/scene/possession.d.ts +2 -0
  277. package/dist/scene/possession.js +21 -1
  278. package/dist/scene/sceneKinds.d.ts +9 -0
  279. package/dist/scene/sceneRaycast.d.ts +7 -0
  280. package/dist/scene/sceneRaycast.js +55 -0
  281. package/dist/scene/spatial.d.ts +20 -1
  282. package/dist/scene/spatial.js +94 -2
  283. package/dist/scene/vehicleSeat.d.ts +41 -3
  284. package/dist/scene/vehicleSeat.js +94 -3
  285. package/dist/sensor/concealment.d.ts +6 -0
  286. package/dist/sensor/concealment.js +8 -0
  287. package/dist/sensor/freezeMonitor.d.ts +6 -0
  288. package/dist/sensor/freezeMonitor.js +8 -0
  289. package/dist/sensor/perception.d.ts +11 -1
  290. package/dist/sensor/perception.js +57 -10
  291. package/dist/sensor/recordingBuffer.d.ts +3 -0
  292. package/dist/sensor/recordingBuffer.js +8 -0
  293. package/dist/session/roundState.d.ts +4 -0
  294. package/dist/session/roundState.js +11 -0
  295. package/dist/settings/frameRateLimit.d.ts +16 -0
  296. package/dist/settings/frameRateLimit.js +37 -0
  297. package/dist/settings/graphicsProfile.d.ts +2 -0
  298. package/dist/settings/graphicsProfile.js +12 -0
  299. package/dist/settings/graphicsSettings.d.ts +40 -0
  300. package/dist/settings/graphicsSettings.js +81 -0
  301. package/dist/settings/settingsModel.d.ts +9 -1
  302. package/dist/settings/settingsModel.js +10 -0
  303. package/dist/stats/accumulatorMeter.d.ts +10 -0
  304. package/dist/stats/accumulatorMeter.js +6 -0
  305. package/dist/stats/eventMeter.d.ts +10 -3
  306. package/dist/stats/eventMeter.js +6 -3
  307. package/dist/store/defineStore.d.ts +3 -1
  308. package/dist/store/defineStore.js +3 -1
  309. package/dist/store/observableKeyedStore.d.ts +1 -1
  310. package/dist/store/observableKeyedStore.js +8 -6
  311. package/dist/time/simClock.d.ts +6 -2
  312. package/dist/time/simClock.js +5 -0
  313. package/dist/vfx/feedbackMixer.d.ts +61 -0
  314. package/dist/vfx/feedbackMixer.js +106 -0
  315. package/dist/vfx/particleDirector.d.ts +58 -6
  316. package/dist/vfx/particleDirector.js +113 -22
  317. package/dist/vfx/particles.d.ts +68 -5
  318. package/dist/vfx/particles.js +284 -32
  319. package/dist/visibility/assetStreaming.d.ts +21 -3
  320. package/dist/visibility/assetStreaming.js +108 -24
  321. package/dist/visibility/settings.d.ts +8 -2
  322. package/dist/visibility/settings.js +3 -0
  323. package/dist/visibility/simulationCulling.d.ts +8 -1
  324. package/dist/visibility/simulationCulling.js +7 -0
  325. package/dist/visibility/spatialIndex.d.ts +21 -0
  326. package/dist/visibility/spatialIndex.js +16 -0
  327. package/dist/visibility/visibilitySystem.js +1 -0
  328. package/dist/work/expedition.d.ts +110 -0
  329. package/dist/work/expedition.js +195 -0
  330. package/dist/work/staffedStation.d.ts +29 -0
  331. package/dist/work/staffedStation.js +27 -0
  332. package/dist/world/authoredAnimation.d.ts +38 -0
  333. package/dist/world/authoredAnimation.js +99 -0
  334. package/dist/world/authoredObjects.d.ts +24 -4
  335. package/dist/world/authoredObjects.js +43 -16
  336. package/dist/world/authoredSimulation.d.ts +74 -0
  337. package/dist/world/authoredSimulation.js +302 -0
  338. package/dist/world/authoredSolids.d.ts +46 -0
  339. package/dist/world/authoredSolids.js +75 -0
  340. package/dist/world/authoredWeather.d.ts +120 -0
  341. package/dist/world/authoredWeather.js +314 -0
  342. package/dist/world/buildingGenerator.js +2 -2
  343. package/dist/world/buildingKit.d.ts +11 -4
  344. package/dist/world/buildingKit.js +21 -8
  345. package/dist/world/buildings.d.ts +34 -2
  346. package/dist/world/buildings.js +11 -1
  347. package/dist/world/cityKind.d.ts +7 -0
  348. package/dist/world/cityKind.js +52 -1
  349. package/dist/world/environmentForces.d.ts +21 -0
  350. package/dist/world/environmentForces.js +42 -0
  351. package/dist/world/features.d.ts +32 -5
  352. package/dist/world/features.js +6 -0
  353. package/dist/world/footprintGrid.d.ts +62 -0
  354. package/dist/world/footprintGrid.js +217 -0
  355. package/dist/world/interiors.d.ts +1 -0
  356. package/dist/world/interiors.js +5 -3
  357. package/dist/world/lod.d.ts +6 -0
  358. package/dist/world/lod.js +8 -0
  359. package/dist/world/pathTerrain.d.ts +1 -1
  360. package/dist/world/terraform.d.ts +9 -1
  361. package/dist/world/terraform.js +53 -18
  362. package/dist/world/walls.d.ts +7 -0
  363. package/dist/world/walls.js +7 -0
  364. package/dist/world/weather.d.ts +17 -0
  365. package/dist/world/weather.js +116 -20
  366. package/dist/world/worldSolids.d.ts +95 -0
  367. package/dist/world/worldSolids.js +351 -0
  368. package/dist/world.d.ts +6 -6
  369. package/dist/world.js +5 -5
  370. package/package.json +1 -1
  371. package/skills/ce-handoff/SKILL.md +2 -2
  372. package/skills/game-design/SKILL.md +2 -2
  373. package/skills/jgengine/SKILL.md +28 -6
  374. package/skills/jgengine/api.md +139 -99
  375. package/skills/jgengine/capabilities.md +28 -0
  376. package/skills/jgengine/recipes/minimal-game.md +10 -2
  377. package/skills/jgengine-combat/SKILL.md +13 -2
  378. package/skills/jgengine-combat/api.md +150 -103
  379. package/skills/jgengine-combat/capabilities.md +29 -1
  380. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  381. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  382. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  383. package/skills/jgengine-combat/reference.md +24 -2
  384. package/skills/jgengine-gameplay/SKILL.md +63 -0
  385. package/skills/jgengine-gameplay/api.md +583 -463
  386. package/skills/jgengine-gameplay/capabilities.md +92 -0
  387. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  388. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  389. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  390. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  391. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  392. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  393. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  394. package/skills/jgengine-gameplay/reference-systems.md +123 -0
  395. package/skills/jgengine-world/SKILL.md +25 -4
  396. package/skills/jgengine-world/api.md +1045 -655
  397. package/skills/jgengine-world/capabilities.md +246 -9
  398. package/skills/jgengine-world/recipes/character-animation.md +62 -0
  399. package/skills/jgengine-world/recipes/controls.md +48 -0
  400. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  401. package/skills/jgengine-world/recipes/npc-senses.md +55 -0
  402. package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
  403. package/skills/jgengine-world/reference.md +180 -4
  404. package/skills/jgengine-world/visibility.md +139 -0
@@ -24,29 +24,142 @@ export function createHostedGameRunner(options) {
24
24
  const loop = definition.loop ?? {};
25
25
  const replicator = createWorldReplicator(() => ctx.snapshot(), {
26
26
  worldVersion: () => ctx.replicationVersion(),
27
+ initialRevision: options.revision ?? 0,
27
28
  });
28
29
  const members = new Map();
29
30
  const inputs = new Map();
30
31
  const latestInputs = new Map();
31
- const movementTuning = resolvePlayerMovementTuning({ world: definition.world, physics: definition.physics });
32
+ const movementTuning = { ...resolvePlayerMovementTuning({ world: definition.world, physics: definition.physics }), authoritativeStep: true };
32
33
  const inputSeq = new Map();
34
+ const inputPressSeq = new Map();
35
+ const departed = new Map();
36
+ const maxDepartedUsers = options.inputRetention?.maxDepartedUsers ?? 256;
37
+ const departedTtlMs = options.inputRetention?.departedTtlMs ?? 300_000;
38
+ const inputNowMs = options.inputRetention?.nowMs ?? Date.now;
39
+ if (!Number.isInteger(maxDepartedUsers) || maxDepartedUsers < 0 || !Number.isFinite(departedTtlMs) || departedTtlMs < 0) {
40
+ throw new RangeError("inputRetention requires a nonnegative integer user limit and finite nonnegative TTL");
41
+ }
42
+ let retentionTime = -Infinity;
43
+ const retentionNow = () => {
44
+ const time = inputNowMs();
45
+ if (!Number.isFinite(time))
46
+ throw new RangeError("inputRetention.nowMs must return finite milliseconds");
47
+ return retentionTime = Math.max(retentionTime, time);
48
+ };
49
+ const expireDeparted = () => {
50
+ const time = retentionNow();
51
+ // Monotone leave times keep insertion order equal to expiry order, even if
52
+ // the wall clock reverses. Live ingress inspects only the first tombstone.
53
+ while (departed.size > 0) {
54
+ const [userId, expiresAt] = departed.entries().next().value;
55
+ if (expiresAt > time && departed.size <= maxDepartedUsers)
56
+ break;
57
+ departed.delete(userId);
58
+ inputSeq.delete(userId);
59
+ inputPressSeq.delete(userId);
60
+ }
61
+ };
62
+ const compactRecorder = (recorder, tick) => {
63
+ const continuous = recorder.frameAt(tick);
64
+ const frames = recorder.frames().filter(entry => entry.tick > tick);
65
+ if (continuous !== null) {
66
+ const { presses: _consumed, ...held } = continuous;
67
+ frames.unshift({ tick, frame: held });
68
+ }
69
+ recorder.restore({ frames });
70
+ };
33
71
  let hostTick = 0;
72
+ const inputModule = {
73
+ key: "hostInput",
74
+ snapshot() {
75
+ const tick = ctx.sim.tick();
76
+ const saved = [];
77
+ for (const userId of members.keys()) {
78
+ const recorder = inputs.get(userId);
79
+ const latest = latestInputs.get(userId);
80
+ if (recorder === undefined || latest === undefined)
81
+ continue;
82
+ const continuous = recorder.frameAt(tick);
83
+ // Keep continuous intent and unconsumed future ticks, never elapsed history.
84
+ // Live input coalesces onto the next tick; explicit replay may stage future ticks.
85
+ const frames = recorder.frames().filter(entry => entry.tick > tick);
86
+ if (continuous !== null) {
87
+ const { presses: _consumed, ...held } = continuous;
88
+ frames.unshift({ tick, frame: held });
89
+ }
90
+ const seq = inputSeq.get(userId);
91
+ const pressSeq = inputPressSeq.get(userId);
92
+ saved.push({ userId, latest, recorder: { frames }, ...(seq === undefined ? {} : { seq }), ...(pressSeq === undefined ? {} : { pressSeq }) });
93
+ }
94
+ return saved;
95
+ },
96
+ hydrate(saved) {
97
+ inputs.clear();
98
+ latestInputs.clear();
99
+ inputSeq.clear();
100
+ inputPressSeq.clear();
101
+ departed.clear();
102
+ for (const input of saved) {
103
+ const recorder = createInputRecorder();
104
+ recorder.restore(input.recorder);
105
+ inputs.set(input.userId, recorder);
106
+ latestInputs.set(input.userId, input.latest);
107
+ if (input.seq !== undefined)
108
+ inputSeq.set(input.userId, input.seq);
109
+ if (input.pressSeq !== undefined)
110
+ inputPressSeq.set(input.userId, input.pressSeq);
111
+ }
112
+ },
113
+ decode(raw) {
114
+ const frame = (value) => {
115
+ if (value === null || typeof value !== "object" || !("held" in value) || !Array.isArray(value.held) || !value.held.every(action => typeof action === "string"))
116
+ return false;
117
+ if (!("pointer" in value))
118
+ return false;
119
+ const pointer = value.pointer;
120
+ if (pointer !== null && (typeof pointer !== "object" || !("x" in pointer) || !("y" in pointer) || !("active" in pointer) || !Number.isFinite(pointer.x) || !Number.isFinite(pointer.y) || typeof pointer.active !== "boolean"))
121
+ return false;
122
+ if ("tick" in value && value.tick !== undefined && !Number.isFinite(value.tick))
123
+ return false;
124
+ if ("presses" in value && value.presses !== undefined && (!Array.isArray(value.presses) || !value.presses.every(press => press !== null && typeof press === "object" && typeof press.action === "string" && Number.isFinite(press.seq))))
125
+ return false;
126
+ if ("analog" in value && value.analog !== undefined && value.analog !== null && (typeof value.analog !== "object" || Array.isArray(value.analog) || !Object.values(value.analog).every(Number.isFinite)))
127
+ return false;
128
+ return true;
129
+ };
130
+ if (!Array.isArray(raw) || !raw.every(input => input !== null && typeof input === "object" && typeof input.userId === "string" &&
131
+ (input.seq === undefined || Number.isFinite(input.seq)) && (input.pressSeq === undefined || Number.isFinite(input.pressSeq)) && frame(input.latest) &&
132
+ input.recorder !== null && typeof input.recorder === "object" && Array.isArray(input.recorder.frames) &&
133
+ input.recorder.frames.every((entry) => entry !== null && typeof entry === "object" && Number.isFinite(entry.tick) && frame(entry.frame))))
134
+ return null;
135
+ if (new Set(raw.map(input => input.userId)).size !== raw.length)
136
+ return null;
137
+ return raw;
138
+ },
139
+ };
140
+ ctx.game.registerSave?.(inputModule);
34
141
  loop.onInit?.(ctx);
35
142
  syncLifecyclePhase(ctx, definition.lifecycle);
36
143
  if (restore !== undefined)
37
- ctx.hydrate(restore);
144
+ ctx.restore(restore);
38
145
  return {
39
146
  join(userId, isNew) {
147
+ expireDeparted();
40
148
  if (members.has(userId))
41
149
  return;
150
+ departed.delete(userId);
42
151
  const player = { userId, isNew };
43
152
  members.set(userId, player);
153
+ ctx.player.possession.own(userId, userId);
44
154
  ctx.game.players?.join(userId, isNew);
45
155
  loop.onNewPlayer?.(ctx, player);
46
156
  syncLifecyclePhase(ctx, definition.lifecycle);
47
157
  },
48
158
  resume(userId) {
159
+ expireDeparted();
160
+ departed.delete(userId);
49
161
  members.set(userId, { userId, isNew: false });
162
+ ctx.player.possession.own(userId, userId);
50
163
  ctx.game.players?.join(userId, false);
51
164
  },
52
165
  leave(userId) {
@@ -56,12 +169,22 @@ export function createHostedGameRunner(options) {
56
169
  members.delete(userId);
57
170
  inputs.delete(userId);
58
171
  latestInputs.delete(userId);
59
- inputSeq.delete(userId);
172
+ // Retain only bounded, recent admission marks; departed ingress is ignored.
173
+ if (inputSeq.has(userId) || inputPressSeq.has(userId)) {
174
+ departed.delete(userId);
175
+ departed.set(userId, retentionNow() + departedTtlMs);
176
+ }
177
+ expireDeparted();
60
178
  loop.onPlayerLeave?.(ctx, player);
61
179
  ctx.game.players?.leave(userId);
62
180
  },
63
181
  input(userId, frame) {
182
+ expireDeparted();
183
+ if (!members.has(userId))
184
+ return;
64
185
  const seq = frame.seq;
186
+ if ((seq !== undefined && !Number.isFinite(seq)) || (frame.tick !== undefined && !Number.isFinite(frame.tick)))
187
+ return;
65
188
  if (seq !== undefined) {
66
189
  const lastSeq = inputSeq.get(userId);
67
190
  if (lastSeq !== undefined && seq <= lastSeq)
@@ -73,8 +196,26 @@ export function createHostedGameRunner(options) {
73
196
  recorder = createInputRecorder();
74
197
  inputs.set(userId, recorder);
75
198
  }
76
- recorder.record(frame.tick ?? ctx.sim.tick() + 1, frame);
77
- latestInputs.set(userId, frame);
199
+ let admitted = frame;
200
+ if (frame.presses !== undefined) {
201
+ let lastPress = inputPressSeq.get(userId) ?? -Infinity;
202
+ const presses = [...frame.presses].sort((a, b) => a.seq - b.seq).filter(press => {
203
+ if (!Number.isFinite(press.seq) || press.seq <= lastPress)
204
+ return false;
205
+ lastPress = press.seq;
206
+ return true;
207
+ });
208
+ if (presses.length > 0)
209
+ inputPressSeq.set(userId, lastPress);
210
+ admitted = { ...frame, presses };
211
+ }
212
+ recorder.record(frame.tick ?? ctx.sim.tick() + 1, admitted);
213
+ compactRecorder(recorder, ctx.sim.tick());
214
+ latestInputs.set(userId, admitted);
215
+ },
216
+ inputStats() {
217
+ expireDeparted();
218
+ return { members: members.size, recorders: inputs.size, recordedFrames: [...inputs.values()].reduce((count, recorder) => count + recorder.frames().length, 0), latestInputs: latestInputs.size, wireSequences: inputSeq.size, pressSequences: inputPressSeq.size, departedUsers: departed.size };
78
219
  },
79
220
  heldInput(userId) {
80
221
  return latestInputs.get(userId) ?? null;
@@ -83,8 +224,9 @@ export function createHostedGameRunner(options) {
83
224
  return ctx.game.commands.runAs(userId, name, input);
84
225
  },
85
226
  tick(dt) {
227
+ expireDeparted();
86
228
  hostTick += 1;
87
- ctx.sim.advance(dt, (stepDt, tick) => {
229
+ ctx.sim.advance(dt, (stepDt, tick, gameDt) => {
88
230
  for (const [userId, recorder] of inputs) {
89
231
  const frame = recorder.frameAt(tick);
90
232
  if (frame !== null)
@@ -93,13 +235,15 @@ export function createHostedGameRunner(options) {
93
235
  ctx.sim.runStages("beforeMovement", stepDt);
94
236
  for (const userId of members.keys()) {
95
237
  const frame = inputs.get(userId)?.frameAt(tick) ?? null;
96
- if (frame !== null)
97
- serverStep(ctx, userId, frame, stepDt, movementTuning);
238
+ if (frame !== null && gameDt > 0)
239
+ serverStep(ctx, userId, frame, gameDt, movementTuning);
98
240
  }
99
241
  ctx.sim.runStages("afterMovement", stepDt);
100
- loop.onTick?.(ctx, stepDt);
101
- advanceBehaviors(ctx, stepDt);
242
+ loop.onTick?.(ctx, gameDt);
243
+ advanceBehaviors(ctx, gameDt);
102
244
  ctx.sim.runStages("afterTick", stepDt);
245
+ for (const recorder of inputs.values())
246
+ compactRecorder(recorder, tick);
103
247
  });
104
248
  syncLifecyclePhase(ctx, definition.lifecycle);
105
249
  return replicator.commit();
@@ -107,6 +251,8 @@ export function createHostedGameRunner(options) {
107
251
  diff: (sinceRevision) => replicator.diff(sinceRevision),
108
252
  revision: () => replicator.revision(),
109
253
  snapshot: (viewer) => ctx.snapshot(viewer === undefined ? undefined : { ...viewer, tick: hostTick }),
254
+ state: ctx.state,
255
+ commit: () => replicator.commit(),
110
256
  projectsViewers: () => ctx.replicatesPerViewer(),
111
257
  members: () => Array.from(members.keys()),
112
258
  context: () => ctx,
@@ -7,7 +7,12 @@ import type { WorldDiff } from "./worldReplication.js";
7
7
  import type { SnapshotViewer, WorldSnapshot } from "./worldSnapshot.js";
8
8
  import type { HostedWorldRecord, HostedWorldStore, SyncHostedWorldStore } from "./hostedWorldStore.js";
9
9
  export type { HostedWorldRecord, HostedWorldStore, SyncHostedWorldStore } from "./hostedWorldStore.js";
10
- /** In-process {@link HostedWorldStore} for tests, local play, and the browser-tab P2P host.
10
+ /** @internal Decode the retry envelope shared by authoritative hosts. */
11
+ export declare function unwrapOpEnvelope(input: unknown): {
12
+ opId: string;
13
+ input: unknown;
14
+ } | null;
15
+ /** Detached in-process {@link SyncHostedWorldStore} for tests, local play, and the browser-tab P2P host.
11
16
  * @internal
12
17
  */
13
18
  export declare function memoryWorldStore(seed?: HostedWorldRecord): SyncHostedWorldStore;
@@ -30,8 +35,10 @@ export interface HostedWorldSessionOptions<TAssetRef extends ModelAssetRef, TMul
30
35
  now?: () => number;
31
36
  /** Where the authoritative snapshot persists; defaults to {@link memoryWorldStore}. */
32
37
  store?: SyncHostedWorldStore;
33
- /** Minimum elapsed ms between auto-saves on tick, measured against `now` (defaults to `Date.now` when omitted). Default `0` — persist on every revision-changing tick. */
38
+ /** Minimum elapsed ms between automatic saves, measured against `now` (default `Date.now`). Default `0`; private clock/simulation changes also count. A pending write defers automatic capture until a later tick. */
34
39
  saveIntervalMs?: number;
40
+ /** Retained retry receipts per player. Default `64`; retries after eviction are new operations. */
41
+ operationHistoryLimit?: number;
35
42
  /** Render-model lookup for collider auto-fit, forwarded to the runner — see {@link HostedGameRunnerOptions.models}. */
36
43
  models?: GameContextModels;
37
44
  }
@@ -43,11 +50,14 @@ export interface HostedWorldSessionOptions<TAssetRef extends ModelAssetRef, TMul
43
50
  */
44
51
  export interface HostedWorldSession {
45
52
  join(userId: string, isNew: boolean): void;
53
+ /** Whether this identity has joined before, including across leave/restart. */
54
+ hasPlayer(userId: string): boolean;
46
55
  leave(userId: string): void;
47
56
  input(userId: string, frame: InputFrame): void;
48
- command(userId: string, name: string, input: unknown): CommandResult<GameContext>;
57
+ /** Optional stable operation id deduplicates retries across reconnect/restart; the receipt persists atomically with state. */
58
+ command(userId: string, name: string, input: unknown, operationId?: string): CommandResult<GameContext>;
49
59
  tick(dt: number): number;
50
- /** Replication pull: `null`/`0` cursor → full baseline; a prior revision → a diff since it. */
60
+ /** Replication pull: a fresh, pre-restart or future cursor receives a baseline; an in-session revision receives a diff. */
51
61
  sync(sinceRevision: number | null): HostedWorldSync;
52
62
  /** Alias used by host transports when pulling the next baseline or diff for a subscriber. */
53
63
  pull(sinceRevision: number | null): HostedWorldSync;
@@ -58,7 +68,9 @@ export interface HostedWorldSession {
58
68
  revision(): number;
59
69
  members(): readonly string[];
60
70
  /** Force-persist the current world to the store. */
61
- save(): void;
71
+ save(): void | Promise<void>;
72
+ /** Most recent automatic save failure; cleared by a successful write. Explicit `save()` also rejects. */
73
+ persistenceError(): unknown | null;
62
74
  runner(): HostedGameRunner;
63
75
  }
64
76
  /** Build a {@link HostedWorldSession} — a live runner loaded from a {@link HostedWorldStore} and auto-persisted on tick.
@@ -1,22 +1,34 @@
1
1
  import { createHostedGameRunner } from "./hostedGameRunner.js";
2
- /** In-process {@link HostedWorldStore} for tests, local play, and the browser-tab P2P host.
2
+ import { applyWorldSnapshot } from "./worldSnapshot.js";
3
+ /** @internal Decode the retry envelope shared by authoritative hosts. */
4
+ export function unwrapOpEnvelope(input) {
5
+ if (typeof input !== "object" || input === null || Array.isArray(input))
6
+ return null;
7
+ const record = input;
8
+ const opId = record["__jgWsOpId"];
9
+ if (typeof opId !== "string")
10
+ return null;
11
+ const { __jgWsOpId: _opId, ...rest } = record;
12
+ return { opId, input: rest };
13
+ }
14
+ /** Detached in-process {@link SyncHostedWorldStore} for tests, local play, and the browser-tab P2P host.
3
15
  * @internal
4
16
  */
5
17
  export function memoryWorldStore(seed) {
6
- let record = seed ?? null;
18
+ let record = seed === undefined ? null : structuredClone(seed);
7
19
  return {
8
- load: () => record,
20
+ load: () => record === null ? null : structuredClone(record),
9
21
  save(next) {
10
- record = { snapshot: next.snapshot, revision: next.revision };
22
+ record = structuredClone(next);
11
23
  },
12
24
  };
13
25
  }
14
26
  /** Async in-process store for callers exercising the production persistence contract. */
15
27
  export function asyncMemoryWorldStore(seed) {
16
- let record = seed ?? null;
28
+ let record = seed === undefined ? null : structuredClone(seed);
17
29
  return {
18
- async load() { return record; },
19
- async save(next) { record = { snapshot: next.snapshot, revision: next.revision }; },
30
+ async load() { return record === null ? null : structuredClone(record); },
31
+ async save(next) { record = structuredClone(next); },
20
32
  };
21
33
  }
22
34
  /** Build a {@link HostedWorldSession} — a live runner loaded from a {@link HostedWorldStore} and auto-persisted on tick.
@@ -33,32 +45,157 @@ export function createHostedWorldSession(options) {
33
45
  ...(host === undefined ? {} : { host }),
34
46
  ...(now === undefined ? {} : { now }),
35
47
  ...(loaded === null ? {} : { restore: loaded.snapshot }),
48
+ revision: loaded?.revision ?? 0,
36
49
  ...(options.models === undefined ? {} : { models: options.models }),
37
50
  });
51
+ const knownPlayers = new Set();
52
+ const operations = new Map();
53
+ const configuredHistory = options.operationHistoryLimit ?? 64;
54
+ const historyLimit = Number.isFinite(configuredHistory) ? Math.max(1, Math.floor(configuredHistory)) : 64;
55
+ const sessionModule = {
56
+ key: "hostSession",
57
+ snapshot: () => ({ players: [...knownPlayers], operations: [...operations] }),
58
+ hydrate(state) {
59
+ knownPlayers.clear();
60
+ for (const userId of state.players)
61
+ knownPlayers.add(userId);
62
+ operations.clear();
63
+ for (const [userId, receipts] of state.operations)
64
+ operations.set(userId, receipts.slice(-historyLimit));
65
+ },
66
+ decode(raw) {
67
+ if (raw === null || typeof raw !== "object" || !("players" in raw) || !("operations" in raw))
68
+ return null;
69
+ if (!Array.isArray(raw.players) || !raw.players.every((id) => typeof id === "string") || !Array.isArray(raw.operations))
70
+ return null;
71
+ for (const entry of raw.operations) {
72
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || !Array.isArray(entry[1]))
73
+ return null;
74
+ for (const receipt of entry[1]) {
75
+ if (receipt === null || typeof receipt !== "object" || typeof receipt.id !== "string" ||
76
+ typeof receipt.command !== "string" || typeof receipt.input !== "string" ||
77
+ !["applied", "rejected", "unknown-command"].includes(receipt.outcome) ||
78
+ (receipt.outcome === "rejected" && typeof receipt.reason !== "string"))
79
+ return null;
80
+ }
81
+ }
82
+ return raw;
83
+ },
84
+ };
85
+ runner.context().game.registerSave?.(sessionModule);
86
+ if (loaded !== null) {
87
+ if (Object.hasOwn(loaded.snapshot, "hostSession") && sessionModule.decode(loaded.snapshot["hostSession"]) === null) {
88
+ throw new Error("Invalid hosted player/retry state; save left untouched");
89
+ }
90
+ applyWorldSnapshot([sessionModule], loaded.snapshot);
91
+ }
38
92
  let savedRevision = loaded?.revision ?? 0;
93
+ let savedVersion = runner.context().version();
94
+ let savedSimTick = runner.context().sim.tick();
39
95
  let lastSaveAt = clock();
96
+ let writing = null;
97
+ let saveFailure = null;
40
98
  function persist() {
41
- savedRevision = runner.revision();
42
- store.save({ snapshot: runner.snapshot(), revision: savedRevision });
99
+ const record = { snapshot: runner.state(), revision: runner.revision() };
100
+ const version = runner.context().version();
101
+ const simTick = runner.context().sim.tick();
102
+ const success = () => {
103
+ savedRevision = record.revision;
104
+ savedVersion = version;
105
+ savedSimTick = simTick;
106
+ saveFailure = null;
107
+ };
108
+ const write = () => store.save(record);
109
+ let pending;
110
+ try {
111
+ pending = writing === null ? write() : writing.catch(() => { }).then(write);
112
+ }
113
+ catch (error) {
114
+ saveFailure = error;
115
+ throw error;
116
+ }
117
+ if (pending === undefined) {
118
+ success();
119
+ return;
120
+ }
121
+ const completion = pending.then(success, (error) => {
122
+ saveFailure = error;
123
+ throw error;
124
+ });
125
+ writing = completion;
126
+ void completion.catch(() => { }).finally(() => {
127
+ if (writing === completion)
128
+ writing = null;
129
+ });
130
+ return completion;
43
131
  }
44
132
  const pull = (sinceRevision) => {
45
- if (sinceRevision === null || sinceRevision <= 0) {
133
+ if (sinceRevision === null || sinceRevision <= (loaded?.revision ?? 0) || sinceRevision > runner.revision()) {
46
134
  return { kind: "baseline", revision: runner.revision(), snapshot: runner.snapshot() };
47
135
  }
48
136
  return { kind: "diff", diff: runner.diff(sinceRevision) };
49
137
  };
50
138
  return {
51
- join: (userId, isNew) => runner.join(userId, isNew),
52
- leave: (userId) => runner.leave(userId),
139
+ join(userId, isNew) {
140
+ runner.join(userId, knownPlayers.has(userId) ? false : isNew);
141
+ knownPlayers.add(userId);
142
+ runner.context().touch();
143
+ runner.commit();
144
+ },
145
+ hasPlayer: (userId) => knownPlayers.has(userId),
146
+ leave(userId) {
147
+ runner.leave(userId);
148
+ runner.commit();
149
+ },
53
150
  input: (userId, frame) => runner.input(userId, frame),
54
- command: (userId, name, input) => runner.command(userId, name, input),
151
+ command(userId, name, input, operationId) {
152
+ const envelope = unwrapOpEnvelope(input);
153
+ if (envelope !== null) {
154
+ operationId ??= envelope.opId;
155
+ input = envelope.input;
156
+ }
157
+ const receipts = operations.get(userId) ?? [];
158
+ const inputJson = JSON.stringify(input) ?? "null";
159
+ const receipt = operationId === undefined ? undefined : receipts.find((entry) => entry.id === operationId);
160
+ if (receipt !== undefined) {
161
+ if (receipt.command !== name || receipt.input !== inputJson)
162
+ return { status: "rejected", reason: "operation-conflict" };
163
+ if (receipt.outcome === "applied")
164
+ return { status: "applied", state: runner.context() };
165
+ if (receipt.outcome === "rejected")
166
+ return { status: "rejected", reason: receipt.reason };
167
+ return { status: "unknown-command" };
168
+ }
169
+ const before = runner.state();
170
+ let result;
171
+ try {
172
+ result = runner.command(userId, name, input);
173
+ }
174
+ catch (error) {
175
+ runner.context().restore(before);
176
+ throw error;
177
+ }
178
+ if (result.status !== "applied")
179
+ runner.context().restore(before);
180
+ if (operationId !== undefined) {
181
+ receipts.push({ id: operationId, command: name, input: inputJson, outcome: result.status,
182
+ ...(result.status === "rejected" ? { reason: result.reason } : {}) });
183
+ operations.set(userId, receipts.slice(-historyLimit));
184
+ runner.context().touch();
185
+ }
186
+ runner.commit();
187
+ return result;
188
+ },
55
189
  tick(dt) {
56
190
  const revision = runner.tick(dt);
57
- if (revision !== savedRevision) {
191
+ if (writing === null && (revision !== savedRevision || runner.context().version() !== savedVersion || runner.context().sim.tick() !== savedSimTick)) {
58
192
  const at = clock();
59
193
  if (saveIntervalMs <= 0 || at - lastSaveAt >= saveIntervalMs) {
60
194
  lastSaveAt = at;
61
- persist();
195
+ try {
196
+ persist();
197
+ }
198
+ catch { /* Exposed by persistenceError and retried by the next save. */ }
62
199
  }
63
200
  }
64
201
  return revision;
@@ -70,6 +207,7 @@ export function createHostedWorldSession(options) {
70
207
  revision: () => runner.revision(),
71
208
  members: () => runner.members(),
72
209
  save: persist,
210
+ persistenceError: () => saveFailure,
73
211
  runner: () => runner,
74
212
  };
75
213
  }
@@ -81,7 +219,7 @@ export async function createHostedWorldSessionAsync(options) {
81
219
  ...sessionOptions,
82
220
  store: {
83
221
  load: () => loaded,
84
- save: (record) => { void store.save(record); },
222
+ save: (record) => store.save(record),
85
223
  },
86
224
  });
87
225
  return session;
@@ -13,8 +13,8 @@ export interface HostedWorldStore {
13
13
  load(): Promise<HostedWorldRecord | null>;
14
14
  save(record: HostedWorldRecord): Promise<void>;
15
15
  }
16
- /** Synchronous store adapter retained for deterministic in-process tests. */
16
+ /** Already-loaded store adapter: synchronous reads, with synchronous or asynchronous writes. */
17
17
  export interface SyncHostedWorldStore {
18
18
  load(): HostedWorldRecord | null;
19
- save(record: HostedWorldRecord): void;
19
+ save(record: HostedWorldRecord): void | Promise<void>;
20
20
  }
@@ -10,7 +10,7 @@ export interface InputRecorderState {
10
10
  }
11
11
  /** Tick-indexed input log: record per tick, look up the frame in force at any tick. */
12
12
  export interface InputRecorder {
13
- /** Store the frame in force from `tick` onward. Recording the same tick twice replaces the earlier frame. */
13
+ /** Store held state from `tick` onward and presses on that tick only. Same-tick recordings merge press identities while replacing continuous state. */
14
14
  record(tick: number, frame: InputFrame): void;
15
15
  /** The frame in force at `tick`: the latest recorded frame whose tick is at most `tick`, or `null` before the first. */
16
16
  frameAt(tick: number): InputFrame | null;
@@ -2,6 +2,7 @@ function cloneFrame(frame) {
2
2
  return {
3
3
  held: [...frame.held],
4
4
  pointer: frame.pointer === null ? null : { ...frame.pointer },
5
+ ...(frame.presses === undefined ? {} : { presses: frame.presses.map(press => ({ ...press })) }),
5
6
  ...(frame.analog === undefined ? {} : { analog: frame.analog === null ? null : { ...frame.analog } }),
6
7
  ...(frame.tick === undefined ? {} : { tick: frame.tick }),
7
8
  };
@@ -14,6 +15,7 @@ function cloneFrame(frame) {
14
15
  */
15
16
  export function createInputRecorder() {
16
17
  let frames = [];
18
+ const continuousFrames = new WeakMap();
17
19
  function indexAtOrBefore(tick) {
18
20
  let lo = 0;
19
21
  let hi = frames.length - 1;
@@ -35,6 +37,13 @@ export function createInputRecorder() {
35
37
  const entry = { tick, frame: cloneFrame(frame) };
36
38
  const index = indexAtOrBefore(tick);
37
39
  if (index >= 0 && frames[index].tick === tick) {
40
+ const previous = frames[index].frame.presses;
41
+ if (previous !== undefined) {
42
+ const presses = new Map(previous.map(press => [press.seq, press]));
43
+ for (const press of entry.frame.presses ?? [])
44
+ presses.set(press.seq, press);
45
+ entry.frame.presses = [...presses.values()].map(press => ({ ...press }));
46
+ }
38
47
  frames[index] = entry;
39
48
  return;
40
49
  }
@@ -42,7 +51,18 @@ export function createInputRecorder() {
42
51
  },
43
52
  frameAt(tick) {
44
53
  const index = indexAtOrBefore(tick);
45
- return index < 0 ? null : frames[index].frame;
54
+ if (index < 0)
55
+ return null;
56
+ const entry = frames[index];
57
+ if (entry.tick === tick || entry.frame.presses === undefined)
58
+ return entry.frame;
59
+ let heldFrame = continuousFrames.get(entry.frame);
60
+ if (heldFrame === undefined) {
61
+ const { presses: _presses, ...continuous } = entry.frame;
62
+ heldFrame = continuous;
63
+ continuousFrames.set(entry.frame, heldFrame);
64
+ }
65
+ return heldFrame;
46
66
  },
47
67
  frames: () => frames,
48
68
  lastTick: () => (frames.length === 0 ? -1 : frames[frames.length - 1].tick),
@@ -1,9 +1,17 @@
1
1
  import type { PointerAxisState } from "../input/pointerAxis.js";
2
2
  import { type AxisBinding, type AxisRange } from "../input/axisInput.js";
3
- /** One client's input for a tick — the semantic held-action set plus pointer state, the serializable, over-the-wire counterpart of {@link InputSnapshot} the host stores per connected player. */
3
+ import { type HapticChannels } from "../input/haptics.js";
4
+ /** One discrete press, identified across retransmitted live input frames. */
5
+ export interface InputPress {
6
+ action: string;
7
+ seq: number;
8
+ }
9
+ /** One client's serializable held state and discrete edges for a simulation tick. */
4
10
  export interface InputFrame {
5
11
  held: readonly string[];
6
12
  pointer: PointerAxisState | null;
13
+ /** Discrete presses retained through release until the host accepts them. Each identity applies once, on its recorded simulation tick. */
14
+ presses?: readonly InputPress[];
7
15
  /** Analog per-action magnitudes (0..1) for actions driven by a continuous source (virtual joystick, gamepad stick); absent/null means every held action is fully pressed. */
8
16
  analog?: Readonly<Record<string, number>> | null;
9
17
  /** Simulation tick (`ctx.sim.tick()`) the frame was captured for; hosts and replay align inputs on it. Absent from senders that predate the fixed-step loop. */
@@ -16,8 +24,18 @@ export interface InputSnapshot {
16
24
  weak: number;
17
25
  ms: number;
18
26
  }): Promise<boolean>;
19
- /** Replaces the held-action set for this frame, rolling the previous held set forward for edge detection (#671). Called by the shell before `onTick` each frame; does not bump `ctx.version()` or notify `ctx.subscribe` listeners — per-frame publishes would storm subscribers. */
20
- publish(held: readonly string[]): void;
27
+ /**
28
+ * Continuous rumble channels for a local user (`engine`, `road`, `impact`, …): set levels each tick
29
+ * and the shell mixes them by priority onto that user's pad. Local only — never in a snapshot or on the wire.
30
+ */
31
+ haptics(userId: string): HapticChannels;
32
+ /** Replaces held state without notifying subscribers. Supplying `edges` buffers short presses until `beginStep`; `reset` discards the current owner's pending edges. Without `edges`, legacy publication-based edge detection is preserved. */
33
+ publish(held: readonly string[], edges?: {
34
+ pressed?: readonly string[];
35
+ reset?: boolean;
36
+ }): void;
37
+ /** Sample buffered input once before a simulation step. Zero-step render frames retain presses; subsequent steps retire the sampled edges. */
38
+ beginStep(): void;
21
39
  /** Replaces the normalized pointer-position state for this frame (#293). Same no-notify contract as `publish`. */
22
40
  publishPointer(state: PointerAxisState | null): void;
23
41
  /** Replaces the analog per-action magnitudes for this frame (#1370) — the virtual joystick's continuous vector, split into 0..1 values per movement action. `null` clears back to digital-only. Same no-notify contract as `publish`. */
@@ -25,7 +43,7 @@ export interface InputSnapshot {
25
43
  isDown(action: string): boolean;
26
44
  /** The held-action set for this frame — an owned, frozen array; the array {@link publish} was called with is never aliased or returned. */
27
45
  held(): readonly string[];
28
- /** True only on the frame `action` transitions from up to down (#671) — derived from the last two published held sets, so it stays replay-safe. */
46
+ /** True on the sampled simulation step containing a press; legacy publishers use the last two held sets. */
29
47
  justPressed(action: string): boolean;
30
48
  /** True only on the frame `action` transitions from down to up (#671); mirrors {@link justPressed}. */
31
49
  justReleased(action: string): boolean;