@its-not-rocket-science/ananke 0.1.9 → 0.1.10
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 +26 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/modding.d.ts +137 -0
- package/dist/src/modding.js +188 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
10
10
|
|
|
11
11
|
### Added
|
|
12
12
|
|
|
13
|
+
- **CE-16 · Modding Support** (`src/modding.ts`)
|
|
14
|
+
- Layer 1 — `hashMod(json)`: deterministic FNV-1a fingerprint (8-char hex) for any
|
|
15
|
+
parsed JSON mod file; canonical key-sorted serialisation ensures order-independence.
|
|
16
|
+
- Layer 2 — Post-tick behavior hooks: `registerPostTickHook / unregisterPostTickHook /
|
|
17
|
+
runPostTickHooks / listPostTickHooks / clearPostTickHooks`; hooks fire after
|
|
18
|
+
`stepWorld`, are purely observational (logging, analytics, renderer updates).
|
|
19
|
+
- Layer 3 — AI behavior node registry: `registerBehaviorNode / unregisterBehaviorNode /
|
|
20
|
+
getBehaviorNode / listBehaviorNodes / clearBehaviorNodes`; custom `BehaviorNode`
|
|
21
|
+
factories registered by id for scenario and behavior-tree composition.
|
|
22
|
+
- Session fingerprint: `computeModManifest(catalogIds)` returns sorted id lists and a
|
|
23
|
+
single fingerprint covering all three layers for multiplayer client validation.
|
|
24
|
+
- `clearAllMods()` resets hooks and behavior nodes (catalog unchanged).
|
|
25
|
+
- 42 tests in `test/modding.test.ts`; exported via `src/index.ts`.
|
|
26
|
+
|
|
13
27
|
- **CE-14 · Socio-Economic Campaign Layer → Stable Promotion**
|
|
14
28
|
- Promote `stepPolityDay`, `declareWar`, `makePeace`, `areAtWar`,
|
|
15
29
|
`createPolity`, `createPolityRegistry`, `Polity`, `PolityRegistry`,
|
|
@@ -84,6 +98,18 @@ Adding new **optional** fields to these interfaces is never a breaking change.
|
|
|
84
98
|
|
|
85
99
|
---
|
|
86
100
|
|
|
101
|
+
## [0.1.10] — 2026-03-24
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- **CE-16 · Modding Support — HashMod, Post-tick Hooks, Behaviour Node Registry** (`src/parallel.ts`)
|
|
106
|
+
- Three-layer modding contract: FNV-1a data fingerprinting, observational
|
|
107
|
+
post-tick hooks, and named AI behavior node factories. computeModManifest()
|
|
108
|
+
provides a single session fingerprint for multiplayer client validation.
|
|
109
|
+
- exported via src/index.ts.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
87
113
|
## [0.1.8] — 2026-03-24
|
|
88
114
|
|
|
89
115
|
### Added
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -32,3 +32,4 @@ export * from "./sim/cover.js"; // CE-15: CoverSegment, computeCoverProtection()
|
|
|
32
32
|
export * from "./sim/ai/behavior-trees.js"; // CE-10: BehaviorNode, FlankTarget, RetreatTo, ProtectAlly, GuardPosition, HealTarget, Sequence, Fallback
|
|
33
33
|
export * from "./snapshot.js"; // CE-9: diffWorldState(), applyDiff(), packDiff(), unpackDiff(), WorldStateDiff
|
|
34
34
|
export * from "./parallel.js"; // CE-7: partitionWorld(), mergePartitions(), detectBoundaryPairs(), assignEntitiesToPartitions()
|
|
35
|
+
export * from "./modding.js"; // CE-16: hashMod(), registerPostTickHook(), runPostTickHooks(), registerBehaviorNode(), computeModManifest()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CE-16 — Modding Support
|
|
3
|
+
*
|
|
4
|
+
* Three-layer modding contract built on CE-12 (data-driven catalog) and the stable API:
|
|
5
|
+
*
|
|
6
|
+
* **Layer 1 — Data mod fingerprinting**
|
|
7
|
+
* `hashMod(json)` produces a deterministic 8-char hex fingerprint of any parsed JSON
|
|
8
|
+
* mod file. The network replication layer (CE-11) compares fingerprints across clients
|
|
9
|
+
* to guarantee all participants use identical mod definitions.
|
|
10
|
+
*
|
|
11
|
+
* **Layer 2 — Post-tick behavior hooks**
|
|
12
|
+
* `registerPostTickHook(id, fn)` registers an observer callback that the host fires
|
|
13
|
+
* after each `stepWorld` call via `runPostTickHooks(world)`. Hooks are purely
|
|
14
|
+
* observational — they MUST NOT mutate `WorldState` during the call. Because they run
|
|
15
|
+
* outside the kernel path they cannot break determinism.
|
|
16
|
+
*
|
|
17
|
+
* **Layer 3 — AI behavior node overrides**
|
|
18
|
+
* `registerBehaviorNode(id, factory)` installs a named factory for custom
|
|
19
|
+
* `BehaviorNode` implementations. `loadScenario` (CE-3) can reference them by id in
|
|
20
|
+
* scenario JSON. AI overrides require explicit host opt-in.
|
|
21
|
+
*
|
|
22
|
+
* **Session fingerprint**
|
|
23
|
+
* `computeModManifest()` returns a single fingerprint covering all three registries,
|
|
24
|
+
* suitable for multiplayer session validation.
|
|
25
|
+
*/
|
|
26
|
+
import type { WorldState } from "./sim/world.js";
|
|
27
|
+
import type { BehaviorNode } from "./sim/ai/behavior-trees.js";
|
|
28
|
+
/**
|
|
29
|
+
* Produce a deterministic 8-character hex fingerprint for a parsed JSON mod
|
|
30
|
+
* object (archetype, weapon, armour, or any CE-12 catalog entry).
|
|
31
|
+
*
|
|
32
|
+
* Keys are sorted before hashing so `{ a:1, b:2 }` and `{ b:2, a:1 }` produce
|
|
33
|
+
* the same fingerprint. The result is stable across JS engines and Node versions
|
|
34
|
+
* as long as the JSON content is identical.
|
|
35
|
+
*
|
|
36
|
+
* Use `computeModManifest()` to fingerprint the full active mod set for session
|
|
37
|
+
* validation.
|
|
38
|
+
*/
|
|
39
|
+
export declare function hashMod(json: unknown): string;
|
|
40
|
+
export type PostTickHook = (world: WorldState) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Register an observational callback that fires after each `stepWorld` tick.
|
|
43
|
+
*
|
|
44
|
+
* The host is responsible for calling `runPostTickHooks(world)` immediately after
|
|
45
|
+
* `stepWorld(world, cmds, ctx)`. Hooks MUST NOT mutate `WorldState`; they are
|
|
46
|
+
* intended for analytics, logging, renderer updates, and network broadcast.
|
|
47
|
+
*
|
|
48
|
+
* Re-registering an existing id overwrites the previous hook.
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerPostTickHook(id: string, fn: PostTickHook): void;
|
|
51
|
+
/**
|
|
52
|
+
* Remove a previously registered post-tick hook.
|
|
53
|
+
* Returns `true` if the hook existed and was removed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function unregisterPostTickHook(id: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Invoke all registered post-tick hooks in registration order.
|
|
58
|
+
*
|
|
59
|
+
* Call this immediately after `stepWorld`:
|
|
60
|
+
* ```typescript
|
|
61
|
+
* stepWorld(world, cmds, ctx);
|
|
62
|
+
* runPostTickHooks(world);
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* Errors thrown by individual hooks are caught and re-thrown after all hooks
|
|
66
|
+
* have been attempted, to avoid silently dropping subsequent hooks.
|
|
67
|
+
*/
|
|
68
|
+
export declare function runPostTickHooks(world: WorldState): void;
|
|
69
|
+
/** Return the ids of all registered post-tick hooks in registration order. */
|
|
70
|
+
export declare function listPostTickHooks(): string[];
|
|
71
|
+
/** Remove all post-tick hooks (useful for testing and hot-reload scenarios). */
|
|
72
|
+
export declare function clearPostTickHooks(): void;
|
|
73
|
+
export type BehaviorNodeFactory = (...args: unknown[]) => BehaviorNode;
|
|
74
|
+
/**
|
|
75
|
+
* Register a named factory for a custom `BehaviorNode` implementation.
|
|
76
|
+
*
|
|
77
|
+
* The factory will be looked up by id when `loadScenario` (CE-3) encounters an
|
|
78
|
+
* `"aiOverride"` reference in scenario JSON, or when a host builds a behavior
|
|
79
|
+
* tree programmatically:
|
|
80
|
+
*
|
|
81
|
+
* ```typescript
|
|
82
|
+
* registerBehaviorNode("patrol_guard", (waypointX, waypointY) =>
|
|
83
|
+
* PatrolGuard(Number(waypointX), Number(waypointY))
|
|
84
|
+
* );
|
|
85
|
+
* const factory = getBehaviorNode("patrol_guard");
|
|
86
|
+
* const node = factory?.(1000, 2000);
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* **Deterministic multiplayer**: AI overrides affect simulation output. All
|
|
90
|
+
* clients must register the same behavior nodes (verified via `computeModManifest`)
|
|
91
|
+
* before joining a session.
|
|
92
|
+
*
|
|
93
|
+
* Re-registering an existing id overwrites the previous factory.
|
|
94
|
+
*/
|
|
95
|
+
export declare function registerBehaviorNode(id: string, factory: BehaviorNodeFactory): void;
|
|
96
|
+
/**
|
|
97
|
+
* Remove a previously registered behavior node factory.
|
|
98
|
+
* Returns `true` if the factory existed and was removed.
|
|
99
|
+
*/
|
|
100
|
+
export declare function unregisterBehaviorNode(id: string): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Look up a registered behavior node factory by id.
|
|
103
|
+
* Returns `undefined` if not found.
|
|
104
|
+
*/
|
|
105
|
+
export declare function getBehaviorNode(id: string): BehaviorNodeFactory | undefined;
|
|
106
|
+
/** Return the ids of all registered behavior node factories in registration order. */
|
|
107
|
+
export declare function listBehaviorNodes(): string[];
|
|
108
|
+
/** Remove all behavior node factories (useful for testing and hot-reload scenarios). */
|
|
109
|
+
export declare function clearBehaviorNodes(): void;
|
|
110
|
+
export interface ModManifest {
|
|
111
|
+
/** Sorted list of all data mod ids currently in the CE-12 catalog. */
|
|
112
|
+
dataIds: string[];
|
|
113
|
+
/** Sorted list of all registered post-tick hook ids. */
|
|
114
|
+
hookIds: string[];
|
|
115
|
+
/** Sorted list of all registered behavior node ids. */
|
|
116
|
+
behaviorIds: string[];
|
|
117
|
+
/**
|
|
118
|
+
* Single fingerprint covering all three id lists.
|
|
119
|
+
* Two clients are mod-compatible iff their `fingerprint` values match.
|
|
120
|
+
*/
|
|
121
|
+
fingerprint: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Compute a session manifest covering all active mods (CE-12 catalog entries,
|
|
125
|
+
* post-tick hooks, and AI behavior node overrides).
|
|
126
|
+
*
|
|
127
|
+
* The `fingerprint` is a deterministic 8-char hex string suitable for
|
|
128
|
+
* multiplayer session comparison. Clients are considered mod-compatible iff
|
|
129
|
+
* their fingerprints match.
|
|
130
|
+
*
|
|
131
|
+
* @param catalogIds Sorted list of CE-12 catalog entry ids (pass `listCatalog()`).
|
|
132
|
+
* Provided as a parameter to keep this module free of a circular dependency
|
|
133
|
+
* on `catalog.ts`.
|
|
134
|
+
*/
|
|
135
|
+
export declare function computeModManifest(catalogIds?: string[]): ModManifest;
|
|
136
|
+
/** Remove all hooks and behavior node factories. Does not affect the CE-12 catalog. */
|
|
137
|
+
export declare function clearAllMods(): void;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CE-16 — Modding Support
|
|
3
|
+
*
|
|
4
|
+
* Three-layer modding contract built on CE-12 (data-driven catalog) and the stable API:
|
|
5
|
+
*
|
|
6
|
+
* **Layer 1 — Data mod fingerprinting**
|
|
7
|
+
* `hashMod(json)` produces a deterministic 8-char hex fingerprint of any parsed JSON
|
|
8
|
+
* mod file. The network replication layer (CE-11) compares fingerprints across clients
|
|
9
|
+
* to guarantee all participants use identical mod definitions.
|
|
10
|
+
*
|
|
11
|
+
* **Layer 2 — Post-tick behavior hooks**
|
|
12
|
+
* `registerPostTickHook(id, fn)` registers an observer callback that the host fires
|
|
13
|
+
* after each `stepWorld` call via `runPostTickHooks(world)`. Hooks are purely
|
|
14
|
+
* observational — they MUST NOT mutate `WorldState` during the call. Because they run
|
|
15
|
+
* outside the kernel path they cannot break determinism.
|
|
16
|
+
*
|
|
17
|
+
* **Layer 3 — AI behavior node overrides**
|
|
18
|
+
* `registerBehaviorNode(id, factory)` installs a named factory for custom
|
|
19
|
+
* `BehaviorNode` implementations. `loadScenario` (CE-3) can reference them by id in
|
|
20
|
+
* scenario JSON. AI overrides require explicit host opt-in.
|
|
21
|
+
*
|
|
22
|
+
* **Session fingerprint**
|
|
23
|
+
* `computeModManifest()` returns a single fingerprint covering all three registries,
|
|
24
|
+
* suitable for multiplayer session validation.
|
|
25
|
+
*/
|
|
26
|
+
// ── Internal: FNV-1a 32-bit hash ──────────────────────────────────────────────
|
|
27
|
+
/** FNV-1a 32-bit hash over a UTF-16 code-unit string. */
|
|
28
|
+
function fnv1a32(s) {
|
|
29
|
+
let h = 0x811c9dc5;
|
|
30
|
+
for (let i = 0; i < s.length; i++) {
|
|
31
|
+
h ^= s.charCodeAt(i);
|
|
32
|
+
h = Math.imul(h, 0x01000193) | 0;
|
|
33
|
+
}
|
|
34
|
+
return h >>> 0;
|
|
35
|
+
}
|
|
36
|
+
/** Serialize any JSON-compatible value in canonical (key-sorted) form. */
|
|
37
|
+
function canonicalJson(v) {
|
|
38
|
+
if (v === null || typeof v !== "object")
|
|
39
|
+
return JSON.stringify(v);
|
|
40
|
+
if (Array.isArray(v))
|
|
41
|
+
return "[" + v.map(canonicalJson).join(",") + "]";
|
|
42
|
+
const obj = v;
|
|
43
|
+
return "{" + Object.keys(obj).sort().map(k => JSON.stringify(k) + ":" + canonicalJson(obj[k])).join(",") + "}";
|
|
44
|
+
}
|
|
45
|
+
// ── Layer 1: Data mod fingerprinting ─────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Produce a deterministic 8-character hex fingerprint for a parsed JSON mod
|
|
48
|
+
* object (archetype, weapon, armour, or any CE-12 catalog entry).
|
|
49
|
+
*
|
|
50
|
+
* Keys are sorted before hashing so `{ a:1, b:2 }` and `{ b:2, a:1 }` produce
|
|
51
|
+
* the same fingerprint. The result is stable across JS engines and Node versions
|
|
52
|
+
* as long as the JSON content is identical.
|
|
53
|
+
*
|
|
54
|
+
* Use `computeModManifest()` to fingerprint the full active mod set for session
|
|
55
|
+
* validation.
|
|
56
|
+
*/
|
|
57
|
+
export function hashMod(json) {
|
|
58
|
+
return fnv1a32(canonicalJson(json)).toString(16).padStart(8, "0");
|
|
59
|
+
}
|
|
60
|
+
const _hooks = new Map();
|
|
61
|
+
/**
|
|
62
|
+
* Register an observational callback that fires after each `stepWorld` tick.
|
|
63
|
+
*
|
|
64
|
+
* The host is responsible for calling `runPostTickHooks(world)` immediately after
|
|
65
|
+
* `stepWorld(world, cmds, ctx)`. Hooks MUST NOT mutate `WorldState`; they are
|
|
66
|
+
* intended for analytics, logging, renderer updates, and network broadcast.
|
|
67
|
+
*
|
|
68
|
+
* Re-registering an existing id overwrites the previous hook.
|
|
69
|
+
*/
|
|
70
|
+
export function registerPostTickHook(id, fn) {
|
|
71
|
+
if (!id)
|
|
72
|
+
throw new Error("Hook id must be a non-empty string");
|
|
73
|
+
_hooks.set(id, fn);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Remove a previously registered post-tick hook.
|
|
77
|
+
* Returns `true` if the hook existed and was removed.
|
|
78
|
+
*/
|
|
79
|
+
export function unregisterPostTickHook(id) {
|
|
80
|
+
return _hooks.delete(id);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Invoke all registered post-tick hooks in registration order.
|
|
84
|
+
*
|
|
85
|
+
* Call this immediately after `stepWorld`:
|
|
86
|
+
* ```typescript
|
|
87
|
+
* stepWorld(world, cmds, ctx);
|
|
88
|
+
* runPostTickHooks(world);
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* Errors thrown by individual hooks are caught and re-thrown after all hooks
|
|
92
|
+
* have been attempted, to avoid silently dropping subsequent hooks.
|
|
93
|
+
*/
|
|
94
|
+
export function runPostTickHooks(world) {
|
|
95
|
+
const errors = [];
|
|
96
|
+
for (const fn of _hooks.values()) {
|
|
97
|
+
try {
|
|
98
|
+
fn(world);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
errors.push(e);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (errors.length > 0)
|
|
105
|
+
throw errors[0];
|
|
106
|
+
}
|
|
107
|
+
/** Return the ids of all registered post-tick hooks in registration order. */
|
|
108
|
+
export function listPostTickHooks() {
|
|
109
|
+
return [..._hooks.keys()];
|
|
110
|
+
}
|
|
111
|
+
/** Remove all post-tick hooks (useful for testing and hot-reload scenarios). */
|
|
112
|
+
export function clearPostTickHooks() {
|
|
113
|
+
_hooks.clear();
|
|
114
|
+
}
|
|
115
|
+
const _behaviorNodes = new Map();
|
|
116
|
+
/**
|
|
117
|
+
* Register a named factory for a custom `BehaviorNode` implementation.
|
|
118
|
+
*
|
|
119
|
+
* The factory will be looked up by id when `loadScenario` (CE-3) encounters an
|
|
120
|
+
* `"aiOverride"` reference in scenario JSON, or when a host builds a behavior
|
|
121
|
+
* tree programmatically:
|
|
122
|
+
*
|
|
123
|
+
* ```typescript
|
|
124
|
+
* registerBehaviorNode("patrol_guard", (waypointX, waypointY) =>
|
|
125
|
+
* PatrolGuard(Number(waypointX), Number(waypointY))
|
|
126
|
+
* );
|
|
127
|
+
* const factory = getBehaviorNode("patrol_guard");
|
|
128
|
+
* const node = factory?.(1000, 2000);
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* **Deterministic multiplayer**: AI overrides affect simulation output. All
|
|
132
|
+
* clients must register the same behavior nodes (verified via `computeModManifest`)
|
|
133
|
+
* before joining a session.
|
|
134
|
+
*
|
|
135
|
+
* Re-registering an existing id overwrites the previous factory.
|
|
136
|
+
*/
|
|
137
|
+
export function registerBehaviorNode(id, factory) {
|
|
138
|
+
if (!id)
|
|
139
|
+
throw new Error("Behavior node id must be a non-empty string");
|
|
140
|
+
_behaviorNodes.set(id, factory);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Remove a previously registered behavior node factory.
|
|
144
|
+
* Returns `true` if the factory existed and was removed.
|
|
145
|
+
*/
|
|
146
|
+
export function unregisterBehaviorNode(id) {
|
|
147
|
+
return _behaviorNodes.delete(id);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Look up a registered behavior node factory by id.
|
|
151
|
+
* Returns `undefined` if not found.
|
|
152
|
+
*/
|
|
153
|
+
export function getBehaviorNode(id) {
|
|
154
|
+
return _behaviorNodes.get(id);
|
|
155
|
+
}
|
|
156
|
+
/** Return the ids of all registered behavior node factories in registration order. */
|
|
157
|
+
export function listBehaviorNodes() {
|
|
158
|
+
return [..._behaviorNodes.keys()];
|
|
159
|
+
}
|
|
160
|
+
/** Remove all behavior node factories (useful for testing and hot-reload scenarios). */
|
|
161
|
+
export function clearBehaviorNodes() {
|
|
162
|
+
_behaviorNodes.clear();
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Compute a session manifest covering all active mods (CE-12 catalog entries,
|
|
166
|
+
* post-tick hooks, and AI behavior node overrides).
|
|
167
|
+
*
|
|
168
|
+
* The `fingerprint` is a deterministic 8-char hex string suitable for
|
|
169
|
+
* multiplayer session comparison. Clients are considered mod-compatible iff
|
|
170
|
+
* their fingerprints match.
|
|
171
|
+
*
|
|
172
|
+
* @param catalogIds Sorted list of CE-12 catalog entry ids (pass `listCatalog()`).
|
|
173
|
+
* Provided as a parameter to keep this module free of a circular dependency
|
|
174
|
+
* on `catalog.ts`.
|
|
175
|
+
*/
|
|
176
|
+
export function computeModManifest(catalogIds = []) {
|
|
177
|
+
const dataIds = [...catalogIds].sort();
|
|
178
|
+
const hookIds = listPostTickHooks().slice().sort();
|
|
179
|
+
const behaviorIds = listBehaviorNodes().slice().sort();
|
|
180
|
+
const combined = JSON.stringify({ dataIds, hookIds, behaviorIds });
|
|
181
|
+
const fingerprint = fnv1a32(combined).toString(16).padStart(8, "0");
|
|
182
|
+
return { dataIds, hookIds, behaviorIds, fingerprint };
|
|
183
|
+
}
|
|
184
|
+
/** Remove all hooks and behavior node factories. Does not affect the CE-12 catalog. */
|
|
185
|
+
export function clearAllMods() {
|
|
186
|
+
_hooks.clear();
|
|
187
|
+
_behaviorNodes.clear();
|
|
188
|
+
}
|