lecodes-cli 0.19.0 → 0.19.2

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 (254) hide show
  1. package/dist/index.js +730 -235
  2. package/package.json +14 -13
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk-types.json +1 -1
  5. package/src/api.ts +302 -0
  6. package/src/browserAuth.ts +87 -0
  7. package/src/cmgenTool.ts +104 -0
  8. package/src/commands/app.ts +891 -0
  9. package/src/commands/appAndroid.ts +603 -0
  10. package/src/commands/appDesktop.ts +326 -0
  11. package/src/commands/appDesktopMac.ts +470 -0
  12. package/src/commands/appIcon.ts +187 -0
  13. package/src/commands/appShared.ts +448 -0
  14. package/src/commands/appTemplates.ts +883 -0
  15. package/src/commands/appTemplatesAndroid.ts +599 -0
  16. package/src/commands/appTemplatesGradlew.ts +9 -0
  17. package/src/commands/assets.ts +28 -0
  18. package/src/commands/clone.ts +60 -0
  19. package/src/commands/compile.ts +253 -0
  20. package/src/commands/create.ts +59 -0
  21. package/src/commands/design.ts +863 -0
  22. package/src/commands/designTemplates.ts +11 -0
  23. package/src/commands/desktop.ts +122 -0
  24. package/src/commands/dev.ts +214 -0
  25. package/src/commands/diff.ts +59 -0
  26. package/src/commands/init.ts +191 -0
  27. package/src/commands/install.ts +198 -0
  28. package/src/commands/lightmap.ts +290 -0
  29. package/src/commands/link.ts +147 -0
  30. package/src/commands/login.ts +24 -0
  31. package/src/commands/navmesh.ts +225 -0
  32. package/src/commands/pn.ts +305 -0
  33. package/src/commands/projectTemplates.ts +129 -0
  34. package/src/commands/pull.ts +109 -0
  35. package/src/commands/push.ts +173 -0
  36. package/src/commands/render.ts +414 -0
  37. package/src/commands/scene.ts +148 -0
  38. package/src/commands/shaders.ts +157 -0
  39. package/src/commands/status.ts +29 -0
  40. package/src/commands/test.ts +370 -0
  41. package/src/commands/thumbs.ts +178 -0
  42. package/src/commands/types.ts +87 -0
  43. package/src/commands/update.ts +190 -0
  44. package/src/compile/assetIcons.ts +214 -0
  45. package/src/compile/collect.ts +46 -0
  46. package/src/compile/collectLocal.ts +37 -0
  47. package/src/compile/designCompile.ts +104 -0
  48. package/src/compile/fonts.ts +156 -0
  49. package/src/compile/headlessBundle.ts +129 -0
  50. package/src/compile/nativeStack.ts +21 -0
  51. package/src/compile/projectCompile.ts +109 -0
  52. package/src/compile/sceneCompile.ts +113 -0
  53. package/src/compile/screenEntry.ts +127 -0
  54. package/src/compile/shaders.ts +245 -0
  55. package/src/config.ts +42 -0
  56. package/src/designMeta.ts +35 -0
  57. package/src/desktopRenderer.ts +532 -0
  58. package/src/desktopScript.ts +276 -0
  59. package/src/dev/clientTemplates.ts +160 -0
  60. package/src/dev/devServer.ts +292 -0
  61. package/src/dev/wsServer.ts +144 -0
  62. package/src/distRoot.ts +20 -0
  63. package/src/ignore.ts +163 -0
  64. package/src/index.ts +491 -0
  65. package/src/lecodes-3d-editor.d.ts +41 -0
  66. package/src/lecodes-assets.d.ts +7 -0
  67. package/src/lecodes-design.d.ts +191 -0
  68. package/src/lecodes-renderer.d.ts +131 -0
  69. package/src/localFiles.ts +144 -0
  70. package/src/manifest.ts +46 -0
  71. package/src/matcTool.ts +137 -0
  72. package/src/peers.ts +40 -0
  73. package/src/project.ts +61 -0
  74. package/src/projectEnv.ts +94 -0
  75. package/src/qrcode-terminal.d.ts +9 -0
  76. package/src/releases.ts +125 -0
  77. package/src/serverDiff.ts +78 -0
  78. package/src/textDiff.ts +103 -0
  79. package/src/types.ts +0 -0
  80. package/src/util.ts +146 -0
  81. package/runtime/sdk/animate/animate.ts +0 -238
  82. package/runtime/sdk/animate/bezier.ts +0 -138
  83. package/runtime/sdk/animate/easings.ts +0 -126
  84. package/runtime/sdk/canvas/Canvas.ts +0 -305
  85. package/runtime/sdk/compile/aspectMacro.ts +0 -86
  86. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  87. package/runtime/sdk/compile/assetMacro.ts +0 -146
  88. package/runtime/sdk/compile/assetName.ts +0 -50
  89. package/runtime/sdk/compile/bundler.ts +0 -287
  90. package/runtime/sdk/compile/compileProject.ts +0 -129
  91. package/runtime/sdk/compile/detectEntry.ts +0 -128
  92. package/runtime/sdk/compile/fontMacro.ts +0 -459
  93. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  94. package/runtime/sdk/compile/header.ts +0 -67
  95. package/runtime/sdk/compile/index.ts +0 -100
  96. package/runtime/sdk/compile/libraryImports.ts +0 -52
  97. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  98. package/runtime/sdk/compile/sceneEditor.ts +0 -88
  99. package/runtime/sdk/compile/serverSplit.ts +0 -233
  100. package/runtime/sdk/compile/serverTypes.ts +0 -227
  101. package/runtime/sdk/compile/sfnt.ts +0 -98
  102. package/runtime/sdk/compile/shaderSchema.ts +0 -202
  103. package/runtime/sdk/compile/shaderTargets.ts +0 -81
  104. package/runtime/sdk/compile/sourcemap.ts +0 -25
  105. package/runtime/sdk/core/Aspect.ts +0 -568
  106. package/runtime/sdk/core/InspectorUI.ts +0 -212
  107. package/runtime/sdk/core/StateMachine.ts +0 -308
  108. package/runtime/sdk/core/color.ts +0 -66
  109. package/runtime/sdk/core/compWrite.ts +0 -42
  110. package/runtime/sdk/core/events.ts +0 -38
  111. package/runtime/sdk/core/fields.ts +0 -120
  112. package/runtime/sdk/core/registry.ts +0 -23
  113. package/runtime/sdk/core/signals.ts +0 -277
  114. package/runtime/sdk/core/time.ts +0 -84
  115. package/runtime/sdk/g2/Camera2D.ts +0 -40
  116. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  117. package/runtime/sdk/g2/Node2D.ts +0 -267
  118. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  119. package/runtime/sdk/g2/Physics2D.ts +0 -346
  120. package/runtime/sdk/g2/Scene2D.ts +0 -209
  121. package/runtime/sdk/g2/Shape2D.ts +0 -259
  122. package/runtime/sdk/g2/Sprite.ts +0 -89
  123. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  124. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  125. package/runtime/sdk/g2/Texture2D.ts +0 -47
  126. package/runtime/sdk/g2/Tilemap.ts +0 -41
  127. package/runtime/sdk/g2/Tileset.ts +0 -71
  128. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  129. package/runtime/sdk/g2/autotile.ts +0 -433
  130. package/runtime/sdk/g2/cells.ts +0 -91
  131. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  132. package/runtime/sdk/g2/groups2d.ts +0 -106
  133. package/runtime/sdk/g2/loop.ts +0 -50
  134. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  135. package/runtime/sdk/g2/touch.ts +0 -83
  136. package/runtime/sdk/gl/Camera.ts +0 -160
  137. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  138. package/runtime/sdk/gl/CharacterController.ts +0 -238
  139. package/runtime/sdk/gl/Gearbox.ts +0 -212
  140. package/runtime/sdk/gl/Geometry.ts +0 -279
  141. package/runtime/sdk/gl/IK.ts +0 -193
  142. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  143. package/runtime/sdk/gl/Light.ts +0 -107
  144. package/runtime/sdk/gl/Lightmap.ts +0 -256
  145. package/runtime/sdk/gl/Material.ts +0 -260
  146. package/runtime/sdk/gl/Mesh.ts +0 -83
  147. package/runtime/sdk/gl/Model.ts +0 -124
  148. package/runtime/sdk/gl/NavAgent.ts +0 -337
  149. package/runtime/sdk/gl/NavMesh.ts +0 -397
  150. package/runtime/sdk/gl/Node.ts +0 -350
  151. package/runtime/sdk/gl/Noise.ts +0 -30
  152. package/runtime/sdk/gl/Particles.ts +0 -676
  153. package/runtime/sdk/gl/Physics.ts +0 -222
  154. package/runtime/sdk/gl/Plane.ts +0 -53
  155. package/runtime/sdk/gl/Ragdoll.ts +0 -270
  156. package/runtime/sdk/gl/Ray.ts +0 -16
  157. package/runtime/sdk/gl/Scene.ts +0 -486
  158. package/runtime/sdk/gl/Shape.ts +0 -409
  159. package/runtime/sdk/gl/Terrain.ts +0 -1092
  160. package/runtime/sdk/gl/Texture.ts +0 -63
  161. package/runtime/sdk/gl/Trigger.ts +0 -45
  162. package/runtime/sdk/gl/Vehicle.ts +0 -473
  163. package/runtime/sdk/gl/Wheel.ts +0 -240
  164. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  165. package/runtime/sdk/gl/animation/Animator.ts +0 -120
  166. package/runtime/sdk/gl/animation/Layer.ts +0 -31
  167. package/runtime/sdk/gl/animation/Loop.ts +0 -30
  168. package/runtime/sdk/gl/animation/Playback.ts +0 -44
  169. package/runtime/sdk/gl/animation/core.ts +0 -336
  170. package/runtime/sdk/gl/controls.ts +0 -95
  171. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  172. package/runtime/sdk/gl/scenarios.ts +0 -291
  173. package/runtime/sdk/gl/state.ts +0 -6
  174. package/runtime/sdk/gl/terrainMesh.ts +0 -219
  175. package/runtime/sdk/gl/touch.ts +0 -68
  176. package/runtime/sdk/inject.ts +0 -197
  177. package/runtime/sdk/math/Mathf.ts +0 -118
  178. package/runtime/sdk/math/mat4.ts +0 -278
  179. package/runtime/sdk/math/quat.ts +0 -232
  180. package/runtime/sdk/math/vec.ts +0 -255
  181. package/runtime/sdk/net/codec.ts +0 -119
  182. package/runtime/sdk/net/core.ts +0 -384
  183. package/runtime/sdk/net/index.ts +0 -181
  184. package/runtime/sdk/net/replication.ts +0 -622
  185. package/runtime/sdk/plugins/camera.ts +0 -81
  186. package/runtime/sdk/plugins/geolocation.ts +0 -123
  187. package/runtime/sdk/plugins/oauth.ts +0 -61
  188. package/runtime/sdk/plugins/permission.ts +0 -7
  189. package/runtime/sdk/plugins/push.ts +0 -132
  190. package/runtime/sdk/plugins/qr.ts +0 -73
  191. package/runtime/sdk/plugins/service.ts +0 -47
  192. package/runtime/sdk/runtime/app.ts +0 -101
  193. package/runtime/sdk/runtime/appEvents.ts +0 -54
  194. package/runtime/sdk/runtime/channel.ts +0 -61
  195. package/runtime/sdk/runtime/clipboard.ts +0 -20
  196. package/runtime/sdk/runtime/datetime.ts +0 -329
  197. package/runtime/sdk/runtime/device.ts +0 -293
  198. package/runtime/sdk/runtime/fetch.ts +0 -77
  199. package/runtime/sdk/runtime/files.ts +0 -23
  200. package/runtime/sdk/runtime/input.ts +0 -175
  201. package/runtime/sdk/runtime/media.ts +0 -111
  202. package/runtime/sdk/runtime/misc.ts +0 -16
  203. package/runtime/sdk/runtime/net.ts +0 -36
  204. package/runtime/sdk/runtime/rpc.ts +0 -218
  205. package/runtime/sdk/runtime/service.ts +0 -83
  206. package/runtime/sdk/runtime/share.ts +0 -9
  207. package/runtime/sdk/runtime/storage.ts +0 -13
  208. package/runtime/sdk/runtime/touch.ts +0 -76
  209. package/runtime/sdk/scene/defineScene.ts +0 -1353
  210. package/runtime/sdk/scene/editorPlugins.ts +0 -110
  211. package/runtime/sdk/scene/gizmos.ts +0 -148
  212. package/runtime/sdk/scene/grammar.ts +0 -120
  213. package/runtime/sdk/scene/material.ts +0 -188
  214. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  215. package/runtime/sdk/server/auth/global.ts +0 -80
  216. package/runtime/sdk/server/auth/host.ts +0 -318
  217. package/runtime/sdk/server/auth/models.ts +0 -83
  218. package/runtime/sdk/server/auth/types.ts +0 -50
  219. package/runtime/sdk/server/channel.ts +0 -56
  220. package/runtime/sdk/server/context.ts +0 -36
  221. package/runtime/sdk/server/db/defineDb.ts +0 -237
  222. package/runtime/sdk/server/db/fields.ts +0 -132
  223. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  224. package/runtime/sdk/server/db/index.ts +0 -7
  225. package/runtime/sdk/server/db/marci/query.ts +0 -412
  226. package/runtime/sdk/server/db/types.ts +0 -202
  227. package/runtime/sdk/server/errors.ts +0 -12
  228. package/runtime/sdk/server/host.ts +0 -74
  229. package/runtime/sdk/server/inject.ts +0 -13
  230. package/runtime/sdk/server/runtime.ts +0 -133
  231. package/runtime/sdk/server/validate.ts +0 -87
  232. package/runtime/sdk/ui/NativeView.ts +0 -142
  233. package/runtime/sdk/ui/UI.ts +0 -39
  234. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  235. package/runtime/sdk/ui/UIButton.ts +0 -101
  236. package/runtime/sdk/ui/UIContainer.ts +0 -60
  237. package/runtime/sdk/ui/UIImage.ts +0 -83
  238. package/runtime/sdk/ui/UIInput.ts +0 -185
  239. package/runtime/sdk/ui/UIModal.ts +0 -139
  240. package/runtime/sdk/ui/UINode.ts +0 -830
  241. package/runtime/sdk/ui/UIPager.ts +0 -362
  242. package/runtime/sdk/ui/UIPopover.ts +0 -100
  243. package/runtime/sdk/ui/UIScreen.ts +0 -123
  244. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  245. package/runtime/sdk/ui/UISpacer.ts +0 -14
  246. package/runtime/sdk/ui/UITabs.ts +0 -236
  247. package/runtime/sdk/ui/UIText.ts +0 -51
  248. package/runtime/sdk/ui/UIVideo.ts +0 -88
  249. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  250. package/runtime/sdk/ui/UIWidget.ts +0 -127
  251. package/runtime/sdk/ui/fonts.ts +0 -13
  252. package/runtime/sdk/ui/presentable.ts +0 -117
  253. package/runtime/sdk/ui/router.ts +0 -132
  254. package/runtime/sdk/ui/theme.ts +0 -84
