@netforge/engine 0.0.0-stage → 0.1.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 (244) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +147 -2
  5. package/dist/api/appearance.d.ts +37 -0
  6. package/dist/api/appearance.js +41 -0
  7. package/dist/api/cards.d.ts +105 -0
  8. package/dist/api/cards.js +133 -0
  9. package/dist/api/collector.d.ts +114 -0
  10. package/dist/api/collector.js +204 -0
  11. package/dist/api/dungeon.d.ts +58 -0
  12. package/dist/api/dungeon.js +216 -0
  13. package/dist/api/harness.d.ts +149 -0
  14. package/dist/api/harness.js +549 -0
  15. package/dist/api/idle.d.ts +71 -0
  16. package/dist/api/idle.js +104 -0
  17. package/dist/api/race-harness.d.ts +3 -0
  18. package/dist/api/race-harness.js +148 -0
  19. package/dist/api/registry.d.ts +241 -0
  20. package/dist/api/registry.js +394 -0
  21. package/dist/api/scheduler.d.ts +56 -0
  22. package/dist/api/scheduler.js +160 -0
  23. package/dist/api/schemas.d.ts +958 -0
  24. package/dist/api/schemas.js +859 -0
  25. package/dist/api/tower.d.ts +127 -0
  26. package/dist/api/tower.js +183 -0
  27. package/dist/game/adapter.d.ts +69 -0
  28. package/dist/game/adapter.js +341 -0
  29. package/dist/game/arena-bot.d.ts +66 -0
  30. package/dist/game/arena-bot.js +188 -0
  31. package/dist/game/bot.d.ts +95 -0
  32. package/dist/game/bot.js +285 -0
  33. package/dist/game/compile.d.ts +97 -0
  34. package/dist/game/compile.js +476 -0
  35. package/dist/game/director.d.ts +42 -0
  36. package/dist/game/director.js +179 -0
  37. package/dist/game/earn-source.d.ts +122 -0
  38. package/dist/game/earn-source.js +386 -0
  39. package/dist/game/earn.d.ts +49 -0
  40. package/dist/game/earn.js +55 -0
  41. package/dist/game/index.d.ts +37 -0
  42. package/dist/game/index.js +25 -0
  43. package/dist/game/platformer.d.ts +156 -0
  44. package/dist/game/platformer.js +619 -0
  45. package/dist/game/profile.d.ts +97 -0
  46. package/dist/game/profile.js +140 -0
  47. package/dist/game/race.d.ts +177 -0
  48. package/dist/game/race.js +386 -0
  49. package/dist/game/racer.d.ts +98 -0
  50. package/dist/game/racer.js +379 -0
  51. package/dist/game/runner.d.ts +286 -0
  52. package/dist/game/runner.js +824 -0
  53. package/dist/game/threats.d.ts +23 -0
  54. package/dist/game/threats.js +49 -0
  55. package/dist/game/view.d.ts +47 -0
  56. package/dist/game/view.js +123 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +24 -0
  59. package/dist/net/bits.d.ts +33 -0
  60. package/dist/net/bits.js +135 -0
  61. package/dist/net/client.d.ts +75 -0
  62. package/dist/net/client.js +151 -0
  63. package/dist/net/index.d.ts +23 -0
  64. package/dist/net/index.js +23 -0
  65. package/dist/net/input.d.ts +49 -0
  66. package/dist/net/input.js +112 -0
  67. package/dist/net/interpolation.d.ts +37 -0
  68. package/dist/net/interpolation.js +97 -0
  69. package/dist/net/netsim.d.ts +22 -0
  70. package/dist/net/netsim.js +52 -0
  71. package/dist/net/prediction.d.ts +31 -0
  72. package/dist/net/prediction.js +101 -0
  73. package/dist/net/profile-client.d.ts +75 -0
  74. package/dist/net/profile-client.js +173 -0
  75. package/dist/net/protocol.d.ts +208 -0
  76. package/dist/net/protocol.js +8 -0
  77. package/dist/net/room-client.d.ts +57 -0
  78. package/dist/net/room-client.js +144 -0
  79. package/dist/net/schema.d.ts +78 -0
  80. package/dist/net/schema.js +121 -0
  81. package/dist/net/sim-adapter.d.ts +134 -0
  82. package/dist/net/sim-adapter.js +22 -0
  83. package/dist/net/snapshot.d.ts +61 -0
  84. package/dist/net/snapshot.js +301 -0
  85. package/dist/rules/adapter.d.ts +53 -0
  86. package/dist/rules/adapter.js +261 -0
  87. package/dist/rules/cards.d.ts +115 -0
  88. package/dist/rules/cards.js +547 -0
  89. package/dist/rules/collector.d.ts +161 -0
  90. package/dist/rules/collector.js +732 -0
  91. package/dist/rules/genres.d.ts +13 -0
  92. package/dist/rules/genres.js +38 -0
  93. package/dist/rules/harness.d.ts +16 -0
  94. package/dist/rules/harness.js +127 -0
  95. package/dist/rules/idle.d.ts +115 -0
  96. package/dist/rules/idle.js +370 -0
  97. package/dist/rules/index.d.ts +16 -0
  98. package/dist/rules/index.js +14 -0
  99. package/dist/rules/kit.d.ts +171 -0
  100. package/dist/rules/kit.js +88 -0
  101. package/dist/rules/tower.d.ts +214 -0
  102. package/dist/rules/tower.js +655 -0
  103. package/dist/runtime/arena.d.ts +86 -0
  104. package/dist/runtime/arena.js +269 -0
  105. package/dist/runtime/assets.d.ts +26 -0
  106. package/dist/runtime/assets.js +35 -0
  107. package/dist/runtime/camera.d.ts +113 -0
  108. package/dist/runtime/camera.js +247 -0
  109. package/dist/runtime/crowd.d.ts +18 -0
  110. package/dist/runtime/crowd.js +62 -0
  111. package/dist/runtime/feel/audio.d.ts +150 -0
  112. package/dist/runtime/feel/audio.js +327 -0
  113. package/dist/runtime/feel/clock.d.ts +52 -0
  114. package/dist/runtime/feel/clock.js +80 -0
  115. package/dist/runtime/feel/flash.d.ts +28 -0
  116. package/dist/runtime/feel/flash.js +60 -0
  117. package/dist/runtime/feel/index.d.ts +27 -0
  118. package/dist/runtime/feel/index.js +16 -0
  119. package/dist/runtime/feel/kit.d.ts +159 -0
  120. package/dist/runtime/feel/kit.js +226 -0
  121. package/dist/runtime/feel/knockback.d.ts +41 -0
  122. package/dist/runtime/feel/knockback.js +70 -0
  123. package/dist/runtime/feel/noise.d.ts +6 -0
  124. package/dist/runtime/feel/noise.js +30 -0
  125. package/dist/runtime/feel/numbers.d.ts +91 -0
  126. package/dist/runtime/feel/numbers.js +172 -0
  127. package/dist/runtime/feel/rumble.d.ts +40 -0
  128. package/dist/runtime/feel/rumble.js +58 -0
  129. package/dist/runtime/feel/sound.d.ts +96 -0
  130. package/dist/runtime/feel/sound.js +176 -0
  131. package/dist/runtime/feel/spring.d.ts +42 -0
  132. package/dist/runtime/feel/spring.js +95 -0
  133. package/dist/runtime/feel/trauma.d.ts +48 -0
  134. package/dist/runtime/feel/trauma.js +76 -0
  135. package/dist/runtime/feel/tween.d.ts +66 -0
  136. package/dist/runtime/feel/tween.js +180 -0
  137. package/dist/runtime/feel/types.d.ts +30 -0
  138. package/dist/runtime/feel/types.js +5 -0
  139. package/dist/runtime/index.d.ts +28 -0
  140. package/dist/runtime/index.js +26 -0
  141. package/dist/runtime/input.d.ts +38 -0
  142. package/dist/runtime/input.js +159 -0
  143. package/dist/runtime/interpolate.d.ts +6 -0
  144. package/dist/runtime/interpolate.js +13 -0
  145. package/dist/runtime/render/animation.d.ts +63 -0
  146. package/dist/runtime/render/animation.js +128 -0
  147. package/dist/runtime/render/atmosphere.d.ts +32 -0
  148. package/dist/runtime/render/atmosphere.js +59 -0
  149. package/dist/runtime/render/crowd-motion.d.ts +68 -0
  150. package/dist/runtime/render/crowd-motion.js +163 -0
  151. package/dist/runtime/render/decals.d.ts +64 -0
  152. package/dist/runtime/render/decals.js +222 -0
  153. package/dist/runtime/render/highlight.d.ts +29 -0
  154. package/dist/runtime/render/highlight.js +110 -0
  155. package/dist/runtime/render/index.d.ts +35 -0
  156. package/dist/runtime/render/index.js +22 -0
  157. package/dist/runtime/render/lighting.d.ts +50 -0
  158. package/dist/runtime/render/lighting.js +142 -0
  159. package/dist/runtime/render/particles.d.ts +117 -0
  160. package/dist/runtime/render/particles.js +289 -0
  161. package/dist/runtime/render/post.d.ts +92 -0
  162. package/dist/runtime/render/post.js +191 -0
  163. package/dist/runtime/render/presets.d.ts +57 -0
  164. package/dist/runtime/render/presets.js +116 -0
  165. package/dist/runtime/render/quality.d.ts +113 -0
  166. package/dist/runtime/render/quality.js +180 -0
  167. package/dist/runtime/render/rig.d.ts +29 -0
  168. package/dist/runtime/render/rig.js +107 -0
  169. package/dist/runtime/render/sprite.d.ts +34 -0
  170. package/dist/runtime/render/sprite.js +58 -0
  171. package/dist/runtime/render/stack.d.ts +74 -0
  172. package/dist/runtime/render/stack.js +132 -0
  173. package/dist/runtime/render/vat.d.ts +78 -0
  174. package/dist/runtime/render/vat.js +219 -0
  175. package/dist/runtime/renderer.d.ts +19 -0
  176. package/dist/runtime/renderer.js +18 -0
  177. package/dist/runtime/stats.d.ts +11 -0
  178. package/dist/runtime/stats.js +29 -0
  179. package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
  180. package/dist/runtime/styles/handdrawn/ground.js +282 -0
  181. package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
  182. package/dist/runtime/styles/handdrawn/index.js +94 -0
  183. package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
  184. package/dist/runtime/styles/handdrawn/ink.js +68 -0
  185. package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
  186. package/dist/runtime/styles/handdrawn/looks.js +42 -0
  187. package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
  188. package/dist/runtime/styles/handdrawn/paint.js +129 -0
  189. package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
  190. package/dist/runtime/styles/handdrawn/palette.js +70 -0
  191. package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
  192. package/dist/runtime/styles/handdrawn/textures.js +150 -0
  193. package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
  194. package/dist/runtime/styles/handdrawn/vfx.js +73 -0
  195. package/dist/runtime/styles/index.d.ts +9 -0
  196. package/dist/runtime/styles/index.js +32 -0
  197. package/dist/runtime/styles/kit.d.ts +193 -0
  198. package/dist/runtime/styles/kit.js +1 -0
  199. package/dist/runtime/styles/pixel/index.d.ts +46 -0
  200. package/dist/runtime/styles/pixel/index.js +167 -0
  201. package/dist/runtime/styles/ps1/index.d.ts +29 -0
  202. package/dist/runtime/styles/ps1/index.js +41 -0
  203. package/dist/runtime/styles/ps1/lights.d.ts +47 -0
  204. package/dist/runtime/styles/ps1/lights.js +146 -0
  205. package/dist/runtime/styles/ps1/materials.d.ts +62 -0
  206. package/dist/runtime/styles/ps1/materials.js +285 -0
  207. package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
  208. package/dist/runtime/styles/ps1/pipeline.js +111 -0
  209. package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
  210. package/dist/runtime/styles/ps1/surfaces.js +228 -0
  211. package/dist/runtime/styles/toon/index.d.ts +5 -0
  212. package/dist/runtime/styles/toon/index.js +178 -0
  213. package/dist/runtime/styles/toon/ink.d.ts +39 -0
  214. package/dist/runtime/styles/toon/ink.js +73 -0
  215. package/dist/runtime/styles/toon/look.d.ts +10 -0
  216. package/dist/runtime/styles/toon/look.js +112 -0
  217. package/dist/runtime/styles/toon/material.d.ts +74 -0
  218. package/dist/runtime/styles/toon/material.js +208 -0
  219. package/dist/runtime/styles/toon/normals.d.ts +26 -0
  220. package/dist/runtime/styles/toon/normals.js +127 -0
  221. package/dist/runtime/styles/toon/shadows.d.ts +24 -0
  222. package/dist/runtime/styles/toon/shadows.js +42 -0
  223. package/dist/runtime/styles/toon/vfx.d.ts +15 -0
  224. package/dist/runtime/styles/toon/vfx.js +47 -0
  225. package/dist/runtime/styles/view.d.ts +35 -0
  226. package/dist/runtime/styles/view.js +34 -0
  227. package/dist/runtime/styles/voxel/core.d.ts +114 -0
  228. package/dist/runtime/styles/voxel/core.js +577 -0
  229. package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
  230. package/dist/runtime/styles/voxel/geometry.js +40 -0
  231. package/dist/runtime/styles/voxel/index.d.ts +21 -0
  232. package/dist/runtime/styles/voxel/index.js +23 -0
  233. package/dist/runtime/styles/voxel/kit.d.ts +6 -0
  234. package/dist/runtime/styles/voxel/kit.js +11 -0
  235. package/dist/runtime/styles/voxel/look.d.ts +10 -0
  236. package/dist/runtime/styles/voxel/look.js +16 -0
  237. package/dist/runtime/styles/voxel/props.d.ts +9 -0
  238. package/dist/runtime/styles/voxel/props.js +52 -0
  239. package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
  240. package/dist/runtime/styles/voxel/shapes.js +83 -0
  241. package/dist/runtime/styles/voxel/world.d.ts +49 -0
  242. package/dist/runtime/styles/voxel/world.js +422 -0
  243. package/llms.txt +183 -0
  244. package/package.json +82 -3
