@series-inc/rundot-kinetix 0.0.0-bootstrap.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 (58) hide show
  1. package/README.md +77 -0
  2. package/authoring.d.ts +30 -0
  3. package/index.d.ts +221 -0
  4. package/native/component_runtime.cpp +341 -0
  5. package/native/component_runtime.hpp +112 -0
  6. package/native/deterministic_math.cpp +594 -0
  7. package/native/deterministic_math.hpp +21 -0
  8. package/native/kinetix-f64-native-profile.cpp +406 -0
  9. package/native/kinetix-f64-native-profile.hpp +5 -0
  10. package/native/kinetix-fixed1000-native-profile.cpp +777 -0
  11. package/native/kinetix-fixed1000-native-profile.hpp +58 -0
  12. package/native/kinetix-fixed1000-physics.cpp +887 -0
  13. package/native/kinetix-fixed1000-physics.hpp +28 -0
  14. package/native/kinetix-installed-f64-renderer.cpp +344 -0
  15. package/native/kinetix-installed-f64-renderer.hpp +35 -0
  16. package/native/kinetix-installed-f64-runtime.cpp +1085 -0
  17. package/native/kinetix-installed-f64-runtime.hpp +141 -0
  18. package/native/kinetix-installed-fixed1000-data.hpp +77 -0
  19. package/native/kinetix-native-main.cpp +37 -0
  20. package/native/kinetix-native-runtime.cpp +20 -0
  21. package/native/kinetix-native-runtime.hpp +25 -0
  22. package/native/kinetix-render-projection.cpp +20 -0
  23. package/native/kinetix-render-projection.hpp +14 -0
  24. package/package.json +65 -0
  25. package/runtime.d.ts +76 -0
  26. package/scripts/build-native.mjs +67 -0
  27. package/scripts/emit-product-digests.mjs +33 -0
  28. package/scripts/preflight.mjs +76 -0
  29. package/src/index.mjs +57 -0
  30. package/src/kinetix-authoring.mjs +69 -0
  31. package/src/kinetix-baker.mjs +587 -0
  32. package/src/kinetix-deterministic-math.mjs +1044 -0
  33. package/src/kinetix-fixed1000-runtime-adapter.mjs +33 -0
  34. package/src/kinetix-fixed1000-runtime.mjs +954 -0
  35. package/src/kinetix-installed-f64-reference.mjs +157 -0
  36. package/src/kinetix-installed-f64-render-frames.mjs +53 -0
  37. package/src/kinetix-installed-f64-renderer.mjs +240 -0
  38. package/src/kinetix-installed-f64-runtime-adapter.mjs +68 -0
  39. package/src/kinetix-installed-f64-runtime.mjs +607 -0
  40. package/src/kinetix-installed-mechanics.mjs +377 -0
  41. package/src/kinetix-installed-systems.mjs +181 -0
  42. package/src/kinetix-native-product.mjs +72 -0
  43. package/src/kinetix-product-contract.mjs +121 -0
  44. package/src/kinetix-project-compiler.mjs +1017 -0
  45. package/src/kinetix-render-projection.mjs +28 -0
  46. package/src/kinetix-runtime-adapter-utils.mjs +168 -0
  47. package/src/kinetix-runtime-contract.mjs +54 -0
  48. package/src/kinetix-session-config.mjs +170 -0
  49. package/src/kinetix-source-snapshot.mjs +24 -0
  50. package/src/kinetix-survival-runtime-adapter.mjs +305 -0
  51. package/src/kinetix-survival-runtime.generated.mjs +1 -0
  52. package/src/kinetix-world-kernel.mjs +580 -0
  53. package/src/kinetix-world-runtime.mjs +171 -0
  54. package/src/runtime.mjs +14 -0
  55. package/src/shared/f64-bits.mjs +14 -0
  56. package/src/shared/kinetix-envelope-v1.mjs +589 -0
  57. package/src/shared/render-records-v1.mjs +168 -0
  58. package/src/shared/sha256.mjs +73 -0