@@ -1,622 +0,0 @@
1
- // Replication — the state half of LeCodes multiplayer (docs/multiplayer-plan.md §3–4, phase 2):
2
- // `Net.kind` (a replicated entity type: defaults = the snapshot layout + the client factory),
3
- // `Net.input` (the per-tick input layout), the `Replicated` aspect (the server's handle on an entity;
4
- // the SDK attaches it to the client's proxy), inputs client→server, 20 Hz snapshots server→client,
5
- // spawn / despawn, the client's server-tick estimate + input lead control, snapshot interpolation
6
- // for remotes and predict + delta-correct for the owner.
7
- //
8
- // The clock: `Net.tick` counts fixed steps (one per Jolt substep — core/Aspect.ts `updateFixed`).
9
- // The server's tick IS the simulation tick. A client runs AHEAD of the server by `lead` ticks so its
10
- // input for tick T is on the server before the server simulates T; the snapshot header tells the
11
- // client how far ahead its newest input was (`lastInputTick − tick`) and the client nudges its own
12
- // tick to keep that inside [2, 8]. Remotes render at `serverEstimate − 6` (100 ms behind, between
13
- // two snapshots). The owner's proxy is its own prediction: same input, same code, same dt as the
14
- // server; when a snapshot for tick T disagrees with the recorded prediction for T the body is
15
- // shifted by the delta (later ticks keep their relative motion — no rewind) and the visual
16
- // offset `rep.correction` absorbs the jump and decays.
17
- //
18
- // Wire (channel 1, unreliable, binary; a leading type byte):
19
- // 1 inputs C→S u8 1 · u32 firstTick · u8 count · count × input record (last 3 ticks)
20
- // 2 snapshot S→C u8 2 · u32 tick · u32 lastInputTick · u8 missed · u16 count ·
21
- // count × { u16 netId · u8 kind · state record } (every 3rd tick)
22
- // Channel 0 (reliable JSON): `$spawn {netId, kind, owner, state}` · `$despawn {netId}`.
23
-
24
- import { Aspect } from "../core/Aspect"
25
- import { Time } from "../core/time"
26
- import { Vec3 } from "../math/vec"
27
- import type { Node } from "../gl/Node"
28
- import { Scene } from "../gl/Scene"
29
- import { assignState, cloneDefaults, hashString, lerpState, makeLayout, packInto, unpackFrom, type Layout } from "./codec"
30
- import { CH_UNRELIABLE, CH_RELIABLE, LOCAL_SLOT, NetPlayer, bridge, broadcastRaw, hooks, sendRaw, state, type NetRole } from "./core"
31
-
32
- export type NetTransform = "pos" | "pos+rot" | "none"
33
-
34
- /** The per-tick input layout: `const PlayerInput = Net.input({ moveX: 0, moveY: 0, jump: false })`. */
35
- export type NetInput<T extends object = any> = { readonly layout: Layout; readonly defaults: T; readonly __t?: T }
36
-
37
- /** A replicated entity type: `const PlayerKind = Net.kind('player', { pos: [0,0,0], hp: 100 }, { transform: 'pos', client })`. */
38
- export type NetKind<S extends object = any> = {
39
- readonly name: string
40
- /** @internal wire id (registration order; the welcome carries the name table) */
41
- readonly id: number
42
- readonly layout: Layout
43
- readonly defaults: S
44
- /** Which state fields the SDK samples on the server and applies to a client proxy: `pos` (a
45
- * `pos: [x,y,z]` field), `pos+rot` (+ a `rot: [x,y,z,w]` quaternion field), or `none`. */
46
- readonly transform: NetTransform
47
- /** Births the client-side proxy on spawn; the SDK attaches `Replicated` to what it returns. */
48
- readonly client?: (rep: NetEntity<S>, scene: Scene | null) => Node | void
49
- readonly __t?: S
50
- /** @internal scratch state for snapshot decoding */
51
- readonly _scratch: S
52
- }
53
-
54
- const TICK_MS = 1000 / 60
55
- const SNAPSHOT_EVERY = 3 // 20 Hz
56
- const INTERP_DELAY = 6 // ticks behind the estimated server tick (100 ms)
57
- const LEAD_MIN = 2, LEAD_MAX = 8
58
- const SNAPSHOT_BUDGET = 1000 // bytes — one unreliable datagram (creator-net caps messages at 1024)
59
- const CORRECTION_TAU = 0.1 // seconds — the visual offset's decay time constant
60
- const SNAP_DISTANCE = 2 // metres — a bigger disagreement is a teleport: no smoothing
61
- const NO_INPUT = 0xffffffff
62
- const VERTICAL_DEADBAND = 0.12 // metres — owner height disagreements below this are floor-snap noise
63
-
64
- // ---- registries ---------------------------------------------------------------------------------------
65
-
66
- const kinds = new Map<string, NetKind>()
67
- const kindList: NetKind[] = [] // index = wire id
68
- let clientKinds: (NetKind | undefined)[] = [] // client: wire id → kind, from the welcome's name table
69
- let inputDef: NetInput | null = null
70
- const entities = new Map<number, NetEntity>()
71
- let nextNetId = 1
72
-
73
- let tick = 0
74
- let localInput: any = null // what Net.setInput set this tick (every role but `server`)
75
- const inputRing = new Map<number, any>() // client: tick → values, resent with the next 2 sends
76
- let estimate = -1 // client: the server's tick, fractional, advanced per frame
77
- let leadCooldown = 0
78
- let pendingSnapshot: { tick: number; body: Uint8Array } | null = null // server: built per 3rd tick, sent at frame end
79
- let snapshotStart = 0 // server: round-robin start when the budget cuts a snapshot
80
- let warnedBudget = false
81
- let warnedKind = false
82
-
83
- /** Live counters (`Net.stats`): how the clocks and the prediction are doing. */
84
- export const stats = {
85
- /** client: snapshots received */ snapshots: 0,
86
- /** client: how far ahead of the server's simulation our newest input was at the last snapshot */ ahead: 0,
87
- /** client: our tick minus the estimated server tick */ lead: 0,
88
- /** client: tick nudges (+1 / −1) by the lead control */ nudges: 0,
89
- /** owner: corrections applied, the largest one and their sum (metres) — `sum / corrections` = the typical one */ corrections: 0, maxCorrection: 0, sumCorrection: 0,
90
- /** server: inputs that arrived late (the last one was repeated), all clients */ missed: 0,
91
- /** bytes sent on the unreliable channel (snapshots / inputs) */ bytesOut: 0,
92
- }
93
-
94
- // The scene a client births proxies into: the active one. (Net replication implies a 3D scene —
95
- // `transform` mirrors a Node's pose — so importing Scene here is honest, not a 2D-bundle tax.)
96
- const activeScene = (): Scene | null => Scene.active
97
-
98
- // ---- NetEntity: one replicated entity, the same object on every machine that knows it -----------------
99
-
100
- type PlayerIn = { ring: Map<number, any>; last: any; lastTick: number; missed: number; cur: any }
101
- const inOf = (p: NetPlayer): PlayerIn => (p._in ??= { ring: new Map(), last: null, lastTick: -1, missed: 0, cur: null })
102
-
103
- export class NetEntity<S extends object = any> {
104
- /** Session-unique id, assigned by the server. */
105
- readonly netId: number
106
- readonly kind: NetKind<S>
107
- /** The owning player's id; 0 = the server's own (props, the game state). */
108
- readonly ownerId: number
109
- /** Server: the authoritative state you write (`pos` is sampled from the node). Client remote:
110
- * interpolated. Client owner: the newest authoritative state. */
111
- readonly state: S
112
- /** The node carrying this entity (server: yours; client: what the kind's factory returned). */
113
- node: Node | null = null
114
- /** Owner on a client: the visual offset left by the last correction, decaying to zero — add it to
115
- * what you DRAW (a child mesh, the camera), never to physics. */
116
- readonly correction = new Vec3()
117
- /** @internal remote: received states by tick, ascending */
118
- _ring: { tick: number; s: S }[] = []
119
- /** @internal owner: predicted position after each tick */
120
- _pred = new Map<number, [number, number, number]>()
121
- /** @internal owner: the body shift a snapshot asked for, applied at the next fixed step (where the
122
- * node holds the post-step pose — in between it holds the render interpolation) */
123
- _shift: [number, number, number] | null = null
124
- /** @internal */
125
- _listeners: Array<(s: S) => void> = []
126
-
127
- constructor(netId: number, kind: NetKind<S>, ownerId: number, s: S) {
128
- this.netId = netId
129
- this.kind = kind
130
- this.ownerId = ownerId
131
- this.state = s
132
- }
133
-
134
- get owner(): NetPlayer | null {
135
- return this.ownerId ? state.players.find((p) => p.id === this.ownerId) ?? null : null
136
- }
137
- /** Our own player owns it (offline: everything owned by `Net.me`). */
138
- get isMine(): boolean { return this.ownerId !== 0 && state.me !== null && state.me.id === this.ownerId }
139
- /** This machine runs the simulation for it: the server always, a client for what it owns
140
- * (prediction). A remote proxy on a client is interpolation only. */
141
- get simulated(): boolean { return state.isServer || this.isMine }
142
-
143
- /** The owner's input for the tick being simulated — the ring entry on the server (the last one
144
- * repeated when it is late), the local input on the owner / host / offline. */
145
- input<T extends object>(inp: NetInput<T>): T {
146
- if (!state.isServer || this.isMine) return (localInput ?? inp.defaults) as T
147
- const p = this.owner
148
- return (p?._in?.cur ?? inp.defaults) as T
149
- }
150
-
151
- /** Client: `state` was refreshed (a snapshot landed / the interpolation moved). */
152
- on(event: "state", cb: (s: S) => void): this { if (!this._listeners.includes(cb)) this._listeners.push(cb); return this }
153
- off(event: "state", cb: (s: S) => void): this { const i = this._listeners.indexOf(cb); if (i >= 0) this._listeners.splice(i, 1); return this }
154
- /** @internal */
155
- _changed(): void { for (const fn of this._listeners) fn(this.state) }
156
- }
157
-
158
- // ---- declarations (Net.kind / Net.input) --------------------------------------------------------------
159
-
160
- export const declareKind = <S extends object>(name: string, defaults: S, opts: { transform?: NetTransform; client?: NetKind<S>["client"] } = {}): NetKind<S> => {
161
- if (kinds.has(name)) throw new Error(`Net.kind('${name}') is already declared`)
162
- if (kindList.length >= 255) throw new Error("Net.kind: at most 255 kinds")
163
- const layout = makeLayout(defaults as Record<string, unknown>, `Net.kind('${name}')`)
164
- const transform = opts.transform ?? "none"
165
- const need = transform === "pos" ? ["pos"] : transform === "pos+rot" ? ["pos", "rot"] : []
166
- for (const key of need) {
167
- const f = layout.fields.find((x) => x.key === key)
168
- const len = key === "pos" ? 3 : 4
169
- if (!f || f.kind !== 2 || f.len !== len) throw new Error(`Net.kind('${name}'): transform '${transform}' needs a '${key}' field of ${len} numbers`)
170
- }
171
- const kind: NetKind<S> = { name, id: kindList.length, layout, defaults: cloneDefaults(defaults), transform, client: opts.client, _scratch: cloneDefaults(defaults) }
172
- kinds.set(name, kind)
173
- kindList.push(kind)
174
- return kind
175
- }
176
-
177
- export const declareInput = <T extends object>(defaults: T): NetInput<T> => {
178
- if (inputDef) throw new Error("Net.input: one input layout per game")
179
- const layout = makeLayout(defaults as Record<string, unknown>, "Net.input")
180
- inputDef = { layout, defaults: cloneDefaults(defaults) }
181
- localInput = cloneDefaults(defaults)
182
- return inputDef as NetInput<T>
183
- }
184
-
185
- export const setInput = <T extends object>(inp: NetInput<T>, values: Partial<T>): void => {
186
- if (inp !== inputDef) throw new Error("Net.setInput: not the declared Net.input")
187
- const v = cloneDefaults(inp.defaults)
188
- assignState(inp.layout, v, values)
189
- localInput = v
190
- if (state.role === "client") {
191
- inputRing.set(tick, v)
192
- for (const t of inputRing.keys()) if (t < tick - 8) inputRing.delete(t)
193
- }
194
- }
195
-
196
- export const currentTick = (): number => tick
197
- export const allEntities = (): NetEntity[] => [...entities.values()]
198
- export const entityById = (netId: number): NetEntity | undefined => entities.get(netId)
199
-
200
- const schemaHash = (): string =>
201
- hashString(kindList.map((k) => `${k.name}=${k.layout.sig}`).join("|") + "#" + (inputDef?.layout.sig ?? ""))
202
-
203
- // ---- spawn / despawn (server side; the client mirrors) ---------------------------------------------------
204
-
205
- const samplePose = (e: NetEntity): void => {
206
- const t = e.kind.transform
207
- if (t === "none" || !e.node) return
208
- const s = e.state as any
209
- const p = e.node.position
210
- s.pos[0] = p.x; s.pos[1] = p.y; s.pos[2] = p.z
211
- if (t === "pos+rot") {
212
- const q = e.node.quaternion
213
- s.rot[0] = q.x; s.rot[1] = q.y; s.rot[2] = q.z; s.rot[3] = q.w
214
- }
215
- }
216
-
217
- const applyPose = (e: NetEntity): void => {
218
- const t = e.kind.transform
219
- if (t === "none" || !e.node) return
220
- const s = e.state as any
221
- e.node.position = s.pos
222
- if (t === "pos+rot") e.node.quaternion = s.rot
223
- }
224
-
225
- const spawnPayload = (e: NetEntity): string => {
226
- samplePose(e)
227
- return JSON.stringify(["$spawn", { netId: e.netId, kind: e.kind.name, owner: e.ownerId, state: e.state }])
228
- }
229
-
230
- const spawnOnServer = (kind: NetKind, owner: NetPlayer | null, node: Node): NetEntity => {
231
- const e = new NetEntity(nextNetId++, kind, owner?.id ?? 0, cloneDefaults(kind.defaults))
232
- e.node = node
233
- entities.set(e.netId, e)
234
- if (state.role !== "offline") broadcastRaw(spawnPayload(e), null)
235
- return e
236
- }
237
-
238
- const despawnOnServer = (e: NetEntity): void => {
239
- if (entities.get(e.netId) !== e) return
240
- entities.delete(e.netId)
241
- e.node = null
242
- if (state.role !== "offline") broadcastRaw(JSON.stringify(["$despawn", { netId: e.netId }]), null)
243
- }
244
-
245
- const onClientSpawn = (d: any): void => {
246
- if (!d || entities.has(d.netId)) return
247
- const kind = kinds.get(d.kind)
248
- if (!kind) { console.warn(`Net: spawn of unknown kind '${d.kind}' — is this the same bundle?`); return }
249
- const s = cloneDefaults(kind.defaults)
250
- assignState(kind.layout, s, d.state)
251
- const e = new NetEntity(d.netId, kind, d.owner | 0, s)
252
- entities.set(e.netId, e)
253
- if (!kind.client) return
254
- const node = kind.client(e, activeScene())
255
- if (!node) return
256
- e.node = node
257
- node.aspect(Replicated, { _rec: e })
258
- applyPose(e)
259
- }
260
-
261
- const onClientDespawn = (d: any): void => {
262
- const e = entities.get(d?.netId)
263
- if (!e) return
264
- entities.delete(e.netId)
265
- const node = e.node
266
- e.node = null
267
- node?.destroy()
268
- }
269
-
270
- // ---- the Replicated aspect ---------------------------------------------------------------------------
271
-
272
- /**
273
- * The server's handle on a replicated entity — attach it to the node that IS the entity:
274
- * `new Node().aspect(Replicated, { kind: PlayerKind, owner: player })`. From then on the entity exists
275
- * on every client (the kind's `client` factory births the proxy there, and the SDK attaches this same
276
- * aspect to it), its state is packed into every snapshot, and `node.destroy()` despawns it everywhere.
277
- * `node.replicated.state` / `.isMine` / `.simulated` / `.input()` are the same on both sides.
278
- */
279
- export class Replicated extends Aspect<"replicated", Node> {
280
- static readonly aspect = "replicated"
281
-
282
- /** Server: the entity type (required). */
283
- kind!: NetKind<any>
284
- /** Server: the owning player, or null for the server's own. */
285
- owner: NetPlayer | null = null
286
- /** @internal set by the SDK when it attaches the aspect to a client proxy */
287
- _rec: NetEntity | null = null
288
-
289
- protected onAttach(): void {
290
- if (this._rec) { this._rec.node = this.node; return }
291
- if (!state.isServer) {
292
- console.warn("Replicated: only the server spawns entities — on a client the kind's `client` factory births the proxy")
293
- return
294
- }
295
- if (!this.kind) throw new Error("Replicated needs { kind }")
296
- this._rec = spawnOnServer(this.kind, this.owner, this.node)
297
- }
298
-
299
- protected onDetach(): void {
300
- const e = this._rec
301
- if (!e) return
302
- this._rec = null
303
- if (state.isServer) despawnOnServer(e)
304
- else if (entities.get(e.netId) === e) { entities.delete(e.netId); e.node = null }
305
- }
306
-
307
- /** The entity itself (what the client factory received). */
308
- get entity(): NetEntity { return this._rec! }
309
- get netId(): number { return this._rec?.netId ?? 0 }
310
- get state(): any { return this._rec?.state }
311
- get isMine(): boolean { return this._rec?.isMine ?? false }
312
- get simulated(): boolean { return this._rec?.simulated ?? state.isServer }
313
- get ownerPlayer(): NetPlayer | null { return this._rec?.owner ?? null }
314
- get correction(): Vec3 { return this._rec?.correction ?? ZERO }
315
- /** The owner's input for the tick being simulated (see `NetEntity.input`). */
316
- input<T extends object>(inp: NetInput<T>): T { return this._rec ? this._rec.input(inp) : (localInput ?? inp.defaults) }
317
- // state-change listeners live on the entity: `node.replicated.entity.on('state', cb)` (the aspect's
318
- // own on/off are its aspect events)
319
- }
320
- const ZERO = new Vec3()
321
-
322
- // ---- server: inputs in, snapshots out ------------------------------------------------------------------
323
-
324
- const onServerBinary = (p: NetPlayer, channel: number, data: ArrayBuffer): void => {
325
- if (!inputDef || data.byteLength < 6) return
326
- const view = new DataView(data)
327
- if (view.getUint8(0) !== 1) return
328
- const first = view.getUint32(1, true)
329
- const count = view.getUint8(5)
330
- const s = inOf(p)
331
- let o = 6
332
- for (let i = 0; i < count; i++) {
333
- if (o + inputDef.layout.bytes > data.byteLength) break
334
- const t = first + i
335
- if (t > tick + 120) break // garbage
336
- if (t > tick || (t === tick && s.cur === null)) {
337
- const v = cloneDefaults(inputDef.defaults)
338
- o = unpackFrom(view, o, inputDef.layout, v)
339
- s.ring.set(t, v)
340
- } else o += inputDef.layout.bytes // already simulated — late
341
- if (t > s.lastTick) s.lastTick = t
342
- }
343
- }
344
-
345
- // At the top of each fixed step: the input every remote player gets for this tick.
346
- const serverConsumeInputs = (): void => {
347
- for (const p of state.players) {
348
- if (p._slot === LOCAL_SLOT) continue
349
- const s = inOf(p)
350
- const v = s.ring.get(tick)
351
- if (v) { s.cur = v; s.last = v; s.ring.delete(tick) }
352
- else { s.cur = s.last; if (s.lastTick >= 0) { s.missed++; stats.missed++ } }
353
- for (const t of s.ring.keys()) if (t < tick) s.ring.delete(t)
354
- }
355
- }
356
-
357
- const buildSnapshot = (forTick: number): void => {
358
- const list = [...entities.values()]
359
- if (list.length === 0) { pendingSnapshot = null; return }
360
- let size = 0
361
- for (const e of list) size += 3 + e.kind.layout.bytes
362
- const buf = new ArrayBuffer(Math.min(size, SNAPSHOT_BUDGET))
363
- const view = new DataView(buf)
364
- let o = 0
365
- let written = 0
366
- const n = list.length
367
- if (size > SNAPSHOT_BUDGET && !warnedBudget) {
368
- warnedBudget = true
369
- console.warn(`Net: a full snapshot is ${size} B, over the ${SNAPSHOT_BUDGET} B datagram budget — entities are rotated through it (fewer or smaller kinds fix this)`)
370
- }
371
- for (let i = 0; i < n; i++) {
372
- const e = list[(snapshotStart + i) % n]
373
- const rec = 3 + e.kind.layout.bytes
374
- if (o + rec > buf.byteLength) break
375
- samplePose(e)
376
- view.setUint16(o, e.netId, true); o += 2
377
- view.setUint8(o, e.kind.id); o += 1
378
- o = packInto(view, o, e.kind.layout, e.state)
379
- written++
380
- }
381
- if (written < n) snapshotStart = (snapshotStart + written) % n
382
- pendingSnapshot = { tick: forTick, body: new Uint8Array(buf, 0, o), }
383
- ;(pendingSnapshot as any).count = written
384
- }
385
-
386
- const sendSnapshots = (): void => {
387
- const snap = pendingSnapshot
388
- pendingSnapshot = null
389
- if (!snap) return
390
- const count = (snap as any).count as number
391
- for (const p of state.players) {
392
- if (p._slot === LOCAL_SLOT) continue
393
- const s = inOf(p)
394
- const buf = new ArrayBuffer(12 + snap.body.byteLength)
395
- const view = new DataView(buf)
396
- view.setUint8(0, 2)
397
- view.setUint32(1, snap.tick, true)
398
- view.setUint32(5, s.lastTick >= 0 ? s.lastTick : NO_INPUT, true)
399
- view.setUint8(9, Math.min(255, s.missed))
400
- view.setUint16(10, count, true)
401
- new Uint8Array(buf, 12).set(snap.body)
402
- s.missed = 0
403
- stats.bytesOut += buf.byteLength
404
- sendRaw(p._slot, CH_UNRELIABLE, buf)
405
- }
406
- }
407
-
408
- // ---- client: snapshots in, inputs out, the clock, interpolation, correction ----------------------------
409
-
410
- const rttTicks = (): number => (bridge()?.clientInfo(0)[1] ?? 0) / TICK_MS
411
-
412
- const onClientBinary = (channel: number, data: ArrayBuffer): void => {
413
- if (data.byteLength < 12) return
414
- const view = new DataView(data)
415
- if (view.getUint8(0) !== 2) return
416
- const T = view.getUint32(1, true)
417
- const lastIn = view.getUint32(5, true)
418
- const count = view.getUint16(10, true)
419
-
420
- // the server clock as SEEN here: the newest snapshot's tick, advanced per frame. (Not + rtt/2 — the
421
- // interpolation only needs "ticks behind the newest arrival", and a half round trip is a guess
422
- // that a one-sided delay, like the dev simulator's, gets wrong.)
423
- const target = T
424
- if (estimate < 0 || Math.abs(target - estimate) > 30) estimate = target
425
- else estimate += (target - estimate) * 0.1
426
-
427
- // input lead: keep our newest input 2–8 ticks ahead of the server's simulation
428
- stats.snapshots++
429
- if (lastIn !== NO_INPUT) stats.ahead = lastIn - T
430
- // Behind (inputs arriving late): jump straight to the target lead — every tick spent behind is a
431
- // repeated input on the server and a correction here. Too far ahead: back off one tick at a time
432
- // (a repeated tick re-simulates once; cheap, and rare).
433
- if (leadCooldown > 0) leadCooldown--
434
- else if (lastIn !== NO_INPUT && inputRing.size > 0) {
435
- const ahead = lastIn - T
436
- if (ahead < LEAD_MIN) { tick += Math.min(30, LEAD_MIN + 2 - ahead); leadCooldown = 10; stats.nudges++ }
437
- else if (ahead > LEAD_MAX) { tick -= 1; leadCooldown = 10; stats.nudges++ }
438
- }
439
-
440
- let o = 12
441
- for (let i = 0; i < count; i++) {
442
- if (o + 3 > data.byteLength) break
443
- const netId = view.getUint16(o, true)
444
- const kind = clientKinds[view.getUint8(o + 2)]
445
- o += 3
446
- if (!kind) {
447
- if (!warnedKind) { warnedKind = true; console.warn("Net: snapshot with an unknown kind id — dropped") }
448
- return
449
- }
450
- if (o + kind.layout.bytes > data.byteLength) break
451
- const e = entities.get(netId)
452
- if (!e) { o += kind.layout.bytes; continue } // its $spawn is still in flight
453
- if (e.isMine) {
454
- o = unpackFrom(view, o, kind.layout, e.state)
455
- reconcileOwner(e, T)
456
- e._changed()
457
- } else {
458
- const s = cloneDefaults(kind.defaults)
459
- o = unpackFrom(view, o, kind.layout, s)
460
- const ring = e._ring
461
- if (ring.length === 0 || T > ring[ring.length - 1].tick) ring.push({ tick: T, s })
462
- else if (!ring.some((r) => r.tick === T)) { ring.push({ tick: T, s }); ring.sort((a, b) => a.tick - b.tick) }
463
- if (ring.length > 32) ring.shift()
464
- }
465
- }
466
- }
467
-
468
- // The owner's prediction for tick T vs the server's state for T: the body is shifted by the
469
- // difference (the ticks simulated since T keep their relative motion — no rewind), the view stays
470
- // where it was via `correction`, and the later prediction records move along so the same error is
471
- // not applied twice. The shift itself lands at the next fixed step (applyOwnerShift): only there
472
- // does the node hold the post-step pose — in the frame phases it holds the render interpolation,
473
- // and shifting THAT would bake up to a step of lag into the body and never converge.
474
- const reconcileOwner = (e: NetEntity, T: number): void => {
475
- if (e.kind.transform === "none" || !e.node) return
476
- const pred = e._pred.get(T)
477
- for (const t of e._pred.keys()) if (t <= T) e._pred.delete(t)
478
- if (!pred) return
479
- const s = e.state as any
480
- const dx = s.pos[0] - pred[0], dz = s.pos[2] - pred[2]
481
- // Vertical deadband: a grounded character's height differs by a few cm between two machines
482
- // (landing phase, floor snap) and lifting the body by that much only makes it fall again — an
483
- // oscillation the correction itself feeds. Only a real height difference (a ledge, a jump) counts.
484
- let dy = s.pos[1] - pred[1]
485
- if (Math.abs(dy) < VERTICAL_DEADBAND) dy = 0
486
- const d = Math.hypot(dx, dy, dz)
487
- if (d < 0.005) return
488
- stats.corrections++
489
- stats.sumCorrection += d
490
- if (d > stats.maxCorrection) stats.maxCorrection = d
491
- for (const q of e._pred.values()) { q[0] += dx; q[1] += dy; q[2] += dz }
492
- const sh = e._shift ?? (e._shift = [0, 0, 0])
493
- sh[0] += dx; sh[1] += dy; sh[2] += dz
494
- if (d >= SNAP_DISTANCE) e.correction.set(0, 0, 0)
495
- }
496
-
497
- const applyOwnerShift = (e: NetEntity): void => {
498
- const sh = e._shift
499
- if (!sh || !e.node) return
500
- e._shift = null
501
- const p = e.node.position
502
- e.node.position = [p.x + sh[0], p.y + sh[1], p.z + sh[2]]
503
- const rec = e._pred.get(tick - 1)
504
- if (rec) { rec[0] += sh[0]; rec[1] += sh[1]; rec[2] += sh[2] }
505
- if (Math.hypot(sh[0], sh[1], sh[2]) < SNAP_DISTANCE) { e.correction.x -= sh[0]; e.correction.y -= sh[1]; e.correction.z -= sh[2] }
506
- }
507
-
508
- const clientFrame = (): void => {
509
- const dt = Time.unscaledDt
510
- if (estimate >= 0) estimate += dt * 60
511
- stats.lead = estimate >= 0 ? tick - estimate : 0
512
- const rt = estimate - INTERP_DELAY
513
- const k = Math.exp(-dt / CORRECTION_TAU)
514
- for (const e of entities.values()) {
515
- if (e.isMine) {
516
- const c = e.correction
517
- if (c.x !== 0 || c.y !== 0 || c.z !== 0) {
518
- c.x *= k; c.y *= k; c.z *= k
519
- if (Math.hypot(c.x, c.y, c.z) < 1e-3) c.set(0, 0, 0)
520
- }
521
- continue
522
- }
523
- const ring = e._ring
524
- if (ring.length === 0) continue
525
- const layout = e.kind.layout
526
- if (rt <= ring[0].tick) lerpState(layout, ring[0].s, ring[0].s, 0, e.state)
527
- else if (rt >= ring[ring.length - 1].tick) lerpState(layout, ring[ring.length - 1].s, ring[ring.length - 1].s, 0, e.state)
528
- else {
529
- let i = ring.length - 2
530
- while (i > 0 && ring[i].tick > rt) i--
531
- const a = ring[i], b = ring[i + 1]
532
- lerpState(layout, a.s, b.s, (rt - a.tick) / (b.tick - a.tick), e.state)
533
- }
534
- while (ring.length > 2 && ring[1].tick < rt - 30) ring.shift()
535
- applyPose(e)
536
- e._changed()
537
- }
538
- }
539
-
540
- const sendInputs = (): void => {
541
- if (!inputDef || inputRing.size === 0) return
542
- const first = tick - 2
543
- const buf = new ArrayBuffer(6 + 3 * inputDef.layout.bytes)
544
- const view = new DataView(buf)
545
- view.setUint8(0, 1)
546
- view.setUint32(1, first, true)
547
- view.setUint8(5, 3)
548
- let o = 6
549
- let last: any = localInput ?? inputDef.defaults
550
- for (let t = first; t <= tick; t++) {
551
- const v = inputRing.get(t)
552
- if (v) last = v
553
- o = packInto(view, o, inputDef.layout, last)
554
- }
555
- stats.bytesOut += buf.byteLength
556
- sendRaw(0, CH_UNRELIABLE, buf)
557
- }
558
-
559
- // ---- the frame hooks (core/Aspect.ts phase hooks, installed by startSession) -----------------------------
560
-
561
- const frameEarly = (): void => {
562
- if (state.role === "client" && state.status === "online") clientFrame()
563
- }
564
-
565
- const frameFixed = (): void => {
566
- tick++
567
- if (state.isServer) {
568
- serverConsumeInputs()
569
- if (state.role !== "offline" && (tick - 1) % SNAPSHOT_EVERY === 0) buildSnapshot(tick - 1)
570
- } else if (state.status === "online") {
571
- // prediction records: the state after step tick−1 has landed on the entities by now; then the
572
- // shift the last snapshot asked for (the record just taken moves with it)
573
- for (const e of entities.values()) {
574
- if (!e.isMine || !e.node || e.kind.transform === "none") continue
575
- const p = e.node.position
576
- e._pred.set(tick - 1, [p.x, p.y, p.z])
577
- if (e._pred.size > 128) for (const t of e._pred.keys()) { if (e._pred.size <= 128) break; e._pred.delete(t) }
578
- applyOwnerShift(e)
579
- }
580
- }
581
- }
582
-
583
- const frameLate = (): void => {
584
- if (state.isServer) sendSnapshots()
585
- else if (state.status === "online") sendInputs()
586
- }
587
-
588
- // ---- session lifecycle (called by Net.listen / Net.connect / the reset) ----------------------------------
589
-
590
- export const installReplication = (): void => {
591
- hooks.frameEarly = frameEarly
592
- hooks.frameFixed = frameFixed
593
- hooks.frameLate = frameLate
594
- hooks.helloExtra = () => ({ schema: schemaHash() })
595
- hooks.helloCheck = (d) => (d?.schema !== schemaHash() ? "schema" : null)
596
- hooks.welcomeExtra = () => ({ tick, kinds: kindList.map((k) => k.name) })
597
- hooks.afterWelcome = (p) => { for (const e of entities.values()) sendRaw(p._slot, CH_RELIABLE, spawnPayload(e)) }
598
- hooks.onWelcome = (d) => {
599
- clientKinds = ((d?.kinds ?? []) as string[]).map((n) => kinds.get(n))
600
- // yojimbo has no round-trip sample yet at the welcome: assume ~100 ms and let the lead control
601
- // settle the rest from the snapshots' feedback
602
- const rtt = Math.max(rttTicks(), 6)
603
- estimate = (d?.tick ?? 0) + rtt / 2
604
- tick = Math.ceil((d?.tick ?? 0) + rtt + 4)
605
- leadCooldown = 10
606
- }
607
- hooks.clientReserved = (name, d) => { if (name === "$spawn") onClientSpawn(d); else if (name === "$despawn") onClientDespawn(d) }
608
- hooks.serverBinary = onServerBinary
609
- hooks.clientBinary = onClientBinary
610
- hooks.playerLeft = (p) => { for (const e of [...entities.values()]) if (e.ownerId === p.id) { if (e.node) e.node.destroy(); else despawnOnServer(e) } }
611
- hooks.reset = (role: NetRole) => {
612
- if (role === "client") {
613
- // the proxies were ours to make — take them down; the server's own nodes are the game's
614
- for (const e of [...entities.values()]) { entities.delete(e.netId); const n = e.node; e.node = null; n?.destroy() }
615
- estimate = -1
616
- }
617
- inputRing.clear()
618
- pendingSnapshot = null
619
- leadCooldown = 0
620
- for (const p of state.players) p._in = null
621
- }
622
- }