@its-not-rocket-science/ananke 0.1.57 → 0.1.59
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.
- package/CHANGELOG.md +42 -0
- package/README.md +4 -0
- package/dist/src/netcode.d.ts +50 -0
- package/dist/src/netcode.js +115 -0
- package/dist/tools/pack-cli.js +87 -18
- package/package.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,48 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.59] — 2026-03-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **PA-10 — Deterministic Networking Kit (complete):**
|
|
14
|
+
- `src/netcode.ts` (new): determinism utilities for authoritative lockstep and desync diagnosis.
|
|
15
|
+
- **`hashWorldState(world): bigint`**: FNV-64 hash over `tick`, `seed`, and all entity state sorted by `id` (Map fields serialised as sorted entry arrays for canonical form). Use as a per-tick desync checksum in multiplayer loops.
|
|
16
|
+
- **`diffReplays(replayA, replayB, ctx): ReplayDiff`**: steps two replays in lock-step and returns the first tick where their hashes diverge. O(N) in replay length.
|
|
17
|
+
- **`diffReplayJson(jsonA, jsonB, ctx): ReplayDiff`**: convenience wrapper for CLI use.
|
|
18
|
+
- `ReplayDiff` interface: `{ divergeAtTick, hashA, hashB, ticksCompared }`.
|
|
19
|
+
- `"./netcode"` subpath export added to `package.json`.
|
|
20
|
+
- **`ananke replay diff` CLI subcommand**: extends the `npx ananke` CLI — reads two replay JSON files and prints the first divergence tick and hex hashes, or confirms they are identical. Exit code 0 = identical; exit code 1 = divergence.
|
|
21
|
+
- **`docs/netcode-host-checklist.md`** (new): 8-section guide covering fixed tick rate, no wall-clock reads in simulation path, input serialisation format, desync detection, state resync (full snapshot), replay recording and diff, rollback implementation outline, and KernelContext consistency requirements.
|
|
22
|
+
- **`examples/lockstep-server.ts`** (new): self-contained authoritative lockstep demo — one server steps the world, two virtual clients verify hash checksums every tick. Demonstrates replay recording and `serializeBridgeFrame` integration.
|
|
23
|
+
- **`examples/rollback-client.ts`** (new): rollback demo — client predicts speculatively, reconciles against server hash, and re-simulates from the last confirmed snapshot when a mismatch is detected.
|
|
24
|
+
- npm scripts: `example:lockstep`, `example:rollback`.
|
|
25
|
+
- 16 new tests (189 test files, 5,569 tests total). Coverage: 97.11% stmt, 88.07% branch, 95.82% func. `netcode.ts`: 100%/100%/100%. Build: clean.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## [0.1.58] — 2026-03-30
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **PA-9 — Simulation Cookbook (complete):**
|
|
34
|
+
- `docs/cookbook.md` (new): 12 task-oriented recipes designed to take a developer from zero to running simulation in under 30 minutes.
|
|
35
|
+
- **Recipe 1 — Simulate a duel**: `mkWorld` + `stepWorld` + command loop; expected output showing injury accumulation and fight end.
|
|
36
|
+
- **Recipe 2 — Run a 500-agent battle**: entity loop with `buildAICommands`; timing guidance (≤6 ms/tick on modern hardware).
|
|
37
|
+
- **Recipe 3 — Author a new species**: custom `Archetype` → `generateIndividual`; species-specific attribute overrides.
|
|
38
|
+
- **Recipe 4 — Add a custom weapon**: `Item` definition with mass, blade length, and damage profile; `createWorld` with `customItems`.
|
|
39
|
+
- **Recipe 5 — Drive a renderer**: `serializeBridgeFrame` + WebSocket sidecar pattern; references `docs/quickstart-unity.md`, `docs/quickstart-godot.md`, `docs/quickstart-web.md`.
|
|
40
|
+
- **Recipe 6 — Create a campaign loop**: `createPolity` + `stepPolityDay`; campaign-to-tactical transition example.
|
|
41
|
+
- **Recipe 7 — Build a validation scenario**: empirical range-check pattern; tolerance bands and `±%` reporting.
|
|
42
|
+
- **Recipe 8 — Use the what-if engine**: `npm run run:what-if`; scenario customization via parameter override.
|
|
43
|
+
- **Recipe 9 — Stream events to an agent**: delta detection + `serializeBridgeFrame` push over Server-Sent Events.
|
|
44
|
+
- **Recipe 10 — Save and reload a world**: `JSON.stringify` / `JSON.parse` round-trip with tick continuity check.
|
|
45
|
+
- **Recipe 11 — Record and replay a fight**: `ReplayRecorder` + `replayTo` + `serializeReplay` / `deserializeReplay`.
|
|
46
|
+
- **Recipe 12 — Load a content pack**: `loadPack` + `validatePack` + pack JSON schema reference.
|
|
47
|
+
- `README.md`: cookbook cross-link added in intro and "Further reading" table.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
9
51
|
## [0.1.57] — 2026-03-30
|
|
10
52
|
|
|
11
53
|
### Added
|
package/README.md
CHANGED
|
@@ -96,6 +96,9 @@ for (let tick = 0; tick < 2000; tick++) {
|
|
|
96
96
|
`stepWorld` is the only function that mutates state. Everything else is pure computation.
|
|
97
97
|
Call it at 20 Hz for real-time simulation; 1 Hz or lower for campaign-scale time.
|
|
98
98
|
|
|
99
|
+
For task-oriented walkthroughs, see the **[Simulation Cookbook](docs/cookbook.md)** — 12 recipes
|
|
100
|
+
from "Simulate a duel" to "Load a content pack", each with step-by-step code and expected output.
|
|
101
|
+
|
|
99
102
|
---
|
|
100
103
|
|
|
101
104
|
## Quick start A — Melee combat
|
|
@@ -411,6 +414,7 @@ Ananke's outputs are validated against historical and experimental sources:
|
|
|
411
414
|
|
|
412
415
|
| Document | What's in it |
|
|
413
416
|
|---|---|
|
|
417
|
+
| [`docs/cookbook.md`](docs/cookbook.md) | Task-oriented recipes — duel, 500-agent battle, species, renderer, campaign, replay, and more |
|
|
414
418
|
| [`docs/module-index.md`](docs/module-index.md) | All 41 entry points — stability tier, use case, key exports, doc links |
|
|
415
419
|
| [`docs/host-contract.md`](docs/host-contract.md) | Stable integration surface — everything needed to embed Ananke without reading `src/` |
|
|
416
420
|
| [`docs/integration-primer.md`](docs/integration-primer.md) | Data-flow diagrams, type glossary, gotchas |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { WorldState } from "./sim/world.js";
|
|
2
|
+
import type { KernelContext } from "./sim/context.js";
|
|
3
|
+
import { type Replay } from "./replay.js";
|
|
4
|
+
/**
|
|
5
|
+
* Compute a deterministic 64-bit hash of the simulation's core state.
|
|
6
|
+
*
|
|
7
|
+
* Covers `tick`, `seed`, and all entity data sorted by `id`. Optional
|
|
8
|
+
* subsystem fields (`__sensoryEnv`, `__factionRegistry`, etc.) are excluded —
|
|
9
|
+
* they are host concerns and do not affect simulation determinism.
|
|
10
|
+
*
|
|
11
|
+
* Use this as a desync checksum in multiplayer loops:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const hash = hashWorldState(world);
|
|
15
|
+
* socket.emit("tick-ack", { tick: world.tick, hash: hash.toString() });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @returns An unsigned 64-bit bigint.
|
|
19
|
+
*/
|
|
20
|
+
export declare function hashWorldState(world: WorldState): bigint;
|
|
21
|
+
/** Result of comparing two replay traces. */
|
|
22
|
+
export interface ReplayDiff {
|
|
23
|
+
/** Tick at which the two replays first diverge. `-1` means the initial
|
|
24
|
+
* states differ before any step. `undefined` means the replays are
|
|
25
|
+
* identical up to the last compared tick. */
|
|
26
|
+
divergeAtTick: number | undefined;
|
|
27
|
+
/** Hash from replay A at the divergence tick (`undefined` when identical). */
|
|
28
|
+
hashA: bigint | undefined;
|
|
29
|
+
/** Hash from replay B at the divergence tick (`undefined` when identical). */
|
|
30
|
+
hashB: bigint | undefined;
|
|
31
|
+
/** Total ticks compared (including the initial-state check). */
|
|
32
|
+
ticksCompared: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Compare two replay traces tick-by-tick and find the first divergence.
|
|
36
|
+
*
|
|
37
|
+
* Steps both replays from their initial states in lock-step, computing
|
|
38
|
+
* `hashWorldState` after each tick. O(N) in replay length.
|
|
39
|
+
*
|
|
40
|
+
* @param replayA First replay (e.g. client A's recording).
|
|
41
|
+
* @param replayB Second replay (e.g. client B's recording).
|
|
42
|
+
* @param ctx KernelContext forwarded to `stepWorld`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function diffReplays(replayA: Replay, replayB: Replay, ctx: KernelContext): ReplayDiff;
|
|
45
|
+
/**
|
|
46
|
+
* Parse two replay JSON strings and diff them.
|
|
47
|
+
*
|
|
48
|
+
* Convenience wrapper over `diffReplays` for CLI use.
|
|
49
|
+
*/
|
|
50
|
+
export declare function diffReplayJson(jsonA: string, jsonB: string, ctx: KernelContext): ReplayDiff;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/netcode.ts — PA-10: Deterministic Networking Kit
|
|
2
|
+
//
|
|
3
|
+
// Utilities for authoritative lockstep and desync diagnosis.
|
|
4
|
+
//
|
|
5
|
+
// Core guarantee: two clients running identical commands from identical seeds
|
|
6
|
+
// must produce identical hashWorldState() outputs at every tick. A mismatch
|
|
7
|
+
// pinpoints the first tick where state diverged.
|
|
8
|
+
import { stepWorld } from "./sim/kernel.js";
|
|
9
|
+
import { deserializeReplay } from "./replay.js";
|
|
10
|
+
// ── FNV-64 hash ───────────────────────────────────────────────────────────────
|
|
11
|
+
// 64-bit Fowler–Noll–Vo (FNV-1a) over UTF-16 code units. Pure arithmetic,
|
|
12
|
+
// no external dependencies, portable across Node and browsers.
|
|
13
|
+
const FNV64_OFFSET = 14695981039346656037n;
|
|
14
|
+
const FNV64_PRIME = 1099511628211n;
|
|
15
|
+
const UINT64_MASK = 0xffffffffffffffffn;
|
|
16
|
+
function fnv64(data) {
|
|
17
|
+
let hash = FNV64_OFFSET;
|
|
18
|
+
for (let i = 0; i < data.length; i++) {
|
|
19
|
+
hash ^= BigInt(data.charCodeAt(i));
|
|
20
|
+
hash = (hash * FNV64_PRIME) & UINT64_MASK;
|
|
21
|
+
}
|
|
22
|
+
return hash;
|
|
23
|
+
}
|
|
24
|
+
// ── Stable JSON serialiser ────────────────────────────────────────────────────
|
|
25
|
+
// JSON.stringify with sorted object keys so property insertion order does not
|
|
26
|
+
// affect the hash. Maps (armourState, foodInventory, reputations) are
|
|
27
|
+
// serialised as sorted entry arrays to guarantee a canonical form.
|
|
28
|
+
function stableReplacer(_key, value) {
|
|
29
|
+
if (value instanceof Map) {
|
|
30
|
+
const entries = [...value.entries()]
|
|
31
|
+
.sort(([a], [b]) => String(a).localeCompare(String(b)));
|
|
32
|
+
return { __map__: entries };
|
|
33
|
+
}
|
|
34
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
35
|
+
const sorted = {};
|
|
36
|
+
for (const k of Object.keys(value).sort()) {
|
|
37
|
+
sorted[k] = value[k];
|
|
38
|
+
}
|
|
39
|
+
return sorted;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compute a deterministic 64-bit hash of the simulation's core state.
|
|
45
|
+
*
|
|
46
|
+
* Covers `tick`, `seed`, and all entity data sorted by `id`. Optional
|
|
47
|
+
* subsystem fields (`__sensoryEnv`, `__factionRegistry`, etc.) are excluded —
|
|
48
|
+
* they are host concerns and do not affect simulation determinism.
|
|
49
|
+
*
|
|
50
|
+
* Use this as a desync checksum in multiplayer loops:
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* const hash = hashWorldState(world);
|
|
54
|
+
* socket.emit("tick-ack", { tick: world.tick, hash: hash.toString() });
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @returns An unsigned 64-bit bigint.
|
|
58
|
+
*/
|
|
59
|
+
export function hashWorldState(world) {
|
|
60
|
+
const sorted = [...world.entities].sort((a, b) => a.id - b.id);
|
|
61
|
+
const canonical = JSON.stringify({ tick: world.tick, seed: world.seed, entities: sorted }, stableReplacer);
|
|
62
|
+
return fnv64(canonical);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Compare two replay traces tick-by-tick and find the first divergence.
|
|
66
|
+
*
|
|
67
|
+
* Steps both replays from their initial states in lock-step, computing
|
|
68
|
+
* `hashWorldState` after each tick. O(N) in replay length.
|
|
69
|
+
*
|
|
70
|
+
* @param replayA First replay (e.g. client A's recording).
|
|
71
|
+
* @param replayB Second replay (e.g. client B's recording).
|
|
72
|
+
* @param ctx KernelContext forwarded to `stepWorld`.
|
|
73
|
+
*/
|
|
74
|
+
export function diffReplays(replayA, replayB, ctx) {
|
|
75
|
+
const worldA = structuredClone(replayA.initialState);
|
|
76
|
+
const worldB = structuredClone(replayB.initialState);
|
|
77
|
+
// Check initial state before any steps.
|
|
78
|
+
const initA = hashWorldState(worldA);
|
|
79
|
+
const initB = hashWorldState(worldB);
|
|
80
|
+
if (initA !== initB) {
|
|
81
|
+
return { divergeAtTick: -1, hashA: initA, hashB: initB, ticksCompared: 0 };
|
|
82
|
+
}
|
|
83
|
+
const maxFrames = Math.min(replayA.frames.length, replayB.frames.length);
|
|
84
|
+
for (let i = 0; i < maxFrames; i++) {
|
|
85
|
+
const frameA = replayA.frames[i];
|
|
86
|
+
const frameB = replayB.frames[i];
|
|
87
|
+
const cmdsA = new Map(frameA.commands.map(([id, cmds]) => [id, cmds]));
|
|
88
|
+
const cmdsB = new Map(frameB.commands.map(([id, cmds]) => [id, cmds]));
|
|
89
|
+
stepWorld(worldA, cmdsA, ctx);
|
|
90
|
+
stepWorld(worldB, cmdsB, ctx);
|
|
91
|
+
const hA = hashWorldState(worldA);
|
|
92
|
+
const hB = hashWorldState(worldB);
|
|
93
|
+
if (hA !== hB) {
|
|
94
|
+
return {
|
|
95
|
+
divergeAtTick: worldA.tick,
|
|
96
|
+
hashA: hA,
|
|
97
|
+
hashB: hB,
|
|
98
|
+
ticksCompared: i + 1,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// If one replay has more frames, that's not a divergence — just a shorter
|
|
103
|
+
// recording on one side.
|
|
104
|
+
return { divergeAtTick: undefined, hashA: undefined, hashB: undefined, ticksCompared: maxFrames };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Parse two replay JSON strings and diff them.
|
|
108
|
+
*
|
|
109
|
+
* Convenience wrapper over `diffReplays` for CLI use.
|
|
110
|
+
*/
|
|
111
|
+
export function diffReplayJson(jsonA, jsonB, ctx) {
|
|
112
|
+
const replayA = deserializeReplay(jsonA);
|
|
113
|
+
const replayB = deserializeReplay(jsonB);
|
|
114
|
+
return diffReplays(replayA, replayB, ctx);
|
|
115
|
+
}
|
package/dist/tools/pack-cli.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// tools/pack-cli.ts — PA-4: Ananke
|
|
2
|
+
// tools/pack-cli.ts — PA-4 / PA-10: Ananke CLI
|
|
3
3
|
//
|
|
4
4
|
// Usage (after npm run build):
|
|
5
5
|
// node dist/tools/pack-cli.js pack validate <file.json>
|
|
6
6
|
// node dist/tools/pack-cli.js pack bundle <directory>
|
|
7
|
+
// node dist/tools/pack-cli.js replay diff <a.json> <b.json>
|
|
7
8
|
//
|
|
8
9
|
// Or via the installed binary:
|
|
9
10
|
// npx ananke pack validate <file.json>
|
|
10
11
|
// npx ananke pack bundle <directory>
|
|
12
|
+
// npx ananke replay diff <a.json> <b.json>
|
|
11
13
|
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
12
14
|
import { join, resolve, extname, basename } from "node:path";
|
|
13
15
|
import { validatePack, loadPack } from "../src/content-pack.js";
|
|
16
|
+
import { diffReplayJson } from "../src/netcode.js";
|
|
17
|
+
import { q } from "../src/units.js";
|
|
14
18
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
15
19
|
function readJson(filePath) {
|
|
16
20
|
try {
|
|
@@ -122,34 +126,99 @@ function cmdLoad(args) {
|
|
|
122
126
|
console.log(` scenarios: ${result.scenarioIds.join(", ") || "none"}`);
|
|
123
127
|
console.log(` fingerprint: ${result.fingerprint}`);
|
|
124
128
|
}
|
|
129
|
+
function cmdReplayDiff(args) {
|
|
130
|
+
const fileA = args[0];
|
|
131
|
+
const fileB = args[1];
|
|
132
|
+
if (!fileA || !fileB) {
|
|
133
|
+
console.error("Usage: ananke replay diff <replay-a.json> <replay-b.json>");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
let jsonA;
|
|
137
|
+
let jsonB;
|
|
138
|
+
try {
|
|
139
|
+
jsonA = readFileSync(resolve(fileA), "utf8");
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.error(`Cannot read ${fileA}: ${String(e)}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
jsonB = readFileSync(resolve(fileB), "utf8");
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
console.error(`Cannot read ${fileB}: ${String(e)}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const ctx = { tractionCoeff: q(1.0) };
|
|
153
|
+
const result = diffReplayJson(jsonA, jsonB, ctx);
|
|
154
|
+
console.log(`Ticks compared: ${result.ticksCompared}`);
|
|
155
|
+
if (result.divergeAtTick === undefined) {
|
|
156
|
+
console.log("✓ Replays are identical — no divergence detected.");
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
if (result.divergeAtTick === -1) {
|
|
160
|
+
console.error("✗ Initial states differ (before tick 0).");
|
|
161
|
+
console.error(` hash A: ${result.hashA?.toString(16)}`);
|
|
162
|
+
console.error(` hash B: ${result.hashB?.toString(16)}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
console.error(`✗ Divergence at tick ${result.divergeAtTick}.`);
|
|
166
|
+
console.error(` hash A: ${result.hashA?.toString(16)}`);
|
|
167
|
+
console.error(` hash B: ${result.hashB?.toString(16)}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
125
170
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
126
171
|
function main() {
|
|
127
172
|
const argv = process.argv.slice(2);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
console.log("Commands:");
|
|
132
|
-
console.log(" pack validate <file.json> — validate a pack manifest");
|
|
133
|
-
console.log(" pack bundle <directory> [out.json] — merge JSON files into one pack");
|
|
134
|
-
console.log(" pack load <file.json> — load a pack and report registered ids");
|
|
173
|
+
const cmd = argv[0];
|
|
174
|
+
if (!cmd) {
|
|
175
|
+
printHelp();
|
|
135
176
|
process.exit(0);
|
|
136
177
|
}
|
|
137
178
|
const sub = argv[1];
|
|
138
179
|
const rest = argv.slice(2);
|
|
139
|
-
switch (
|
|
140
|
-
case "
|
|
141
|
-
|
|
180
|
+
switch (cmd) {
|
|
181
|
+
case "pack":
|
|
182
|
+
switch (sub) {
|
|
183
|
+
case "validate":
|
|
184
|
+
cmdValidate(rest);
|
|
185
|
+
break;
|
|
186
|
+
case "bundle":
|
|
187
|
+
cmdBundle(rest);
|
|
188
|
+
break;
|
|
189
|
+
case "load":
|
|
190
|
+
cmdLoad(rest);
|
|
191
|
+
break;
|
|
192
|
+
default:
|
|
193
|
+
console.error(`Unknown subcommand: pack ${sub ?? ""}`);
|
|
194
|
+
console.error("Available: validate, bundle, load");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
142
197
|
break;
|
|
143
|
-
case "
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
198
|
+
case "replay":
|
|
199
|
+
switch (sub) {
|
|
200
|
+
case "diff":
|
|
201
|
+
cmdReplayDiff(rest);
|
|
202
|
+
break;
|
|
203
|
+
default:
|
|
204
|
+
console.error(`Unknown subcommand: replay ${sub ?? ""}`);
|
|
205
|
+
console.error("Available: diff");
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
148
208
|
break;
|
|
149
209
|
default:
|
|
150
|
-
console.error(`Unknown
|
|
151
|
-
|
|
210
|
+
console.error(`Unknown command: ${cmd}`);
|
|
211
|
+
printHelp();
|
|
152
212
|
process.exit(1);
|
|
153
213
|
}
|
|
154
214
|
}
|
|
215
|
+
function printHelp() {
|
|
216
|
+
console.log("Ananke CLI");
|
|
217
|
+
console.log("");
|
|
218
|
+
console.log("Commands:");
|
|
219
|
+
console.log(" pack validate <file.json> — validate a pack manifest");
|
|
220
|
+
console.log(" pack bundle <directory> [out.json] — merge JSON files into one pack");
|
|
221
|
+
console.log(" pack load <file.json> — load a pack and report registered ids");
|
|
222
|
+
console.log(" replay diff <replay-a.json> <replay-b.json> — find the first tick divergence between two replays");
|
|
223
|
+
}
|
|
155
224
|
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@its-not-rocket-science/ananke",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.59",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -197,6 +197,10 @@
|
|
|
197
197
|
"./host-loop": {
|
|
198
198
|
"import": "./dist/src/host-loop.js",
|
|
199
199
|
"types": "./dist/src/host-loop.d.ts"
|
|
200
|
+
},
|
|
201
|
+
"./netcode": {
|
|
202
|
+
"import": "./dist/src/netcode.js",
|
|
203
|
+
"types": "./dist/src/netcode.d.ts"
|
|
200
204
|
}
|
|
201
205
|
},
|
|
202
206
|
"workspaces": [
|
|
@@ -262,6 +266,8 @@
|
|
|
262
266
|
"example:combat": "node dist/examples/quickstart-combat.js",
|
|
263
267
|
"example:campaign": "node dist/examples/quickstart-campaign.js",
|
|
264
268
|
"example:species": "node dist/examples/quickstart-species.js",
|
|
269
|
+
"example:lockstep": "node dist/examples/lockstep-server.js",
|
|
270
|
+
"example:rollback": "node dist/examples/rollback-client.js",
|
|
265
271
|
"generate-module-index": "node dist/tools/generate-module-index.js",
|
|
266
272
|
"pack": "node dist/tools/pack-cli.js pack",
|
|
267
273
|
"generate-fixtures": "node dist/tools/generate-fixtures.js",
|