@@ -0,0 +1,777 @@
1
+ #include "kinetix-installed-fixed1000-data.hpp"
2
+ #include "kinetix-fixed1000-native-profile.hpp"
3
+ #include "component_runtime.hpp"
4
+ // The render configuration consumes a render-records encoder and a camera-follow
5
+ // core that the CORE does not own — the host supplies them on its include path.
6
+ // This package builds only the simulation configuration, where the macro below is
7
+ // defined and these are compiled out; a host that wants the render configuration
8
+ // (today: the console lab's Metal player) must -I the directory holding them.
9
+ #ifndef KINETIX_FIXED1000_SIM_ONLY
10
+ #include "actor_follow_core.hpp"
11
+ #include "render_records_encoder.hpp"
12
+ #endif
13
+ #include "kinetix-fixed1000-physics.hpp"
14
+
15
+ #include <algorithm>
16
+ #include <array>
17
+ #include <cmath>
18
+ #include <cstdint>
19
+ #include <cstdio>
20
+ #include <cstdlib>
21
+ #include <fstream>
22
+ #include <limits>
23
+ #include <map>
24
+ #include <memory>
25
+ #include <mutex>
26
+ #include <sstream>
27
+ #include <string>
28
+ #include <utility>
29
+ #include <vector>
30
+
31
+ namespace {
32
+ using namespace rundot::kinetix::fixed1000_data;
33
+ using I = std::int64_t;
34
+ constexpr int kLobby = 0;
35
+ constexpr int kCountdown = 1;
36
+ constexpr int kActive = 2;
37
+ constexpr int kFinished = 3;
38
+
39
+ std::uint32_t readU32(std::istream& input) {
40
+ std::uint32_t value = 0;
41
+ for (int shift = 0; shift < 32; shift += 8) {
42
+ const int byte = input.get();
43
+ if (byte == EOF) std::abort();
44
+ value |= static_cast<std::uint32_t>(static_cast<std::uint8_t>(byte)) << shift;
45
+ }
46
+ return value;
47
+ }
48
+
49
+ I readI64(std::istream& input) {
50
+ std::uint64_t value = 0;
51
+ for (int shift = 0; shift < 64; shift += 8) {
52
+ const int byte = input.get();
53
+ if (byte == EOF) std::abort();
54
+ value |= static_cast<std::uint64_t>(static_cast<std::uint8_t>(byte)) << shift;
55
+ }
56
+ return static_cast<I>(value);
57
+ }
58
+
59
+ std::string readString(std::istream& input) {
60
+ const std::uint32_t length = readU32(input);
61
+ std::string value(length, '\0');
62
+ input.read(value.data(), static_cast<std::streamsize>(length));
63
+ if (!input) std::abort();
64
+ return value;
65
+ }
66
+
67
+ std::vector<I> readI64Vector(std::istream& input) {
68
+ std::vector<I> values(readU32(input));
69
+ for (I& value : values) value = readI64(input);
70
+ return values;
71
+ }
72
+
73
+ void loadInstalledProfileData(std::istream& input) {
74
+ std::array<char, 8> magic{};
75
+ input.read(magic.data(), static_cast<std::streamsize>(magic.size()));
76
+ if (std::string(magic.data(), magic.size()) != "KFXDATA1") std::abort();
77
+ kTrack = readString(input);
78
+ const std::array<I*, 57> scalars = {
79
+ &kScale, &kSlotCount, &kHumanCount, &kInitialPhase,
80
+ &kCellRaw, &kGridScale, &kFinishTypeId,
81
+ &kMaxSpeed, &kAccelRate, &kBrakeRate, &kMaxReverse, &kTurnPerTick,
82
+ &kGripMin, &kSlipSpeed, &kTractionSlow, &kTractionFast, &kDriftSteer, &kStopDeadband,
83
+ &kWallHalfThick, &kWallLateral, &kWallY, &kWallHalfHeightRaw, &kHalfPiFixed,
84
+ &kTwoPiFixed, &kOuterArcSegments, &kInnerArcSegments, &kColliderLayer, &kColliderMask,
85
+ &kArenaHalfUnits, &kArenaWallThick, &kArenaWallHalfHeight, &kArenaWallY,
86
+ &kArenaCenterX, &kArenaCenterZ, &kScatterCols, &kScatterSpacing,
87
+ &kGridSpawnLateral, &kGridSpawnRowBack, &kGridSpawnMinSlots, &kGridSpawnMaxSlots,
88
+ &kWaypointLookahead, &kWaypointSteerGain, &kWaypointSharpTurnThrottle,
89
+ &kTotalLaps, &kMaxRaceFrames, &kNoTeleport, &kMaxRequiredBits,
90
+ &kLobbyInitialFrames, &kLobbyExtendFrames, &kLobbyMaxFrames, &kCountdownFrames,
91
+ &kResultsHoldFrames, &kMaxSlotsAutoStart, &kBodyRadius, &kBodyY, &kBodyLayer, &kBodyMask,
92
+ };
93
+ for (I* scalar : scalars) *scalar = readI64(input);
94
+ kCells.resize(readU32(input));
95
+ for (Cell& cell : kCells) cell = {readI64(input), readI64(input), readI64(input), readI64(input)};
96
+ kCornerTypeIds = readI64Vector(input);
97
+ kPassThroughTypeIds = readI64Vector(input);
98
+ kWallBodyPrefix = readString(input);
99
+ kCarBodyPrefix = readString(input);
100
+ if (readU32(input) != kArenaBodyIds.size()) std::abort();
101
+ for (std::string& id : kArenaBodyIds) id = readString(input);
102
+ if (input.get() != EOF || kScale <= 0 || kSlotCount < 1
103
+ || static_cast<std::size_t>(kSlotCount) > kMaxSlots) std::abort();
104
+ }
105
+
106
+ std::string readKinetixProductsProfile(const std::string& path) {
107
+ std::ifstream input(path, std::ios::binary);
108
+ if (!input) std::abort();
109
+ std::array<char, 8> magic{};
110
+ input.read(magic.data(), static_cast<std::streamsize>(magic.size()));
111
+ if (std::string(magic.data(), magic.size()) != "KNATIVE1") std::abort();
112
+ const std::string sourceDigest = readString(input);
113
+ const std::string simulationDigest = readString(input);
114
+ if (sourceDigest.size() != 64 || simulationDigest.size() != 64) std::abort();
115
+ const std::array<std::string, 5> ids = {
116
+ "slot-input-resolution", "vehicle-drive-body-build", "vehicle-physics-step",
117
+ "vehicle-lap-finish", "match-phase-transition",
118
+ };
119
+ const std::array<std::string, 5> phases = {
120
+ "fixed-simulation", "fixed-simulation", "fixed-simulation",
121
+ "fixed-simulation", "post-simulation",
122
+ };
123
+ const std::array<I, 5> phaseIndices = {1, 1, 1, 1, 2};
124
+ const std::array<I, 5> orders = {100, 200, 300, 400, 500};
125
+ if (readU32(input) != ids.size()) std::abort();
126
+ for (std::size_t index = 0; index < ids.size(); index += 1) {
127
+ if (readString(input) != ids[index] || readString(input) != phases[index]
128
+ || readI64(input) != phaseIndices[index] || readI64(input) != orders[index]) std::abort();
129
+ }
130
+ const std::string profileDigest = readString(input);
131
+ const std::uint32_t profileLength = readU32(input);
132
+ std::string profile(profileLength, '\0');
133
+ input.read(profile.data(), static_cast<std::streamsize>(profile.size()));
134
+ if (!input || input.get() != EOF || profileDigest.size() != 64
135
+ || rundot::sha256Hex(reinterpret_cast<const std::uint8_t*>(profile.data()), profile.size()) != profileDigest) std::abort();
136
+ return profile;
137
+ }
138
+
139
+ void activateInstalledProfile(const std::string& profile) {
140
+ std::istringstream profileInput(profile, std::ios::binary);
141
+ loadInstalledProfileData(profileInput);
142
+ }
143
+
144
+ std::mutex kInstalledProfileMutex;
145
+
146
+ void loadKinetixProducts(const std::string& path) {
147
+ const std::string profile = readKinetixProductsProfile(path);
148
+ std::scoped_lock lock(kInstalledProfileMutex);
149
+ activateInstalledProfile(profile);
150
+ }
151
+
152
+ I mul(I a, I b) { return (a * b) / kScale; }
153
+ I divFixed(I a, I b) { return b == 0 ? 0 : (a * kScale) / b; }
154
+ I clamp(I value, I low, I high) { return value < low ? low : value > high ? high : value; }
155
+ I absFixed(I value) { return value < 0 ? -value : value; }
156
+ I signFixed(I value) { return value > 0 ? 1 : value < 0 ? -1 : 0; }
157
+ I lerp(I a, I b, I t) { return a + mul(b - a, t); }
158
+
159
+ I sinQuarter(I x) {
160
+ const I u = divFixed(x, kScale / 4);
161
+ const I u2 = mul(u, u);
162
+ const I u3 = mul(u2, u);
163
+ const I u5 = mul(u3, u2);
164
+ return mul(1571, u) + mul(-646, u3) + mul(80, u5);
165
+ }
166
+
167
+ I fsin(I turns) {
168
+ const I t = ((turns % kScale) + kScale) % kScale;
169
+ if (t < kScale / 4) return clamp(sinQuarter(t), -kScale, kScale);
170
+ if (t < kScale / 2) return clamp(sinQuarter(kScale / 2 - t), -kScale, kScale);
171
+ if (t < 3 * kScale / 4) return clamp(-sinQuarter(t - kScale / 2), -kScale, kScale);
172
+ return clamp(-sinQuarter(kScale - t), -kScale, kScale);
173
+ }
174
+
175
+ I fcos(I turns) { return fsin(turns + kScale / 4); }
176
+ I orientTurns(I orientation) { return std::array<I, 4>{0, kScale / 4, kScale / 2, 3 * kScale / 4}[orientation]; }
177
+
178
+ struct Pose { I x, z, yaw; };
179
+ struct BodyFixed {
180
+ std::string id;
181
+ bool dynamic;
182
+ I x, y, z, vx, vy, vz;
183
+ bool sphere;
184
+ I radius, halfX, halfY, halfZ;
185
+ int layer;
186
+ unsigned mask;
187
+ };
188
+
189
+ bool contains(const std::vector<I>& values, I target) {
190
+ return std::find(std::begin(values), std::end(values), target) != std::end(values);
191
+ }
192
+
193
+ std::vector<BodyFixed> cookTrackWalls() {
194
+ const I cellHalf = kCellRaw / 2;
195
+ const I halfThick = mul(kWallHalfThick, kGridScale);
196
+ const I halfHeight = mul(kWallHalfHeightRaw, kGridScale);
197
+ const I halfLength = mul(cellHalf, kGridScale);
198
+ const I arcSpan = -(kScale / 4);
199
+ const I outerRadius = 2 * cellHalf - kWallHalfThick;
200
+ const I outerHalfLength = mul((mul(outerRadius, kHalfPiFixed) / (kOuterArcSegments * 2)), kGridScale);
201
+ const I innerRadius = kWallHalfThick;
202
+ const I innerHalfLength = mul((mul(innerRadius, kHalfPiFixed) / (kInnerArcSegments * 2)), kGridScale);
203
+ std::vector<BodyFixed> bodies;
204
+ int nextId = 0;
205
+ auto box = [&](I x, I z, I hx, I hz) {
206
+ bodies.push_back({std::string(kWallBodyPrefix) + std::to_string(nextId++), false,
207
+ x, kWallY, z, 0, 0, 0, false, 0, hx, halfHeight, hz,
208
+ static_cast<int>(kColliderLayer), static_cast<unsigned>(kColliderMask)});
209
+ };
210
+ auto arc = [&](I centerX, I centerZ, I start, I radius, I segments, I segmentHalfLength) {
211
+ for (I index = 0; index < segments; index += 1) {
212
+ const I fraction = ((2 * index + 1) * kScale) / (2 * segments);
213
+ const I mid = start + mul(fraction, arcSpan);
214
+ box(centerX + mul(mul(radius, fcos(mid)), kGridScale),
215
+ centerZ + mul(mul(radius, fsin(mid)), kGridScale), halfThick, segmentHalfLength);
216
+ }
217
+ };
218
+ for (const Cell& cell : kCells) {
219
+ if (contains(kPassThroughTypeIds, cell.typeId)) continue;
220
+ const I cx = mul((2 * cell.gridX + 1) * cellHalf, kGridScale);
221
+ const I cz = mul((2 * cell.gridZ + 1) * cellHalf, kGridScale);
222
+ const I turns = orientTurns(cell.orientation);
223
+ const I cosine = fcos(turns);
224
+ const I sine = fsin(turns);
225
+ if (contains(kCornerTypeIds, cell.typeId)) {
226
+ const I centerX = cx + mul(mul(-cellHalf, cosine) + mul(cellHalf, sine), kGridScale);
227
+ const I centerZ = cz + mul(mul(cellHalf, sine) + mul(cellHalf, cosine), kGridScale);
228
+ arc(centerX, centerZ, -turns, outerRadius, kOuterArcSegments, outerHalfLength);
229
+ arc(centerX, centerZ, -turns, innerRadius, kInnerArcSegments, innerHalfLength);
230
+ } else {
231
+ for (I side : {-1, 1}) {
232
+ const I lateral = side * kWallLateral;
233
+ box(cx + mul(mul(lateral, cosine), kGridScale),
234
+ cz + mul(mul(-lateral, sine), kGridScale), halfThick, halfLength);
235
+ }
236
+ }
237
+ }
238
+ return bodies;
239
+ }
240
+
241
+ std::vector<BodyFixed> cookArenaWalls() {
242
+ const I half = kArenaHalfUnits * kScale;
243
+ const I span = half + kArenaWallThick;
244
+ auto wall = [&](int id, I x, I z, I hx, I hz) {
245
+ return BodyFixed{std::string(kArenaBodyIds[id]), false, x, kArenaWallY, z, 0, 0, 0,
246
+ false, 0, hx, kArenaWallHalfHeight, hz, static_cast<int>(kColliderLayer), static_cast<unsigned>(kColliderMask)};
247
+ };
248
+ return {
249
+ wall(0, kArenaCenterX, kArenaCenterZ - half, span, kArenaWallThick),
250
+ wall(1, kArenaCenterX, kArenaCenterZ + half, span, kArenaWallThick),
251
+ wall(2, kArenaCenterX - half, kArenaCenterZ, kArenaWallThick, span),
252
+ wall(3, kArenaCenterX + half, kArenaCenterZ, kArenaWallThick, span),
253
+ };
254
+ }
255
+
256
+ const Cell* finishCell() {
257
+ for (const Cell& cell : kCells) if (cell.typeId == kFinishTypeId) return &cell;
258
+ return nullptr;
259
+ }
260
+
261
+ std::array<Pose, kMaxSlots> gridSpawns() {
262
+ const Cell* finish = finishCell();
263
+ if (!finish) std::abort();
264
+ const I half = kCellRaw / 2;
265
+ const I baseX = mul((2 * finish->gridX + 1) * half, kGridScale);
266
+ const I baseZ = mul((2 * finish->gridZ + 1) * half, kGridScale);
267
+ const I turns = orientTurns(finish->orientation);
268
+ const I cosine = fcos(turns);
269
+ const I sine = fsin(turns);
270
+ std::array<Pose, kMaxSlots> poses{};
271
+ for (I slot = 0; slot < kSlotCount; slot += 1) {
272
+ const I lateral = (slot % 2 == 0 ? -1 : 1) * kGridSpawnLateral;
273
+ const I back = -(slot / 2) * kGridSpawnRowBack;
274
+ poses[slot] = {baseX + mul(lateral, cosine) - mul(back, sine),
275
+ baseZ + mul(lateral, sine) + mul(back, cosine), turns};
276
+ }
277
+ return poses;
278
+ }
279
+
280
+ std::array<Pose, kMaxSlots> scatterSpawns() {
281
+ std::array<Pose, kMaxSlots> poses{};
282
+ for (I slot = 0; slot < kSlotCount; slot += 1) {
283
+ const I columnNumerator = 2 * (slot % kScatterCols) - (kScatterCols - 1);
284
+ poses[slot] = {kArenaCenterX + columnNumerator * kScatterSpacing / 2,
285
+ kArenaCenterZ + (slot / kScatterCols - 1) * kScatterSpacing, 0};
286
+ }
287
+ return poses;
288
+ }
289
+
290
+ struct WaypointRing { std::vector<I> x, z; };
291
+ I upperHalf(I x, I z) { return z < 0 || (z == 0 && x < 0) ? 1 : 0; }
292
+
293
+ WaypointRing buildWaypointRing() {
294
+ struct Point { I x, z; };
295
+ std::vector<Point> points;
296
+ I sumX = 0;
297
+ I sumZ = 0;
298
+ for (const Cell& cell : kCells) {
299
+ const I x = mul((2 * cell.gridX + 1) * (kCellRaw / 2), kGridScale);
300
+ const I z = mul((2 * cell.gridZ + 1) * (kCellRaw / 2), kGridScale);
301
+ points.push_back({x, z});
302
+ sumX += x;
303
+ sumZ += z;
304
+ }
305
+ const I centerX = sumX / static_cast<I>(points.size());
306
+ const I centerZ = sumZ / static_cast<I>(points.size());
307
+ for (Point& point : points) { point.x -= centerX; point.z -= centerZ; }
308
+ std::stable_sort(points.begin(), points.end(), [](const Point& a, const Point& b) {
309
+ const I halfA = upperHalf(a.x, a.z);
310
+ const I halfB = upperHalf(b.x, b.z);
311
+ if (halfA != halfB) return halfA < halfB;
312
+ const I cross = a.x * b.z - a.z * b.x;
313
+ if (cross != 0) return cross > 0;
314
+ return a.x * a.x + a.z * a.z < b.x * b.x + b.z * b.z;
315
+ });
316
+ WaypointRing ring;
317
+ for (const Point& point : points) { ring.x.push_back(point.x + centerX); ring.z.push_back(point.z + centerZ); }
318
+ return ring;
319
+ }
320
+
321
+ I approximateMagnitude(I x, I z) {
322
+ const I ax = absFixed(x);
323
+ const I az = absFixed(z);
324
+ const I high = std::max(ax, az);
325
+ const I low = std::min(ax, az);
326
+ return high + low / 2;
327
+ }
328
+
329
+ struct Drive { I steer, throttle; };
330
+ Drive waypointDrive(const WaypointRing& ring, I x, I z, I yaw) {
331
+ std::size_t nearest = 0;
332
+ I best = std::numeric_limits<I>::max();
333
+ for (std::size_t index = 0; index < ring.x.size(); index += 1) {
334
+ const I dx = ring.x[index] - x;
335
+ const I dz = ring.z[index] - z;
336
+ const I distance = dx * dx + dz * dz;
337
+ if (distance < best) { best = distance; nearest = index; }
338
+ }
339
+ const std::size_t target = (nearest + static_cast<std::size_t>(kWaypointLookahead)) % ring.x.size();
340
+ const I dx = ring.x[target] - x;
341
+ const I dz = ring.z[target] - z;
342
+ const I forwardX = fsin(yaw);
343
+ const I forwardZ = fcos(yaw);
344
+ const I magnitude = approximateMagnitude(dx, dz);
345
+ if (magnitude == 0) return {0, kScale};
346
+ const I lateral = divFixed(mul(forwardZ, dx) - mul(forwardX, dz), magnitude);
347
+ const I forward = divFixed(mul(forwardX, dx) + mul(forwardZ, dz), magnitude);
348
+ return {clamp(mul(kWaypointSteerGain, lateral), -kScale, kScale), forward > 0 ? kScale : kWaypointSharpTurnThrottle};
349
+ }
350
+
351
+ struct LapInfo {
352
+ bool enabled = false;
353
+ I centerX = 0, centerZ = 0, forwardX = 0, forwardZ = 0, rightX = 0, rightZ = 0;
354
+ std::map<std::pair<I, I>, int> bits;
355
+ I mask = 0;
356
+ };
357
+
358
+ LapInfo buildLapInfo() {
359
+ const Cell* finish = finishCell();
360
+ LapInfo info;
361
+ if (!finish) return info;
362
+ info.enabled = true;
363
+ info.centerX = mul((2 * finish->gridX + 1) * (kCellRaw / 2), kGridScale);
364
+ info.centerZ = mul((2 * finish->gridZ + 1) * (kCellRaw / 2), kGridScale);
365
+ const I turns = orientTurns(finish->orientation);
366
+ info.forwardX = fsin(turns);
367
+ info.forwardZ = fcos(turns);
368
+ info.rightX = info.forwardZ;
369
+ info.rightZ = -info.forwardX;
370
+ std::vector<Cell> required;
371
+ for (const Cell& cell : kCells) if (cell.typeId != kFinishTypeId) required.push_back(cell);
372
+ std::sort(required.begin(), required.end(), [](const Cell& a, const Cell& b) {
373
+ return a.gridX != b.gridX ? a.gridX < b.gridX : a.gridZ < b.gridZ;
374
+ });
375
+ int bit = 0;
376
+ for (const Cell& cell : required) {
377
+ if (bit >= kMaxRequiredBits) break;
378
+ info.bits[{cell.gridX, cell.gridZ}] = bit++;
379
+ }
380
+ info.mask = bit == 0 ? 0 : bit >= 31 ? 0x7fffffff : (I{1} << bit) - 1;
381
+ return info;
382
+ }
383
+
384
+ struct State {
385
+ I frame = 0;
386
+ I phase = 0;
387
+ I phaseStart = 0;
388
+ I lobbyDeadline = 0;
389
+ std::array<I, kMaxSlots> x{}, z{}, vx{}, vz{}, speed{}, yaw{}, lap{}, visited{}, lapProjection{}, finishFrame{};
390
+ std::array<bool, kMaxSlots> drifting{}, isBot{};
391
+ };
392
+
393
+ struct SlotInput { bool present = false; I steer = 0; I throttle = 0; bool handbrake = false; };
394
+ using TickInputs = std::array<SlotInput, kMaxSlots>;
395
+
396
+ struct Runtime {
397
+ State state;
398
+ std::vector<BodyFixed> trackWalls = cookTrackWalls();
399
+ std::vector<BodyFixed> arenaWalls = cookArenaWalls();
400
+ WaypointRing ring = buildWaypointRing();
401
+ LapInfo lapInfo = buildLapInfo();
402
+ };
403
+
404
+ Runtime createRuntime(I initialPhase = kInitialPhase, I humanCount = kHumanCount) {
405
+ Runtime runtime;
406
+ runtime.state.phase = initialPhase;
407
+ runtime.state.lobbyDeadline = initialPhase == kLobby ? kLobbyInitialFrames : 0;
408
+ const auto poses = initialPhase == kLobby ? scatterSpawns() : gridSpawns();
409
+ for (I slot = 0; slot < kSlotCount; slot += 1) {
410
+ runtime.state.x[slot] = poses[slot].x;
411
+ runtime.state.z[slot] = poses[slot].z;
412
+ runtime.state.yaw[slot] = poses[slot].yaw;
413
+ runtime.state.finishFrame[slot] = -1;
414
+ runtime.state.isBot[slot] = slot >= humanCount;
415
+ }
416
+ return runtime;
417
+ }
418
+
419
+ void freeze(State& state, std::size_t slot) {
420
+ state.speed[slot] = 0;
421
+ state.vx[slot] = 0;
422
+ state.vz[slot] = 0;
423
+ state.drifting[slot] = false;
424
+ }
425
+
426
+ void stepVehicle(State& state, std::size_t slot, I steerInput, I throttleInput) {
427
+ const I steer = clamp(steerInput, -kScale, kScale);
428
+ const I throttle = clamp(throttleInput, -kScale, kScale);
429
+ const I target = throttle >= 0 ? mul(throttle, kMaxSpeed) : mul(throttle, kMaxReverse);
430
+ const bool braking = throttle < 0 && state.speed[slot] > 0;
431
+ I speed = lerp(state.speed[slot], target, braking ? kBrakeRate : kAccelRate);
432
+ if (absFixed(speed) < kStopDeadband) speed = 0;
433
+ const I grip = clamp(divFixed(absFixed(speed), kMaxSpeed), kGripMin, kScale);
434
+ const I direction = speed == 0 ? 0 : signFixed(speed);
435
+ const I yawDelta = mul(mul(steer, kTurnPerTick), grip) * direction;
436
+ I yaw = (state.yaw[slot] + yawDelta) % kScale;
437
+ if (yaw < 0) yaw += kScale;
438
+ const I desiredVX = mul(fsin(yaw), speed);
439
+ const I desiredVZ = mul(fcos(yaw), speed);
440
+ const I traction = absFixed(speed) > kSlipSpeed ? kTractionFast : kTractionSlow;
441
+ state.yaw[slot] = yaw;
442
+ state.speed[slot] = speed;
443
+ state.drifting[slot] = absFixed(speed) > kSlipSpeed && absFixed(steer) > kDriftSteer;
444
+ state.vx[slot] = lerp(state.vx[slot], desiredVX, traction);
445
+ state.vz[slot] = lerp(state.vz[slot], desiredVZ, traction);
446
+ }
447
+
448
+ PhysBody physicsBody(const BodyFixed& body) {
449
+ return {body.id, body.dynamic,
450
+ static_cast<double>(body.x) / kScale, static_cast<double>(body.y) / kScale, static_cast<double>(body.z) / kScale,
451
+ static_cast<double>(body.vx) / kScale, static_cast<double>(body.vy) / kScale, static_cast<double>(body.vz) / kScale,
452
+ body.sphere, static_cast<double>(body.radius) / kScale,
453
+ static_cast<double>(body.halfX) / kScale, static_cast<double>(body.halfY) / kScale, static_cast<double>(body.halfZ) / kScale,
454
+ body.layer, body.mask};
455
+ }
456
+
457
+ I jsRound(double value) { return static_cast<I>(std::floor(value + 0.5)); }
458
+
459
+ void stepLap(State& state, const LapInfo& info, std::size_t slot, I frame) {
460
+ if (!info.enabled || state.finishFrame[slot] >= 0) return;
461
+ const I cellWorld = mul(kCellRaw, kGridScale);
462
+ const I gridX = static_cast<I>(std::floor(static_cast<double>(state.x[slot]) / cellWorld));
463
+ const I gridZ = static_cast<I>(std::floor(static_cast<double>(state.z[slot]) / cellWorld));
464
+ const auto bit = info.bits.find({gridX, gridZ});
465
+ if (bit != info.bits.end()) state.visited[slot] |= I{1} << bit->second;
466
+ const I relativeX = state.x[slot] - info.centerX;
467
+ const I relativeZ = state.z[slot] - info.centerZ;
468
+ const I forward = mul(relativeX, info.forwardX) + mul(relativeZ, info.forwardZ);
469
+ const I lateral = absFixed(mul(relativeX, info.rightX) + mul(relativeZ, info.rightZ));
470
+ const bool onLine = lateral <= cellWorld / 2;
471
+ const bool noTeleport = absFixed(forward - state.lapProjection[slot]) < kNoTeleport;
472
+ const bool crossed = state.lapProjection[slot] < 0 && forward >= 0;
473
+ if (onLine && noTeleport && crossed) {
474
+ if (state.visited[slot] == info.mask && info.mask > 0) {
475
+ state.lap[slot] += 1;
476
+ state.visited[slot] = 0;
477
+ if (state.lap[slot] >= kTotalLaps) state.finishFrame[slot] = frame;
478
+ } else {
479
+ state.visited[slot] = 0;
480
+ }
481
+ }
482
+ state.lapProjection[slot] = forward;
483
+ }
484
+
485
+ void tick(Runtime& runtime, const TickInputs& inputs = {}) {
486
+ State next = runtime.state;
487
+ const I frame = runtime.state.frame;
488
+ bool joined = false;
489
+ I humans = 0;
490
+ for (std::size_t slot = 0; slot < kSlotCount; slot += 1) {
491
+ const bool bot = !inputs[slot].present;
492
+ if (runtime.state.isBot[slot] && !bot) joined = true;
493
+ next.isBot[slot] = bot;
494
+ if (!bot) humans += 1;
495
+ }
496
+ const bool activeAtStart = next.phase == kActive;
497
+ const bool inArena = next.phase == kLobby;
498
+ std::vector<BodyFixed> dynamicBodies;
499
+ for (std::size_t slot = 0; slot < kSlotCount; slot += 1) {
500
+ const bool finished = next.finishFrame[slot] >= 0;
501
+ bool drive = (next.phase == kLobby && !next.isBot[slot]) || (next.phase == kActive && !finished);
502
+ I steer = 0;
503
+ I throttle = 0;
504
+ if (drive) {
505
+ if (next.isBot[slot]) {
506
+ const Drive command = waypointDrive(runtime.ring, next.x[slot], next.z[slot], next.yaw[slot]);
507
+ steer = command.steer;
508
+ throttle = command.throttle;
509
+ } else {
510
+ steer = inputs[slot].steer;
511
+ throttle = inputs[slot].throttle;
512
+ }
513
+ stepVehicle(next, slot, steer, throttle);
514
+ } else {
515
+ freeze(next, slot);
516
+ }
517
+ dynamicBodies.push_back({std::string(kCarBodyPrefix) + std::to_string(slot), true,
518
+ next.x[slot], kBodyY, next.z[slot], next.vx[slot], 0, next.vz[slot], true,
519
+ kBodyRadius, 0, 0, 0, static_cast<int>(kBodyLayer), static_cast<unsigned>(kBodyMask)});
520
+ }
521
+ std::vector<PhysBody> bodies;
522
+ for (const BodyFixed& body : inArena ? runtime.arenaWalls : runtime.trackWalls) bodies.push_back(physicsBody(body));
523
+ for (const BodyFixed& body : dynamicBodies) bodies.push_back(physicsBody(body));
524
+ stepPhysicsSlice(bodies);
525
+ for (std::size_t slot = 0; slot < kSlotCount; slot += 1) {
526
+ const std::string id = std::string(kCarBodyPrefix) + std::to_string(slot);
527
+ const auto body = std::find_if(bodies.begin(), bodies.end(), [&](const PhysBody& candidate) { return candidate.id == id; });
528
+ if (body != bodies.end()) {
529
+ next.x[slot] = jsRound(body->x * kScale);
530
+ next.z[slot] = jsRound(body->z * kScale);
531
+ next.vx[slot] = jsRound(body->vx * kScale);
532
+ next.vz[slot] = jsRound(body->vz * kScale);
533
+ }
534
+ }
535
+ if (activeAtStart) for (std::size_t slot = 0; slot < kSlotCount; slot += 1) stepLap(next, runtime.lapInfo, slot, frame);
536
+ if (next.phase == kLobby) {
537
+ if (joined) next.lobbyDeadline = std::min(next.lobbyDeadline + kLobbyExtendFrames, next.phaseStart + kLobbyMaxFrames);
538
+ if (humans >= kMaxSlotsAutoStart || frame >= next.lobbyDeadline) {
539
+ const auto poses = gridSpawns();
540
+ for (std::size_t slot = 0; slot < kSlotCount; slot += 1) {
541
+ next.x[slot] = poses[slot].x; next.z[slot] = poses[slot].z; next.yaw[slot] = poses[slot].yaw;
542
+ freeze(next, slot); next.lap[slot] = 0; next.visited[slot] = 0; next.lapProjection[slot] = 0; next.finishFrame[slot] = -1;
543
+ }
544
+ next.phase = kCountdown; next.phaseStart = frame;
545
+ }
546
+ } else if (next.phase == kCountdown) {
547
+ if (frame - next.phaseStart >= kCountdownFrames) { next.phase = kActive; next.phaseStart = frame; }
548
+ } else if (next.phase == kActive) {
549
+ const bool everyoneDone = std::all_of(
550
+ next.finishFrame.begin(), next.finishFrame.begin() + kSlotCount,
551
+ [](I value) { return value >= 0; });
552
+ if (everyoneDone || frame - next.phaseStart >= kMaxRaceFrames) { next.phase = kFinished; next.phaseStart = frame; }
553
+ } else if (next.phase == kFinished && frame - next.phaseStart >= kResultsHoldFrames) {
554
+ const auto poses = scatterSpawns();
555
+ for (std::size_t slot = 0; slot < kSlotCount; slot += 1) {
556
+ next.x[slot] = poses[slot].x; next.z[slot] = poses[slot].z; next.yaw[slot] = poses[slot].yaw;
557
+ freeze(next, slot); next.lap[slot] = 0; next.visited[slot] = 0; next.lapProjection[slot] = 0; next.finishFrame[slot] = -1;
558
+ }
559
+ next.phase = kLobby; next.phaseStart = frame; next.lobbyDeadline = frame + kLobbyInitialFrames;
560
+ }
561
+ next.frame = frame + 1;
562
+ runtime.state = next;
563
+ }
564
+
565
+ std::vector<TickInputs> readInputs(const std::string& path, int ticks) {
566
+ std::vector<TickInputs> frames(static_cast<std::size_t>(ticks));
567
+ if (path.empty()) return frames;
568
+ std::ifstream input(path, std::ios::binary);
569
+ if (!input) std::abort();
570
+ for (TickInputs& frame : frames) {
571
+ for (I index = 0; index < kSlotCount; index += 1) {
572
+ SlotInput& slot = frame[static_cast<std::size_t>(index)];
573
+ slot.present = input.get() == 1;
574
+ slot.steer = readI64(input);
575
+ slot.throttle = readI64(input);
576
+ slot.handbrake = input.get() == 1;
577
+ }
578
+ }
579
+ if (input.get() != EOF) std::abort();
580
+ return frames;
581
+ }
582
+
583
+ void appendU64(std::vector<std::uint8_t>& bytes, std::uint64_t value) {
584
+ for (int shift = 0; shift < 64; shift += 8) bytes.push_back(static_cast<std::uint8_t>(value >> shift));
585
+ }
586
+ void appendI64(std::vector<std::uint8_t>& bytes, I value) { appendU64(bytes, static_cast<std::uint64_t>(value)); }
587
+
588
+ std::vector<std::uint8_t> canonical(const State& state) {
589
+ std::vector<std::uint8_t> bytes;
590
+ appendU64(bytes, static_cast<std::uint64_t>(state.frame));
591
+ const std::string& track = kTrack;
592
+ const std::uint32_t length = static_cast<std::uint32_t>(track.size());
593
+ for (int shift = 0; shift < 32; shift += 8) bytes.push_back(static_cast<std::uint8_t>(length >> shift));
594
+ bytes.insert(bytes.end(), track.begin(), track.end());
595
+ appendI64(bytes, state.phase); appendI64(bytes, state.phaseStart); appendI64(bytes, state.lobbyDeadline);
596
+ for (const auto* field : {&state.x, &state.z, &state.vx, &state.vz, &state.speed, &state.yaw})
597
+ for (I slot = 0; slot < kSlotCount; slot += 1) appendI64(bytes, (*field)[static_cast<std::size_t>(slot)]);
598
+ for (I slot = 0; slot < kSlotCount; slot += 1) bytes.push_back(state.drifting[static_cast<std::size_t>(slot)] ? 1 : 0);
599
+ for (const auto* field : {&state.lap, &state.visited, &state.lapProjection, &state.finishFrame})
600
+ for (I slot = 0; slot < kSlotCount; slot += 1) appendI64(bytes, (*field)[static_cast<std::size_t>(slot)]);
601
+ for (I slot = 0; slot < kSlotCount; slot += 1) bytes.push_back(state.isBot[static_cast<std::size_t>(slot)] ? 1 : 0);
602
+ return bytes;
603
+ }
604
+ }
605
+
606
+ struct rundot::kinetix::Fixed1000Session::Impl {
607
+ std::string profile;
608
+ Runtime runtime;
609
+ Fixed1000Command currentCommand{0, 0, false};
610
+ Fixed1000Command commandForNextTick{0, 0, false};
611
+ bool hasCommandForNextTick = false;
612
+ };
613
+
614
+ rundot::kinetix::Fixed1000Session::Fixed1000Session(std::unique_ptr<Impl> impl)
615
+ : impl_(std::move(impl)) {}
616
+
617
+ rundot::kinetix::Fixed1000Session::~Fixed1000Session() = default;
618
+
619
+ std::unique_ptr<rundot::kinetix::Fixed1000Session>
620
+ rundot::kinetix::Fixed1000Session::Load(
621
+ const std::string& productPath,
622
+ std::int64_t initialPhase,
623
+ std::int64_t humanCount
624
+ ) {
625
+ std::string profile = readKinetixProductsProfile(productPath);
626
+ std::scoped_lock lock(kInstalledProfileMutex);
627
+ activateInstalledProfile(profile);
628
+ auto impl = std::make_unique<Impl>();
629
+ impl->profile = std::move(profile);
630
+ impl->runtime = createRuntime(initialPhase, humanCount);
631
+ return std::unique_ptr<Fixed1000Session>(new Fixed1000Session(std::move(impl)));
632
+ }
633
+
634
+ bool rundot::kinetix::Fixed1000Session::SetCommandForNextTick(
635
+ const Fixed1000Command& command,
636
+ std::string& error
637
+ ) {
638
+ if (command.steerX < -1000 || command.steerX > 1000
639
+ || command.throttle < -1000 || command.throttle > 1000) {
640
+ error = "KINETIX_COMMAND_AXIS_OUT_OF_RANGE";
641
+ return false;
642
+ }
643
+ impl_->commandForNextTick = command;
644
+ impl_->hasCommandForNextTick = true;
645
+ error.clear();
646
+ return true;
647
+ }
648
+
649
+ void rundot::kinetix::Fixed1000Session::Tick() {
650
+ std::scoped_lock lock(kInstalledProfileMutex);
651
+ activateInstalledProfile(impl_->profile);
652
+ if (impl_->hasCommandForNextTick) {
653
+ impl_->currentCommand = impl_->commandForNextTick;
654
+ impl_->hasCommandForNextTick = false;
655
+ }
656
+ TickInputs inputs{};
657
+ inputs[0] = {
658
+ true,
659
+ impl_->currentCommand.steerX,
660
+ impl_->currentCommand.throttle,
661
+ impl_->currentCommand.handbrake,
662
+ };
663
+ tick(impl_->runtime, inputs);
664
+ }
665
+
666
+ rundot::kinetix::Fixed1000Presentation
667
+ rundot::kinetix::Fixed1000Session::Presentation() const {
668
+ std::scoped_lock lock(kInstalledProfileMutex);
669
+ activateInstalledProfile(impl_->profile);
670
+ const State& state = impl_->runtime.state;
671
+ const std::array<std::string, 4> phases = {"lobby", "countdown", "active", "finished"};
672
+ Fixed1000Presentation presentation{
673
+ state.frame,
674
+ phases.at(static_cast<std::size_t>(state.phase)),
675
+ state.frame * 1000 / 30,
676
+ std::max<I>(1, state.lap[0] + 1),
677
+ kTotalLaps,
678
+ 1,
679
+ {},
680
+ };
681
+ for (I slot = 1; slot < kSlotCount; slot += 1) {
682
+ if (state.finishFrame[slot] >= 0 && state.finishFrame[0] < 0) {
683
+ presentation.position += 1;
684
+ } else if (state.lap[slot] > state.lap[0]) {
685
+ presentation.position += 1;
686
+ } else if (state.lap[slot] == state.lap[0] && state.visited[slot] > state.visited[0]) {
687
+ presentation.position += 1;
688
+ }
689
+ }
690
+ for (I slot = 0; slot < kSlotCount; slot += 1) {
691
+ presentation.cars.push_back({
692
+ slot,
693
+ static_cast<double>(state.x[slot]) / kScale,
694
+ static_cast<double>(state.z[slot]) / kScale,
695
+ static_cast<double>(state.yaw[slot]) / kScale,
696
+ });
697
+ }
698
+ return presentation;
699
+ }
700
+
701
+ std::vector<std::uint8_t> rundot::kinetix::Fixed1000Session::CanonicalState() const {
702
+ std::scoped_lock lock(kInstalledProfileMutex);
703
+ activateInstalledProfile(impl_->profile);
704
+ return canonical(impl_->runtime.state);
705
+ }
706
+
707
+ std::string rundot::kinetix::Fixed1000Session::CanonicalChecksum() const {
708
+ const auto bytes = CanonicalState();
709
+ return rundot::sha256Hex(bytes.data(), bytes.size());
710
+ }
711
+
712
+ int rundot::kinetix::RunFixed1000Profile(int argc, char** argv) {
713
+ #ifndef KINETIX_FIXED1000_SIM_ONLY
714
+ if (argc == 7 && std::string(argv[1]) == "--records") {
715
+ const int warmup = std::atoi(argv[2]);
716
+ const int capture = std::atoi(argv[3]);
717
+ const std::string packDir = argv[4];
718
+ const std::string output = argv[5];
719
+ const int expectedTicks = std::atoi(argv[6]);
720
+ Runtime runtime = createRuntime();
721
+ rundot::actorfollow::ActorFollowCore core(rundot::actorfollow::makeActorFollowData());
722
+ rundot::renderrec::StreamRec stream;
723
+ const auto renderData = rundot::actorfollow::makeActorFollowData();
724
+ stream.viewportWidth = renderData.viewportWidth;
725
+ stream.viewportHeight = renderData.viewportHeight;
726
+ double accumulatedMs = 0;
727
+ int ticks = 0;
728
+ for (int renderFrame = 0; renderFrame < warmup + capture; renderFrame += 1) {
729
+ accumulatedMs += 1000.0 / 60.0;
730
+ const int framesToStep = std::max(0, static_cast<int>(std::floor((accumulatedMs * 30.0 + 1e-9) / 1000.0)));
731
+ for (int stepIndex = 0; stepIndex < framesToStep; stepIndex += 1) { tick(runtime); ticks += 1; }
732
+ accumulatedMs = std::max(0.0, accumulatedMs - framesToStep * (1000.0 / 30.0));
733
+ std::vector<rundot::actorfollow::CarView> cars;
734
+ for (int slot = 0; slot < kSlotCount; slot += 1) cars.push_back({slot,
735
+ static_cast<double>(runtime.state.x[slot]) / kScale,
736
+ static_cast<double>(runtime.state.z[slot]) / kScale,
737
+ static_cast<double>(runtime.state.yaw[slot]) / kScale});
738
+ auto frame = core.frame(cars);
739
+ if (renderFrame >= warmup) {
740
+ frame.frameIndex = static_cast<std::uint32_t>(renderFrame - warmup);
741
+ if (frame.frameIndex == 0) {
742
+ auto uploads = core.textureUploads(packDir);
743
+ frame.commands.insert(frame.commands.begin(), uploads.begin(), uploads.end());
744
+ }
745
+ stream.frames.push_back(std::move(frame));
746
+ }
747
+ }
748
+ if (ticks != expectedTicks) { std::fprintf(stderr, "unexpected tick count %d != %d\n", ticks, expectedTicks); return 2; }
749
+ const auto encoded = rundot::renderrec::encodeRenderRecordStream(stream);
750
+ std::ofstream out(output, std::ios::binary);
751
+ out.write(reinterpret_cast<const char*>(encoded.data()), static_cast<std::streamsize>(encoded.size()));
752
+ std::printf("KINETIX_FIXED1000_RECORDS_OK ticks=%d frames=%zu bytes=%zu\n", ticks, stream.frames.size(), encoded.size());
753
+ return 0;
754
+ }
755
+ #endif
756
+ if (argc < 4 || argc > 7) {
757
+ std::fprintf(stderr, "usage: kinetix-fixed1000 <profile.bin> <ticks> <out.bin> [inputs.bin] [initialPhase] [humanCount]\n");
758
+ return 2;
759
+ }
760
+ loadKinetixProducts(argv[1]);
761
+ const int ticks = std::atoi(argv[2]);
762
+ const auto inputs = readInputs(argc >= 5 ? argv[4] : "", ticks);
763
+ std::ofstream out(argv[3], std::ios::binary);
764
+ if (!out) return 2;
765
+ const I initialPhase = argc >= 6 ? std::atoll(argv[5]) : kInitialPhase;
766
+ const I humanCount = argc >= 7 ? std::atoll(argv[6]) : kHumanCount;
767
+ Runtime runtime = createRuntime(initialPhase, humanCount);
768
+ for (int index = -1; index < ticks; index += 1) {
769
+ const std::vector<std::uint8_t> bytes = canonical(runtime.state);
770
+ const std::uint32_t length = static_cast<std::uint32_t>(bytes.size());
771
+ out.write(reinterpret_cast<const char*>(&length), sizeof(length));
772
+ out.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
773
+ if (index + 1 < ticks) tick(runtime, inputs[static_cast<std::size_t>(index + 1)]);
774
+ }
775
+ std::printf("KINETIX_FIXED1000_SIM_OK ticks=%d frames=%d\n", ticks, ticks + 1);
776
+ return 0;
777
+ }