@rpgm-tools/neo-angband-core 0.28.0 → 0.29.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.
- package/README.md +1 -1
- package/dist/agent/entity-views.d.ts +6 -0
- package/dist/agent/entity-views.d.ts.map +1 -1
- package/dist/agent/entity-views.js +16 -1
- package/dist/agent/entity-views.js.map +1 -1
- package/dist/agent/types.d.ts +17 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js +7 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/mod/record-keys.d.ts +3 -4
- package/dist/mod/record-keys.d.ts.map +1 -1
- package/dist/mod/record-keys.js +4 -5
- package/dist/mod/record-keys.js.map +1 -1
- package/dist/mod/save-blocks.d.ts +48 -2
- package/dist/mod/save-blocks.d.ts.map +1 -1
- package/dist/mod/save-blocks.js +62 -0
- package/dist/mod/save-blocks.js.map +1 -1
- package/dist/obj/bind.d.ts.map +1 -1
- package/dist/obj/bind.js +30 -31
- package/dist/obj/bind.js.map +1 -1
- package/dist/obj/types.d.ts +9 -2
- package/dist/obj/types.d.ts.map +1 -1
- package/dist/session/game.d.ts +25 -1
- package/dist/session/game.d.ts.map +1 -1
- package/dist/session/game.js +17 -3
- package/dist/session/game.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/agent/entity-views.ts +21 -1
- package/src/agent/types.ts +17 -1
- package/src/mod/record-keys.ts +4 -5
- package/src/mod/save-blocks.ts +78 -1
- package/src/obj/bind.ts +30 -34
- package/src/obj/types.ts +9 -2
- package/src/session/game.ts +50 -3
- package/src/version.ts +1 -1
package/src/mod/record-keys.ts
CHANGED
|
@@ -28,7 +28,7 @@ export const CORE_RECORD_KEYS: Readonly<Record<string, readonly string[]>> = {
|
|
|
28
28
|
"curse": ["combat", "conflict", "conflict-flags", "desc", "effect", "flags", "msg", "name", "time", "type", "values"],
|
|
29
29
|
"dungeon_profile": ["alloc", "min-level", "name", "params", "room", "streamer", "tunnel"],
|
|
30
30
|
"ego_item": ["alloc", "brand", "combat", "desc", "flags", "flags-off", "info", "item", "min-combat", "min-values", "name", "slay", "type", "values"],
|
|
31
|
-
"flavor": ["
|
|
31
|
+
"flavor": ["entries", "kind"],
|
|
32
32
|
"hints": ["H"],
|
|
33
33
|
"history": ["chart", "phrase"],
|
|
34
34
|
"monster": ["armor-class", "base", "blow", "color", "color-cycle", "depth", "desc", "drop", "drop-base", "drop-order", "experience", "flags", "flags-off", "friends", "friends-base", "glyph", "hearing", "hit-points", "innate-freq", "light", "message-invis", "message-vis", "mimic", "name", "plural", "rarity", "shape", "sleepiness", "smell", "speed", "spell-freq", "spell-power", "spells"],
|
|
@@ -82,10 +82,9 @@ export const CORE_RECORD_KEYS: Readonly<Record<string, readonly string[]>> = {
|
|
|
82
82
|
* `ext`, believe the patch worked, and never learn the real field went
|
|
83
83
|
* untouched. The namespace is also what stops two mods coining the same word
|
|
84
84
|
* from silently overwriting each other. The rule is enforced at composition
|
|
85
|
-
* (mod-sdk fields.ts)
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* cannot bypass it.
|
|
85
|
+
* (mod-sdk fields.ts), which additionally requires the owning mod to have
|
|
86
|
+
* DECLARED the field; this is the same rule at the other end, so a record that
|
|
87
|
+
* reached core by some other route cannot bypass it.
|
|
89
88
|
*/
|
|
90
89
|
export function extensionData(
|
|
91
90
|
file: string,
|
package/src/mod/save-blocks.ts
CHANGED
|
@@ -86,7 +86,19 @@ export interface SavePackRef {
|
|
|
86
86
|
id: string;
|
|
87
87
|
/** Semver version string, e.g. "1.2.0". */
|
|
88
88
|
version: string;
|
|
89
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Content hash of the pack that produced the save (optional).
|
|
91
|
+
*
|
|
92
|
+
* ABSENT MEANS UNMEASURED, NEVER "UNCHANGED" - the same convention
|
|
93
|
+
* `InstalledModMeta.digests` and `SessionMod.digest` already use on the web
|
|
94
|
+
* side. Compared by `mismatchedNamespaces` (below) against the SAME
|
|
95
|
+
* namespace's current hash, when a caller has one: this is what lets a load
|
|
96
|
+
* tell "this pack patched a record instead of only adding one, and the
|
|
97
|
+
* patch is now different or gone" apart from an ordinary reload, which
|
|
98
|
+
* `orphanedNamespaces` alone cannot (issue #20) - a PATCHED core record
|
|
99
|
+
* still resolves under core's own, still-present namespace, so no entity is
|
|
100
|
+
* ever orphaned by the patch going away.
|
|
101
|
+
*/
|
|
90
102
|
hash?: string;
|
|
91
103
|
/** Source (git URL + ref, or marketplace id) the pack was installed from. */
|
|
92
104
|
source?: string;
|
|
@@ -269,6 +281,71 @@ export function orphanedNamespaces(
|
|
|
269
281
|
return out;
|
|
270
282
|
}
|
|
271
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Compare the pack set that produced a save against the packs present NOW,
|
|
286
|
+
* for the namespaces on BOTH sides - the sibling case `orphanedNamespaces`
|
|
287
|
+
* cannot see (issue #20). A pack that ADDS content and later goes away leaves
|
|
288
|
+
* an orphan behind (its own ids no longer resolve, so quarantine catches it);
|
|
289
|
+
* a pack that only PATCHES an existing record - a session mod re-pricing a
|
|
290
|
+
* core sword's damage, say - leaves nothing behind to orphan, because the
|
|
291
|
+
* record it touched still resolves under its own, still-present namespace.
|
|
292
|
+
* The composed value from save time is simply gone, silently, on the next
|
|
293
|
+
* load. This is that load's chance to notice: it returns the namespaces whose
|
|
294
|
+
* recorded hash and current hash are both known and disagree.
|
|
295
|
+
*
|
|
296
|
+
* "core" is never reported: core's own content changing between builds is an
|
|
297
|
+
* engine-version question (ENGINE_VERSION / SAVE_VERSION), not a mod-patch one.
|
|
298
|
+
*
|
|
299
|
+
* A namespace missing a hash on EITHER side is never reported - not measured
|
|
300
|
+
* is not the same claim as changed, and a false "this mod changed" alarm off
|
|
301
|
+
* a gap in the data would be worse than the silent gap this function exists
|
|
302
|
+
* to close. `currentPacks` need not cover every present namespace: a caller
|
|
303
|
+
* that cannot measure a pack's current content simply omits it, and that
|
|
304
|
+
* pack's own comparison is skipped rather than guessed at.
|
|
305
|
+
*/
|
|
306
|
+
export function mismatchedNamespaces(
|
|
307
|
+
manifest: SaveManifest,
|
|
308
|
+
currentPacks: readonly SavePackRef[],
|
|
309
|
+
): string[] {
|
|
310
|
+
const current = new Map(currentPacks.map((p) => [p.id, p]));
|
|
311
|
+
const out: string[] = [];
|
|
312
|
+
for (const recorded of manifest.packs) {
|
|
313
|
+
if (recorded.id === "core") continue;
|
|
314
|
+
if (!recorded.hash) continue;
|
|
315
|
+
const now = current.get(recorded.id);
|
|
316
|
+
if (!now?.hash) continue;
|
|
317
|
+
if (now.hash !== recorded.hash) out.push(recorded.id);
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Fold the packs present now into a manifest's pack list, for the NEXT save to
|
|
324
|
+
* carry forward: a namespace `currentPacks` supplies replaces whatever was
|
|
325
|
+
* recorded for it (so a changed hash/version is what the next
|
|
326
|
+
* `mismatchedNamespaces`/`orphanedNamespaces` call compares against); a
|
|
327
|
+
* namespace it does NOT supply keeps its old recorded entry untouched, rather
|
|
328
|
+
* than being dropped, so a pack this host cannot currently measure - or one
|
|
329
|
+
* that has gone away entirely - is still there for `orphanedNamespaces` to
|
|
330
|
+
* find missing later. Pure: neither argument is mutated.
|
|
331
|
+
*/
|
|
332
|
+
export function reconcilePackManifest(
|
|
333
|
+
manifest: SaveManifest,
|
|
334
|
+
currentPacks: readonly SavePackRef[],
|
|
335
|
+
): SaveManifest {
|
|
336
|
+
const current = new Map(currentPacks.map((p) => [p.id, p]));
|
|
337
|
+
const seen = new Set<string>();
|
|
338
|
+
const packs: SavePackRef[] = [];
|
|
339
|
+
for (const recorded of manifest.packs) {
|
|
340
|
+
packs.push(current.get(recorded.id) ?? recorded);
|
|
341
|
+
seen.add(recorded.id);
|
|
342
|
+
}
|
|
343
|
+
for (const pack of currentPacks) {
|
|
344
|
+
if (!seen.has(pack.id)) packs.push(pack);
|
|
345
|
+
}
|
|
346
|
+
return { ...manifest, packs };
|
|
347
|
+
}
|
|
348
|
+
|
|
272
349
|
/** The version each pack was at when the save was written (for orphan keys). */
|
|
273
350
|
function versionMap(manifest: SaveManifest): Map<string, string> {
|
|
274
351
|
const m = new Map<string, string>();
|
package/src/obj/bind.ts
CHANGED
|
@@ -1486,12 +1486,35 @@ export class ObjRegistry {
|
|
|
1486
1486
|
for (const rec of records) {
|
|
1487
1487
|
const tval = tvalFindIdx(rec.kind.tval);
|
|
1488
1488
|
if (tval <= 0) throw new Error(`flavor: unknown tval ${rec.kind.tval}`);
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1489
|
+
/*
|
|
1490
|
+
* FILE ORDER, DIRECTLY. flavor.txt writes a record's `fixed:` lines
|
|
1491
|
+
* above its `flavor:` lines (rings L18-30 before L32-73, amulets
|
|
1492
|
+
* L90-101 before L104-117), and upstream's parser builds one list in
|
|
1493
|
+
* the order the lines arrive - so this array has to be in that order
|
|
1494
|
+
* too. It is not cosmetic: `flavorInit` walks this list BACKWARDS to
|
|
1495
|
+
* reproduce C's prepend-into-a-linked-list (obj/flavor.ts,
|
|
1496
|
+
* flavor_assign_random), so a flavour's position in it is what decides
|
|
1497
|
+
* which kind it lands on.
|
|
1498
|
+
*
|
|
1499
|
+
* A binder that bound every `flavor:` line before every `fixed:` line
|
|
1500
|
+
* was invisible in an ordinary game, because a fixed flavour keeps its
|
|
1501
|
+
* own sval and `flavor_assign_random` skips it - the random ones kept
|
|
1502
|
+
* their relative order and every potion looked right. Under
|
|
1503
|
+
* `birth_randarts` it was not: `flavor_reset_fixed` scrubs every fixed
|
|
1504
|
+
* sval but the One Ring's, which throws all eight ring and amulet
|
|
1505
|
+
* `fixed:` entries into the random pool, and they landed at the wrong
|
|
1506
|
+
* end of the list. Same number of RNG draws either way, so the stream
|
|
1507
|
+
* never moved and nothing downstream diverged - it just handed out the
|
|
1508
|
+
* wrong gems.
|
|
1509
|
+
*
|
|
1510
|
+
* `entries` (issue #2) is what makes this correct for ANY line order,
|
|
1511
|
+
* not just the shipped fixed-then-flavor layout: the content compiler
|
|
1512
|
+
* emits one list per record, already in file order, with `kind` naming
|
|
1513
|
+
* which directive produced each entry - so a mod's flavor.txt that
|
|
1514
|
+
* interleaves `fixed:` and `flavor:` binds in its own true order
|
|
1515
|
+
* instead of always fixed-first.
|
|
1516
|
+
*/
|
|
1517
|
+
for (const entry of rec.entries) {
|
|
1495
1518
|
let sval = SV_UNKNOWN;
|
|
1496
1519
|
if (entry.sval !== undefined) {
|
|
1497
1520
|
sval = this.lookupSval(tval, entry.sval);
|
|
@@ -1504,34 +1527,7 @@ export class ObjRegistry {
|
|
|
1504
1527
|
dChar: rec.kind.glyph,
|
|
1505
1528
|
text: entry.desc ?? "",
|
|
1506
1529
|
});
|
|
1507
|
-
}
|
|
1508
|
-
/*
|
|
1509
|
-
* FIXED FIRST, WHICH IS FILE ORDER. flavor.txt writes a record's
|
|
1510
|
-
* `fixed:` lines above its `flavor:` lines (rings L18-30 before L32-73,
|
|
1511
|
-
* amulets L90-101 before L104-117), and the parser builds one list in
|
|
1512
|
-
* the order the lines arrive - so this array has to be in that order too.
|
|
1513
|
-
* It is not cosmetic: `flavorInit` walks this list BACKWARDS to reproduce
|
|
1514
|
-
* C's prepend-into-a-linked-list (obj/flavor.ts, flavor_assign_random),
|
|
1515
|
-
* so a flavour's position in it is what decides which kind it lands on.
|
|
1516
|
-
*
|
|
1517
|
-
* Binding `flavor:` first was invisible in an ordinary game, because a
|
|
1518
|
-
* fixed flavour keeps its own sval and `flavor_assign_random` skips it -
|
|
1519
|
-
* the random ones kept their relative order and every potion looked
|
|
1520
|
-
* right. Under `birth_randarts` it was not: `flavor_reset_fixed` scrubs
|
|
1521
|
-
* every fixed sval but the One Ring's, which throws all eight ring and
|
|
1522
|
-
* amulet `fixed:` entries into the random pool, and they were at the
|
|
1523
|
-
* wrong end of the list. Same number of RNG draws either way, so the
|
|
1524
|
-
* stream never moved and nothing downstream diverged - it just handed
|
|
1525
|
-
* out the wrong gems.
|
|
1526
|
-
*
|
|
1527
|
-
* The compiled record splits the two into separate arrays, so a record
|
|
1528
|
-
* that INTERLEAVED them could not be reproduced from this shape at all.
|
|
1529
|
-
* Nothing shipped does, and recovering it would mean the compiler
|
|
1530
|
-
* emitting one ordered list; the entry `index` cannot stand in for it,
|
|
1531
|
-
* since flavor.txt's own numbering is not the order it writes them in.
|
|
1532
|
-
*/
|
|
1533
|
-
for (const entry of rec.fixed ?? []) bindEntry(entry);
|
|
1534
|
-
for (const entry of rec.flavor ?? []) bindEntry(entry);
|
|
1530
|
+
}
|
|
1535
1531
|
}
|
|
1536
1532
|
}
|
|
1537
1533
|
|
package/src/obj/types.ts
CHANGED
|
@@ -318,7 +318,14 @@ export interface ObjectPropertyRecordJson {
|
|
|
318
318
|
bindui?: unknown;
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
/**
|
|
322
|
+
* One `flavor:` or `fixed:` line. `kind` tags which directive it came from -
|
|
323
|
+
* only a `fixed:` line carries `sval` - and its position in the record's
|
|
324
|
+
* `entries` array is the file's true line order (issue #2): flavor.txt's own
|
|
325
|
+
* `index` numbering is not that order, so it cannot stand in for it.
|
|
326
|
+
*/
|
|
321
327
|
export interface FlavorEntryJson {
|
|
328
|
+
kind: "flavor" | "fixed";
|
|
322
329
|
index: number;
|
|
323
330
|
sval?: string;
|
|
324
331
|
attr: string;
|
|
@@ -327,8 +334,8 @@ export interface FlavorEntryJson {
|
|
|
327
334
|
|
|
328
335
|
export interface FlavorRecordJson {
|
|
329
336
|
kind: { tval: string; glyph: string };
|
|
330
|
-
flavor
|
|
331
|
-
|
|
337
|
+
/** `flavor:` and `fixed:` lines, ONE list, in the order flavor.txt writes them. */
|
|
338
|
+
entries: FlavorEntryJson[];
|
|
332
339
|
}
|
|
333
340
|
|
|
334
341
|
/** The full bundle of pack JSON record arrays the registry binds from. */
|
package/src/session/game.ts
CHANGED
|
@@ -337,10 +337,17 @@ import { migrateSave } from "./save-migrate.js";
|
|
|
337
337
|
import { ContentIdResolver } from "../mod/ids.js";
|
|
338
338
|
import {
|
|
339
339
|
coreOnlyManifest,
|
|
340
|
+
mismatchedNamespaces,
|
|
340
341
|
quarantineSave,
|
|
342
|
+
reconcilePackManifest,
|
|
341
343
|
rehydrateSave,
|
|
342
344
|
} from "../mod/save-blocks.js";
|
|
343
|
-
import type {
|
|
345
|
+
import type {
|
|
346
|
+
ModBag,
|
|
347
|
+
OrphanStore,
|
|
348
|
+
SaveManifest,
|
|
349
|
+
SavePackRef,
|
|
350
|
+
} from "../mod/save-blocks.js";
|
|
344
351
|
|
|
345
352
|
/**
|
|
346
353
|
* The getItem seam body (cmd_get_item's "tgtitem" fast path, cmd-core.c L1060):
|
|
@@ -518,6 +525,17 @@ export interface StartedGame {
|
|
|
518
525
|
orphans: OrphanStore;
|
|
519
526
|
/** decision-8: whether the one-time orphan keep/purge prompt has been shown. */
|
|
520
527
|
orphansAcknowledged: boolean;
|
|
528
|
+
/**
|
|
529
|
+
* Namespaces present now whose recorded content hash no longer matches
|
|
530
|
+
* their current one (issue #20, save-blocks.ts mismatchedNamespaces): a
|
|
531
|
+
* pack that PATCHED a record - a session mod re-pricing a core sword,
|
|
532
|
+
* say - instead of only adding one, so nothing was orphaned when the patch
|
|
533
|
+
* changed or the pack went away. Empty on a core-only game, on a save
|
|
534
|
+
* written before this field, and whenever the loading host supplied no
|
|
535
|
+
* `currentPacks` (or none with a measurable hash) to compare against -
|
|
536
|
+
* absent evidence is not evidence of a match.
|
|
537
|
+
*/
|
|
538
|
+
mismatchedPacks: readonly string[];
|
|
521
539
|
/**
|
|
522
540
|
* What loadGame had to convert to read this save (session/save-migrate.ts).
|
|
523
541
|
* Absent on a new game and on a save already at SAVE_VERSION. `applied` is one
|
|
@@ -3792,6 +3810,7 @@ export function startGame(pack: GamePack, opts: StartGameOptions = {}): StartedG
|
|
|
3792
3810
|
mods: {},
|
|
3793
3811
|
orphans: {},
|
|
3794
3812
|
orphansAcknowledged: false,
|
|
3813
|
+
mismatchedPacks: [],
|
|
3795
3814
|
options,
|
|
3796
3815
|
randartSeed,
|
|
3797
3816
|
changeLevel,
|
|
@@ -4211,6 +4230,19 @@ export interface LoadGameOptions {
|
|
|
4211
4230
|
* entry (WP-14) passes this; a normal load leaves it false.
|
|
4212
4231
|
*/
|
|
4213
4232
|
wizard?: boolean;
|
|
4233
|
+
/**
|
|
4234
|
+
* The content digest this host can measure RIGHT NOW for each present pack
|
|
4235
|
+
* (issue #20) - one `SavePackRef` per namespace it can hash, with the pack's
|
|
4236
|
+
* CURRENT id/version/hash, not the save's recorded one. Compared against
|
|
4237
|
+
* the save's own recorded manifest (`mismatchedNamespaces`) to catch a
|
|
4238
|
+
* pack that PATCHED a record rather than only adding one, then folded into
|
|
4239
|
+
* the manifest this load returns (`reconcilePackManifest`) so the NEXT save
|
|
4240
|
+
* carries today's hash forward. Omitting an entry for a present namespace
|
|
4241
|
+
* is the honest "cannot measure this one yet" - it is compared against
|
|
4242
|
+
* nothing and never flagged as a mismatch. Absent/empty: no pack is
|
|
4243
|
+
* compared or updated, which is the behavior before this option existed.
|
|
4244
|
+
*/
|
|
4245
|
+
currentPacks?: readonly SavePackRef[];
|
|
4214
4246
|
}
|
|
4215
4247
|
|
|
4216
4248
|
export function loadGame(
|
|
@@ -4276,9 +4308,23 @@ export function loadGame(
|
|
|
4276
4308
|
modNoscore: saveIn.manifest?.modNoscore ?? false,
|
|
4277
4309
|
};
|
|
4278
4310
|
const isPresent = (ns: string): boolean => present.has(ns);
|
|
4311
|
+
|
|
4312
|
+
/* Issue #20's sibling check: a pack that PATCHED a record (rather than only
|
|
4313
|
+
* adding one) leaves nothing for quarantine to catch when the patch changes
|
|
4314
|
+
* or the pack goes away, because the record still resolves under its own,
|
|
4315
|
+
* still-present namespace. Computed against the manifest AS RECORDED, before
|
|
4316
|
+
* folding in what the host can measure now, so a genuinely different hash is
|
|
4317
|
+
* what gets reported rather than something this call just overwrote.
|
|
4318
|
+
* `reconcilePackManifest` then carries today's hash forward for the NEXT
|
|
4319
|
+
* save - a namespace the host cannot measure keeps its old recorded entry,
|
|
4320
|
+
* exactly as if this option had not been passed. */
|
|
4321
|
+
const currentPacks = opts.currentPacks ?? [];
|
|
4322
|
+
const mismatchedPacks = mismatchedNamespaces(manifest, currentPacks);
|
|
4323
|
+
const reconciledManifest = reconcilePackManifest(manifest, currentPacks);
|
|
4324
|
+
|
|
4279
4325
|
const quarantine = quarantineSave(
|
|
4280
4326
|
rehydrateSave(saveIn, isPresent),
|
|
4281
|
-
|
|
4327
|
+
reconciledManifest,
|
|
4282
4328
|
isPresent,
|
|
4283
4329
|
);
|
|
4284
4330
|
const save = quarantine.save;
|
|
@@ -4680,10 +4726,11 @@ export function loadGame(
|
|
|
4680
4726
|
flavor: wired.flavor,
|
|
4681
4727
|
everseen: wired.everseen,
|
|
4682
4728
|
seedFlavor,
|
|
4683
|
-
manifest,
|
|
4729
|
+
manifest: reconciledManifest,
|
|
4684
4730
|
mods: save.mods ?? {},
|
|
4685
4731
|
orphans: quarantine.orphans,
|
|
4686
4732
|
orphansAcknowledged: save.orphansAcknowledged ?? false,
|
|
4733
|
+
mismatchedPacks,
|
|
4687
4734
|
...(migration.applied.length > 0 || migration.notes.length > 0
|
|
4688
4735
|
? { saveMigration: { applied: migration.applied, notes: migration.notes } }
|
|
4689
4736
|
: {}),
|
package/src/version.ts
CHANGED
|
@@ -23,4 +23,4 @@ export const PARITY_BASELINE = "4.2.6";
|
|
|
23
23
|
* released tag is iterated takes a MINOR bump, because a published tag is pinned
|
|
24
24
|
* by digest in a catalogue and must never be moved.
|
|
25
25
|
*/
|
|
26
|
-
export const ENGINE_VERSION = "0.
|
|
26
|
+
export const ENGINE_VERSION = "0.29.0";
|