@@ -0,0 +1,151 @@
1
+ // Transport-agnostic game client: decodes snapshots, predicts the local
2
+ // player, interpolates everyone else, and builds input packets. The browser
3
+ // demo, the headless load bots and the netcode tests all drive this class;
4
+ // the Colyseus wiring is only `send` and `receiveSnapshot`.
5
+ import { encodeInputPacket, REDUNDANCY } from './input.js';
6
+ import { InterpolationBuffer, ServerClock } from './interpolation.js';
7
+ import { Predictor } from './prediction.js';
8
+ import { dequantizeValues } from './schema.js';
9
+ import { BaselineMissingError, decodeSnapshot, SnapshotHistory } from './snapshot.js';
10
+ export const DEFAULT_CLIENT = { interpDelayMs: 100, inputBufferTarget: 2 };
11
+ export class NetClient {
12
+ sim;
13
+ player;
14
+ selfEntity;
15
+ send;
16
+ opts;
17
+ history = new SnapshotHistory(64);
18
+ interp;
19
+ clock;
20
+ predictor;
21
+ latestTick = 0;
22
+ latestSelf;
23
+ lastRenderMs = 0;
24
+ tickMs;
25
+ /** Smoothed server input-queue depth from snapshot headers; null until reported. */
26
+ serverBuffer = null;
27
+ stats = {
28
+ snapshots: 0,
29
+ snapshotBytes: 0,
30
+ inputPackets: 0,
31
+ inputBytes: 0,
32
+ baselineMisses: 0,
33
+ staleSnapshots: 0,
34
+ };
35
+ constructor(sim, player, selfEntity, send, opts = DEFAULT_CLIENT) {
36
+ this.sim = sim;
37
+ this.player = player;
38
+ this.selfEntity = selfEntity;
39
+ this.send = send;
40
+ this.opts = opts;
41
+ this.tickMs = 1000 / sim.tickHz;
42
+ this.interp = new InterpolationBuffer(sim.schema);
43
+ this.clock = new ServerClock(this.tickMs);
44
+ this.predictor = new Predictor(sim, player, selfEntity, 0, opts.firstSeq ?? 1);
45
+ }
46
+ /** The seq the next input gets; pass as `firstSeq` to a client that replaces this one. */
47
+ get nextSeq() {
48
+ return this.predictor.upcomingSeq;
49
+ }
50
+ receiveSnapshot(bytes, nowMs) {
51
+ let snap;
52
+ try {
53
+ snap = decodeSnapshot(this.sim.schema, bytes, (t) => this.history.get(t));
54
+ }
55
+ catch (err) {
56
+ if (err instanceof BaselineMissingError) {
57
+ // Ask for a full snapshot by acking nothing until one arrives.
58
+ this.stats.baselineMisses++;
59
+ this.latestTick = 0;
60
+ return;
61
+ }
62
+ throw err;
63
+ }
64
+ this.stats.snapshots++;
65
+ this.stats.snapshotBytes += bytes.length;
66
+ const { header, entities } = snap;
67
+ if (header.tick <= this.latestTick) {
68
+ this.stats.staleSnapshots++;
69
+ return;
70
+ }
71
+ this.latestTick = header.tick;
72
+ if (header.inputBuffer !== undefined) {
73
+ this.serverBuffer = this.serverBuffer === null ? header.inputBuffer : this.serverBuffer + (header.inputBuffer - this.serverBuffer) * 0.1;
74
+ }
75
+ this.history.put(header.tick, entities);
76
+ this.clock.observe(header.tick, nowMs);
77
+ this.interp.push(header.tick, entities);
78
+ const self = entities.get(this.selfEntity);
79
+ this.latestSelf = self;
80
+ if (self)
81
+ this.predictor.reconcile(header.ackSeq, this.toState(self));
82
+ }
83
+ /** Call once per sim tick with the sampled input. Sends the input packet. */
84
+ tick(input, nowMs) {
85
+ const viewTick16 = Math.max(0, Math.round(this.renderTick(nowMs) * 16));
86
+ this.predictor.predict(input, viewTick16);
87
+ const pending = this.predictor.unacked();
88
+ const commands = pending.slice(Math.max(0, pending.length - REDUNDANCY));
89
+ const bytes = encodeInputPacket(this.sim.input, { ackTick: this.latestTick, commands });
90
+ this.stats.inputPackets++;
91
+ this.stats.inputBytes += bytes.length;
92
+ this.send(bytes);
93
+ }
94
+ /**
95
+ * Milliseconds until the next input tick. Equal to the sim tick when the
96
+ * server reports no input queue; otherwise up to 8% shorter when the
97
+ * server's queue for us runs below target (we are behind, so input would
98
+ * starve) and up to 8% longer when it runs above (added latency). This is
99
+ * the client clock adjustment for one-input-per-tick sims.
100
+ */
101
+ tickIntervalMs() {
102
+ if (this.serverBuffer === null)
103
+ return this.tickMs;
104
+ const target = this.opts.inputBufferTarget ?? 2;
105
+ const scale = 1 + Math.max(-0.08, Math.min(0.08, (this.serverBuffer - target) * 0.03));
106
+ return this.tickMs * scale;
107
+ }
108
+ /** Smoothed input-queue depth the server last reported, or null. */
109
+ get serverInputBuffer() {
110
+ return this.serverBuffer;
111
+ }
112
+ /** Server tick (fractional) that remote entities are drawn at. */
113
+ renderTick(nowMs) {
114
+ return this.clock.serverTick(nowMs) - this.opts.interpDelayMs / this.tickMs;
115
+ }
116
+ render(nowMs) {
117
+ const dt = this.lastRenderMs === 0 ? 0 : nowMs - this.lastRenderMs;
118
+ this.lastRenderMs = nowMs;
119
+ return {
120
+ self: this.predictor.renderState(dt),
121
+ others: this.interp.sample(this.renderTick(nowMs), this.selfEntity),
122
+ };
123
+ }
124
+ /** The newest server state of our own entity (what prediction is ahead of). */
125
+ authoritativeSelf() {
126
+ return this.latestSelf ? this.toState(this.latestSelf) : undefined;
127
+ }
128
+ /** Our own entity drawn like a remote one (what you'd see with no prediction). */
129
+ interpolatedSelf(nowMs) {
130
+ return this.interp.sample(this.renderTick(nowMs)).find((e) => e.id === this.selfEntity);
131
+ }
132
+ /** Exact predicted state (no smoothing). */
133
+ predicted() {
134
+ return this.predictor.state();
135
+ }
136
+ get ready() {
137
+ return this.predictor.isReady;
138
+ }
139
+ get corrections() {
140
+ return this.predictor.corrections;
141
+ }
142
+ get lastCorrection() {
143
+ return this.predictor.lastCorrection;
144
+ }
145
+ get pendingInputs() {
146
+ return this.predictor.unacked().length;
147
+ }
148
+ toState(e) {
149
+ return { id: e.id, kind: e.kind, owner: e.owner, values: dequantizeValues(this.sim.schema, e.q) };
150
+ }
151
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @netforge/engine/net: the client half of the netcode and everything
3
+ * the room server shares with it: the SimAdapter seam,
4
+ * schema-driven bit-packed delta snapshots, sequence-numbered input packets,
5
+ * prediction and reconciliation, interpolation, the server clock estimate,
6
+ * the room wire protocol, and the Colyseus room client.
7
+ *
8
+ * It lives in the engine so templates and the Net Forge site can use it
9
+ * without depending on the room server; the server-only parts (ServerCore,
10
+ * AoI, lag-compensation rewind) stay in the hosted Net Forge room server.
11
+ */
12
+ export * from './bits.js';
13
+ export * from './schema.js';
14
+ export * from './snapshot.js';
15
+ export * from './input.js';
16
+ export * from './interpolation.js';
17
+ export * from './prediction.js';
18
+ export * from './client.js';
19
+ export * from './sim-adapter.js';
20
+ export * from './netsim.js';
21
+ export * from './protocol.js';
22
+ export * from './room-client.js';
23
+ export * from "./profile-client.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @netforge/engine/net: the client half of the netcode and everything
3
+ * the room server shares with it: the SimAdapter seam,
4
+ * schema-driven bit-packed delta snapshots, sequence-numbered input packets,
5
+ * prediction and reconciliation, interpolation, the server clock estimate,
6
+ * the room wire protocol, and the Colyseus room client.
7
+ *
8
+ * It lives in the engine so templates and the Net Forge site can use it
9
+ * without depending on the room server; the server-only parts (ServerCore,
10
+ * AoI, lag-compensation rewind) stay in the hosted Net Forge room server.
11
+ */
12
+ export * from './bits.js';
13
+ export * from './schema.js';
14
+ export * from './snapshot.js';
15
+ export * from './input.js';
16
+ export * from './interpolation.js';
17
+ export * from './prediction.js';
18
+ export * from './client.js';
19
+ export * from './sim-adapter.js';
20
+ export * from './netsim.js';
21
+ export * from './protocol.js';
22
+ export * from './room-client.js';
23
+ export * from "./profile-client.js";
@@ -0,0 +1,49 @@
1
+ import type { InputCodec } from './sim-adapter.js';
2
+ export interface InputCommand<I> {
3
+ readonly seq: number;
4
+ /** Render tick the client was showing when it sampled this input, x16. */
5
+ readonly viewTick16: number;
6
+ readonly input: I;
7
+ }
8
+ export interface InputPacket<I> {
9
+ /** Highest snapshot tick the client has decoded (its delta baseline). */
10
+ readonly ackTick: number;
11
+ readonly commands: readonly InputCommand<I>[];
12
+ }
13
+ /** How many unacked commands each packet repeats. */
14
+ export declare const REDUNDANCY = 4;
15
+ export declare function encodeInputPacket<I>(codec: InputCodec<I>, p: InputPacket<I>): Uint8Array;
16
+ export declare function decodeInputPacket<I>(codec: InputCodec<I>, bytes: Uint8Array): InputPacket<I>;
17
+ export interface InputQueueOptions {
18
+ /** Most commands waiting at once; extra are dropped (client gets corrected). */
19
+ readonly maxQueue: number;
20
+ /** Credit bucket size: how far a player may catch up after a stall. */
21
+ readonly maxCredits: number;
22
+ /**
23
+ * Jitter buffer for one-input-per-tick sims: after the queue runs dry, hold
24
+ * input until this many commands are waiting, then resume one per tick.
25
+ * 0 (default) consumes as soon as anything arrives. Clients hold the depth
26
+ * near this target with `NetClient.tickIntervalMs()`.
27
+ */
28
+ readonly targetDepth?: number;
29
+ }
30
+ export declare const DEFAULT_INPUT_QUEUE: InputQueueOptions;
31
+ /** Server-side per-player input queue with speed-hack protection. */
32
+ export declare class InputQueue<I> {
33
+ private readonly opts;
34
+ private readonly queue;
35
+ private lastQueuedSeq;
36
+ private credits;
37
+ /** Highest seq applied by the sim; echoed to the client as ackSeq. */
38
+ lastAppliedSeq: number;
39
+ /** Commands dropped because the queue was full (telemetry). */
40
+ dropped: number;
41
+ /** Ticks the queue was empty when the sim needed a command (telemetry). */
42
+ starved: number;
43
+ private refilling;
44
+ constructor(opts?: InputQueueOptions);
45
+ receive(commands: readonly InputCommand<I>[]): void;
46
+ /** Commands to apply this tick (0..maxCredits of them). */
47
+ consume(): InputCommand<I>[];
48
+ get pending(): number;
49
+ }
@@ -0,0 +1,112 @@
1
+ // Sequence-numbered input commands.
2
+ //
3
+ // The client produces one command per local tick and sends, every tick, all
4
+ // commands the server has not yet acknowledged (capped at REDUNDANCY), so a
5
+ // lost packet is covered by the next one. The server queues commands in
6
+ // sequence order, ignores ones it already has, and consumes them at no more
7
+ // than one per tick on average (a credit bucket), which is what stops a
8
+ // modified client from moving faster by sending more inputs.
9
+ import { BitReader, BitWriter } from './bits.js';
10
+ /** How many unacked commands each packet repeats. */
11
+ export const REDUNDANCY = 4;
12
+ export function encodeInputPacket(codec, p) {
13
+ const w = new BitWriter(64);
14
+ w.writeBits(p.ackTick, 32);
15
+ w.writeVarUint(p.commands.length);
16
+ const first = p.commands[0];
17
+ if (first) {
18
+ w.writeBits(first.seq, 32);
19
+ w.writeBits(first.viewTick16, 32);
20
+ }
21
+ let prevView = first?.viewTick16 ?? 0;
22
+ for (let i = 0; i < p.commands.length; i++) {
23
+ const c = p.commands[i];
24
+ if (!c)
25
+ continue;
26
+ if (first && c.seq !== first.seq + i)
27
+ throw new Error('input commands must have consecutive seqs');
28
+ if (i > 0) {
29
+ w.writeSigned(c.viewTick16 - prevView, 16);
30
+ prevView = c.viewTick16;
31
+ }
32
+ codec.write(w, c.input);
33
+ }
34
+ return w.finish();
35
+ }
36
+ export function decodeInputPacket(codec, bytes) {
37
+ const r = new BitReader(bytes);
38
+ const ackTick = r.readBits(32);
39
+ const n = r.readVarUint();
40
+ if (n > 64)
41
+ throw new Error('input packet: too many commands');
42
+ const commands = [];
43
+ if (n > 0) {
44
+ const firstSeq = r.readBits(32);
45
+ let view = r.readBits(32);
46
+ for (let i = 0; i < n; i++) {
47
+ if (i > 0)
48
+ view += r.readSigned(16);
49
+ commands.push({ seq: firstSeq + i, viewTick16: view, input: codec.read(r) });
50
+ }
51
+ }
52
+ return { ackTick, commands };
53
+ }
54
+ export const DEFAULT_INPUT_QUEUE = { maxQueue: 16, maxCredits: 6 };
55
+ /** Server-side per-player input queue with speed-hack protection. */
56
+ export class InputQueue {
57
+ opts;
58
+ queue = [];
59
+ lastQueuedSeq = 0;
60
+ credits = 1;
61
+ /** Highest seq applied by the sim; echoed to the client as ackSeq. */
62
+ lastAppliedSeq = 0;
63
+ /** Commands dropped because the queue was full (telemetry). */
64
+ dropped = 0;
65
+ /** Ticks the queue was empty when the sim needed a command (telemetry). */
66
+ starved = 0;
67
+ refilling = true;
68
+ constructor(opts = DEFAULT_INPUT_QUEUE) {
69
+ this.opts = opts;
70
+ }
71
+ receive(commands) {
72
+ for (const c of commands) {
73
+ if (c.seq <= this.lastQueuedSeq)
74
+ continue;
75
+ if (this.queue.length >= this.opts.maxQueue) {
76
+ this.dropped++;
77
+ this.lastQueuedSeq = c.seq;
78
+ continue;
79
+ }
80
+ this.queue.push(c);
81
+ this.lastQueuedSeq = c.seq;
82
+ }
83
+ }
84
+ /** Commands to apply this tick (0..maxCredits of them). */
85
+ consume() {
86
+ const target = this.opts.targetDepth ?? 0;
87
+ if (target > 0) {
88
+ if (this.queue.length === 0) {
89
+ if (!this.refilling && this.lastAppliedSeq > 0)
90
+ this.starved++;
91
+ this.refilling = true;
92
+ return [];
93
+ }
94
+ if (this.refilling && this.queue.length < target)
95
+ return [];
96
+ this.refilling = false;
97
+ }
98
+ this.credits = Math.min(this.credits + 1, this.opts.maxCredits);
99
+ const n = Math.min(this.queue.length, Math.floor(this.credits));
100
+ if (n === 0)
101
+ return [];
102
+ this.credits -= n;
103
+ const out = this.queue.splice(0, n);
104
+ const last = out[out.length - 1];
105
+ if (last)
106
+ this.lastAppliedSeq = last.seq;
107
+ return out;
108
+ }
109
+ get pending() {
110
+ return this.queue.length;
111
+ }
112
+ }
@@ -0,0 +1,37 @@
1
+ import { type EntitySchema } from './schema.js';
2
+ import type { QEntity } from './snapshot.js';
3
+ export interface InterpolatedEntity {
4
+ readonly id: number;
5
+ readonly kind: number;
6
+ readonly owner: number;
7
+ readonly values: readonly number[];
8
+ }
9
+ /**
10
+ * Tracks server time from snapshot arrivals. The offset (server ms minus
11
+ * local ms) is taken from the freshest-looking arrival, eased toward later
12
+ * samples so jitter does not make the render time jump.
13
+ */
14
+ export declare class ServerClock {
15
+ private readonly tickMs;
16
+ private offset;
17
+ constructor(tickMs: number);
18
+ observe(tick: number, localMs: number): void;
19
+ /** Estimated server tick (fractional) at local time. */
20
+ serverTick(localMs: number): number;
21
+ get ready(): boolean;
22
+ }
23
+ export declare class InterpolationBuffer {
24
+ private readonly schema;
25
+ private readonly capacity;
26
+ private readonly frames;
27
+ constructor(schema: EntitySchema, capacity?: number);
28
+ push(tick: number, entities: ReadonlyMap<number, QEntity>): void;
29
+ /**
30
+ * Entities at a fractional render tick. Entities present only in the
31
+ * newer frame pop in; ones only in the older frame are held until gone.
32
+ * Past the newest frame we hold the newest state (no extrapolation).
33
+ */
34
+ sample(renderTick: number, exclude?: number): InterpolatedEntity[];
35
+ get newestTick(): number;
36
+ private frameValues;
37
+ }
@@ -0,0 +1,97 @@
1
+ // Interpolation of remote entities ~100 ms in the past, and the client's
2
+ // estimate of the server's clock that decides "the past".
3
+ import { dequantizeValues, interpolateField } from './schema.js';
4
+ /**
5
+ * Tracks server time from snapshot arrivals. The offset (server ms minus
6
+ * local ms) is taken from the freshest-looking arrival, eased toward later
7
+ * samples so jitter does not make the render time jump.
8
+ */
9
+ export class ServerClock {
10
+ tickMs;
11
+ offset = null;
12
+ constructor(tickMs) {
13
+ this.tickMs = tickMs;
14
+ }
15
+ observe(tick, localMs) {
16
+ const sample = tick * this.tickMs - localMs;
17
+ if (this.offset === null || Math.abs(sample - this.offset) > 250) {
18
+ this.offset = sample;
19
+ return;
20
+ }
21
+ // Earlier-than-expected arrivals mean lower latency: move fast toward them.
22
+ // Late ones are jitter: move slowly.
23
+ const k = sample > this.offset ? 0.2 : 0.02;
24
+ this.offset += (sample - this.offset) * k;
25
+ }
26
+ /** Estimated server tick (fractional) at local time. */
27
+ serverTick(localMs) {
28
+ return this.offset === null ? 0 : (localMs + this.offset) / this.tickMs;
29
+ }
30
+ get ready() {
31
+ return this.offset !== null;
32
+ }
33
+ }
34
+ export class InterpolationBuffer {
35
+ schema;
36
+ capacity;
37
+ frames = [];
38
+ constructor(schema, capacity = 32) {
39
+ this.schema = schema;
40
+ this.capacity = capacity;
41
+ }
42
+ push(tick, entities) {
43
+ const last = this.frames[this.frames.length - 1];
44
+ if (last && tick <= last.tick)
45
+ return;
46
+ this.frames.push({ tick, entities });
47
+ while (this.frames.length > this.capacity)
48
+ this.frames.shift();
49
+ }
50
+ /**
51
+ * Entities at a fractional render tick. Entities present only in the
52
+ * newer frame pop in; ones only in the older frame are held until gone.
53
+ * Past the newest frame we hold the newest state (no extrapolation).
54
+ */
55
+ sample(renderTick, exclude) {
56
+ const n = this.frames.length;
57
+ if (n === 0)
58
+ return [];
59
+ let i = n - 1;
60
+ while (i > 0 && (this.frames[i]?.tick ?? 0) > renderTick)
61
+ i--;
62
+ const a = this.frames[i];
63
+ const b = this.frames[i + 1];
64
+ if (!a)
65
+ return [];
66
+ if (!b || renderTick <= a.tick)
67
+ return this.frameValues(a, exclude);
68
+ const t = (renderTick - a.tick) / (b.tick - a.tick);
69
+ const out = [];
70
+ for (const eb of b.entities.values()) {
71
+ if (eb.id === exclude)
72
+ continue;
73
+ const ea = a.entities.get(eb.id);
74
+ const vb = dequantizeValues(this.schema, eb.q);
75
+ if (!ea) {
76
+ out.push({ id: eb.id, kind: eb.kind, owner: eb.owner, values: vb });
77
+ continue;
78
+ }
79
+ const va = dequantizeValues(this.schema, ea.q);
80
+ const values = this.schema.fields.map((f, k) => interpolateField(f, va[k] ?? 0, vb[k] ?? 0, t));
81
+ out.push({ id: eb.id, kind: eb.kind, owner: eb.owner, values });
82
+ }
83
+ return out;
84
+ }
85
+ get newestTick() {
86
+ return this.frames[this.frames.length - 1]?.tick ?? 0;
87
+ }
88
+ frameValues(f, exclude) {
89
+ const out = [];
90
+ for (const e of f.entities.values()) {
91
+ if (e.id === exclude)
92
+ continue;
93
+ out.push({ id: e.id, kind: e.kind, owner: e.owner, values: dequantizeValues(this.schema, e.q) });
94
+ }
95
+ return out;
96
+ }
97
+ }
@@ -0,0 +1,22 @@
1
+ export interface LinkOptions {
2
+ /** Base one-way latency, ms. */
3
+ readonly latencyMs: number;
4
+ /** Uniform extra delay in [0, jitterMs). */
5
+ readonly jitterMs: number;
6
+ /** Probability a packet is dropped. */
7
+ readonly loss: number;
8
+ }
9
+ export declare class VirtualNet {
10
+ now: number;
11
+ private queue;
12
+ private seq;
13
+ private rng;
14
+ sent: number;
15
+ dropped: number;
16
+ constructor(seed?: number);
17
+ random(): number;
18
+ /** Sends over a link; `deliver` runs at arrival time unless dropped. */
19
+ send(link: LinkOptions, deliver: () => void): void;
20
+ /** Advances the clock to `t`, delivering everything due, in arrival order. */
21
+ advanceTo(t: number): void;
22
+ }
@@ -0,0 +1,52 @@
1
+ // A deterministic simulated network for netcode tests: virtual clock,
2
+ // one-way latency, jitter (so packets reorder), and random loss. Models the
3
+ // unreliable-datagram case (WebTransport), which is strictly harder than the
4
+ // WebSocket case (in order, no loss, latency spikes instead).
5
+ export class VirtualNet {
6
+ now = 0;
7
+ queue = [];
8
+ seq = 0;
9
+ rng;
10
+ sent = 0;
11
+ dropped = 0;
12
+ constructor(seed = 12345) {
13
+ this.rng = seed;
14
+ }
15
+ random() {
16
+ this.rng = (this.rng + 0x6d2b79f5) | 0;
17
+ let t = this.rng;
18
+ t = Math.imul(t ^ (t >>> 15), t | 1);
19
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
20
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
21
+ }
22
+ /** Sends over a link; `deliver` runs at arrival time unless dropped. */
23
+ send(link, deliver) {
24
+ this.sent++;
25
+ if (this.random() < link.loss) {
26
+ this.dropped++;
27
+ return;
28
+ }
29
+ const at = this.now + link.latencyMs + this.random() * link.jitterMs;
30
+ this.queue.push({ at, seq: this.seq++, deliver });
31
+ }
32
+ /** Advances the clock to `t`, delivering everything due, in arrival order. */
33
+ advanceTo(t) {
34
+ for (;;) {
35
+ let best = -1;
36
+ for (let i = 0; i < this.queue.length; i++) {
37
+ const q = this.queue[i];
38
+ const b = best >= 0 ? this.queue[best] : undefined;
39
+ if (q && q.at <= t && (!b || q.at < b.at || (q.at === b.at && q.seq < b.seq)))
40
+ best = i;
41
+ }
42
+ if (best < 0)
43
+ break;
44
+ const [item] = this.queue.splice(best, 1);
45
+ if (!item)
46
+ break;
47
+ this.now = item.at;
48
+ item.deliver();
49
+ }
50
+ this.now = t;
51
+ }
52
+ }
@@ -0,0 +1,31 @@
1
+ import type { EntityState } from './schema.js';
2
+ import type { InputCommand } from './input.js';
3
+ import type { PlayerId, SimAdapter } from './sim-adapter.js';
4
+ export declare class Predictor<W, I> {
5
+ private readonly sim;
6
+ private readonly player;
7
+ private readonly selfEntity;
8
+ private readonly world;
9
+ private pending;
10
+ private nextSeq;
11
+ private ready;
12
+ private offsetX;
13
+ private offsetY;
14
+ /** Magnitude of the last correction in world units (telemetry/tests). */
15
+ lastCorrection: number;
16
+ /** Count of reconciliations that moved the predicted entity. */
17
+ corrections: number;
18
+ constructor(sim: SimAdapter<W, I>, player: PlayerId, selfEntity: number, seed?: number, firstSeq?: number);
19
+ /** The seq the next predicted command gets (a rebased predictor continues from here). */
20
+ get upcomingSeq(): number;
21
+ get isReady(): boolean;
22
+ /** Applies one tick of input locally. Returns the command to send. */
23
+ predict(input: I, viewTick16: number): InputCommand<I> | null;
24
+ /** Unacknowledged commands, oldest first. */
25
+ unacked(): readonly InputCommand<I>[];
26
+ reconcile(ackSeq: number, serverSelf: EntityState): void;
27
+ /** The predicted entity, exact (what reconciliation converges on). */
28
+ state(): EntityState | undefined;
29
+ /** The predicted entity with the correction offset applied, for drawing. */
30
+ renderState(dtMs: number): EntityState | undefined;
31
+ }