@maka/maka-cli 5.91.0 → 5.92.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/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +9 -9
- package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/augmentations.d.ts +54 -3
- package/bundle/typescript/src/commands/game/sideQuest/augmentations.js +987 -53
- package/bundle/typescript/src/commands/game/sideQuest/augmentations.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.d.ts +0 -14
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +14 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/companions.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +8 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/inv.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +13 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/project.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/project.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +7 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.d.ts +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +69 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.d.ts +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js +62 -16
- package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +26 -3
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +95 -3
- package/bundle/typescript/src/commands/game/sideQuest/game.js +191 -13
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/inventory.d.ts +13 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/inventory.js +45 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/inventory.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.d.ts +65 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +83 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +106 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +128 -9
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/qualities.d.ts +33 -1
- package/bundle/typescript/src/commands/game/sideQuest/qualities.js +12 -2
- package/bundle/typescript/src/commands/game/sideQuest/qualities.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +4 -4
- package/bundle/typescript/src/commands/game/sideQuest/skill-groups.d.ts +120 -0
- package/bundle/typescript/src/commands/game/sideQuest/skill-groups.js +135 -0
- package/bundle/typescript/src/commands/game/sideQuest/skill-groups.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +20 -2
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/ui.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.d.ts +116 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js +127 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +123 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.d.ts +131 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +170 -12
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +139 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +307 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.d.ts +38 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +101 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/session-lock.js +19 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/session-lock.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.d.ts +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js +27 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +63 -30
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
- package/package.json +1 -1
|
@@ -18,11 +18,140 @@ import { ICatalogItem } from './catalog-data.js';
|
|
|
18
18
|
* source.ts, so models can import it without pulling in a command.
|
|
19
19
|
*/
|
|
20
20
|
export declare const ROLE_DOMAINS: Record<string, string[]>;
|
|
21
|
+
/** A catalog and the version it claims, from wherever it came. */
|
|
22
|
+
export interface ICatalogSource {
|
|
23
|
+
version: number;
|
|
24
|
+
items: ICatalogItem[];
|
|
25
|
+
origin: 'live' | 'cache' | 'seed';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A VERSION NUMBER, OR NOTHING.
|
|
29
|
+
*
|
|
30
|
+
* `Number(undefined)`, `Number('')` and `Number('1.26.0')` are all NaN,
|
|
31
|
+
* and every comparison against NaN is false -- so an unversioned payload
|
|
32
|
+
* silently loses to whatever it is compared with. That is the SAFE
|
|
33
|
+
* direction (the seed wins) but a silent one, so it is named here and
|
|
34
|
+
* logged at the call site rather than left to fall out of the operator.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseCatalogVersion(raw: unknown): number | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Reads the cache file. Injectable into loadCatalog because the
|
|
39
|
+
* alternative is a test that reads the DEVELOPER'S home directory --
|
|
40
|
+
* and this repo has already lost an afternoon to a suite that went red
|
|
41
|
+
* because a real config file said `hints: false`. A test must not care
|
|
42
|
+
* what is on the machine running it.
|
|
43
|
+
*/
|
|
44
|
+
type CacheReader = () => {
|
|
45
|
+
version: unknown;
|
|
46
|
+
items: ICatalogItem[];
|
|
47
|
+
} | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* RECORD WHAT THE NETWORK RETURNED -- separated from the fetch so the
|
|
50
|
+
* wiring is reachable without a socket.
|
|
51
|
+
*
|
|
52
|
+
* This is not a test hatch, it is the step refreshCatalog was doing
|
|
53
|
+
* inline, and pulling it out is what makes the ACTUAL BUG SITE
|
|
54
|
+
* testable: the defect was never in how the candidates compare, it was
|
|
55
|
+
* that loadCatalog returned the live list without ever comparing it.
|
|
56
|
+
* A suite that only exercises chooseCatalog proves the comparison and
|
|
57
|
+
* says nothing about whether anyone calls it -- restoring the original
|
|
58
|
+
* one-line bug left such a suite completely green.
|
|
59
|
+
*
|
|
60
|
+
* An unusable version scores 0: present, comparable, and last.
|
|
61
|
+
*/
|
|
62
|
+
export declare function applyLiveCatalog(items: ICatalogItem[], version: number | undefined): void;
|
|
21
63
|
/** Fire-and-forget refresh from the site -- call once at session start;
|
|
22
64
|
* the promise resolves when the live list (or a fallback) is settled. */
|
|
23
65
|
export declare function refreshCatalog(): Promise<void>;
|
|
24
|
-
/**
|
|
25
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The best catalog available right now: live > cached > baked-in --
|
|
68
|
+
* except that a cache OLDER than the shipped seed is not "best", it is
|
|
69
|
+
* stale, and the seed wins.
|
|
70
|
+
*
|
|
71
|
+
* THIS USED TO BE `cache?.items ?? CATALOG_ITEMS`, which handed an
|
|
72
|
+
* upgraded client its old catalog until a refresh happened to succeed.
|
|
73
|
+
* Found while wiring the drone hull lookup: this machine was serving a
|
|
74
|
+
* v6 cache of 198 items with 9 drones while the binary shipped a v10
|
|
75
|
+
* seed of 274 with 70 -- so three merged chunks were invisible offline.
|
|
76
|
+
*
|
|
77
|
+
* IT MATTERS MORE NOW THAN IT DID. When the catalog only fed shop
|
|
78
|
+
* listings, a stale cache meant "you cannot buy the new thing yet".
|
|
79
|
+
* maxDroneBoxes now reads Body from here, so staleness reaches COMBAT
|
|
80
|
+
* MATH: a drone the cache has never heard of falls through to the
|
|
81
|
+
* price-based fallback and is a different toughness than the same drone
|
|
82
|
+
* on a fresh client.
|
|
83
|
+
*
|
|
84
|
+
* A cache NEWER than the seed still wins, which is the whole point of
|
|
85
|
+
* the server being the source of truth.
|
|
86
|
+
*
|
|
87
|
+
* THE GUARD ORIGINALLY COVERED THE WRONG HALF, and the half it missed
|
|
88
|
+
* is the one nearly every player is on. It read:
|
|
89
|
+
*
|
|
90
|
+
* if (liveCatalog) return liveCatalog; // <-- unweighed
|
|
91
|
+
* ...then the version check, on the CACHE only
|
|
92
|
+
*
|
|
93
|
+
* so a FETCH SUCCEEDING was what defeated it. Measured 2026-08-31
|
|
94
|
+
* against the live endpoint: maka-cli.com served v6/198 rows while the
|
|
95
|
+
* shipped seed was v10/274. Online players got the 198 and offline
|
|
96
|
+
* players got the 274 -- being reachable was a DOWNGRADE, and the
|
|
97
|
+
* comment above about combat math was describing a live bug rather than
|
|
98
|
+
* a hypothetical: v6 carries 9 drones against v10's 70, so 61 drones
|
|
99
|
+
* missed catalogHullFor and took the price-based fallback for their
|
|
100
|
+
* condition monitor. Two clients, same drone, different toughness.
|
|
101
|
+
*
|
|
102
|
+
* The fix is to stop trusting ANY source's list because of where it
|
|
103
|
+
* came from, and weigh all three by the only thing that orders them.
|
|
104
|
+
*/
|
|
105
|
+
export declare function loadCatalog(readCacheFn?: CacheReader): ICatalogItem[];
|
|
106
|
+
/**
|
|
107
|
+
* WHICH OF THE THREE WINS -- pure, and separated out because this is
|
|
108
|
+
* where the bug lived. A decision buried in early returns is one nobody
|
|
109
|
+
* can put a fixture in front of; a named function taking all three
|
|
110
|
+
* candidates can be asked every question directly.
|
|
111
|
+
*
|
|
112
|
+
* Highest version wins. On a TIE the fresher origin wins (live over
|
|
113
|
+
* cache over seed), because equal versions mean equal content and the
|
|
114
|
+
* live list is the one that just came from the source of truth.
|
|
115
|
+
*
|
|
116
|
+
* The seed is not a fallback here, it is a CANDIDATE -- it is the one
|
|
117
|
+
* list whose version is knowable with certainty, since it shipped in
|
|
118
|
+
* the same binary as this function.
|
|
119
|
+
*/
|
|
120
|
+
export declare function chooseCatalog(live?: ICatalogSource, cache?: ICatalogSource): ICatalogSource;
|
|
121
|
+
/** Drop every memo -- the resolved winner and the hull index with it.
|
|
122
|
+
* Exported for tests; production invalidates via refreshCatalog. */
|
|
123
|
+
export declare function resetCatalogForTest(): void;
|
|
124
|
+
/**
|
|
125
|
+
* A DRONE'S HULL, from the catalog's own stat line.
|
|
126
|
+
*
|
|
127
|
+
* The engine used to size a drone's condition monitor off its RATING --
|
|
128
|
+
* which the catalog assigns by PRICE -- so durability tracked cost.
|
|
129
|
+
* Canon sizes it off BODY (SR5 p.199; anthroforms get two more,
|
|
130
|
+
* Rigger 5.0 p.145), and Body lives only in the `stats` string. This
|
|
131
|
+
* reads it back out.
|
|
132
|
+
*
|
|
133
|
+
* STRICT MATCHING, DELIBERATELY, and this is the important difference
|
|
134
|
+
* from findCatalogItem below. That one is for a PLAYER typing a name and
|
|
135
|
+
* falls back through squashed spellings and word overlap, because
|
|
136
|
+
* "metalink" should find the Meta Link. Here a loose hit would hand a
|
|
137
|
+
* piece of generated street gear some unrelated row's Body and silently
|
|
138
|
+
* resize its hull. A mechanical lookup gets exact slug or exact name and
|
|
139
|
+
* nothing else; anything unrecognised falls through to the documented
|
|
140
|
+
* fallback in models/item.ts.
|
|
141
|
+
*
|
|
142
|
+
* INDEXED, because this is read from a getter that combat touches.
|
|
143
|
+
* loadCatalog() re-reads the cache FILE on every call until a refresh
|
|
144
|
+
* has memoised a live list, so calling it per hull check would put a
|
|
145
|
+
* synchronous read in the middle of a fight.
|
|
146
|
+
*/
|
|
147
|
+
export interface ICatalogHull {
|
|
148
|
+
body: number;
|
|
149
|
+
anthro: boolean;
|
|
150
|
+
}
|
|
151
|
+
/** Drop the memo -- called when the catalog itself is replaced. */
|
|
152
|
+
export declare function resetCatalogHullIndex(): void;
|
|
153
|
+
export declare function catalogHullFor(name: string): ICatalogHull | undefined;
|
|
26
154
|
/** Fuzzy lookup for source.ts: exact slug, exact name, then loose
|
|
27
155
|
* word containment -- a single unambiguous hit or nothing. */
|
|
28
156
|
export declare function findCatalogItem(request: string): ICatalogItem | undefined;
|
|
157
|
+
export {};
|
|
@@ -36,8 +36,33 @@ const HOSTNAME = DEV ? 'localhost' : 'www.maka-cli.com';
|
|
|
36
36
|
const PORT = DEV ? Number(process.env.MAKA_DEV_PORT ?? 3000) : 443;
|
|
37
37
|
const TIMEOUT_MS = 8000;
|
|
38
38
|
const CACHE_PATH = path.join(os.homedir(), '.meteor', '.maka', 'side-quest-catalog.json');
|
|
39
|
-
let
|
|
39
|
+
let liveSource;
|
|
40
|
+
let resolved;
|
|
40
41
|
let fetchAttempted = false;
|
|
42
|
+
/**
|
|
43
|
+
* A VERSION NUMBER, OR NOTHING.
|
|
44
|
+
*
|
|
45
|
+
* `Number(undefined)`, `Number('')` and `Number('1.26.0')` are all NaN,
|
|
46
|
+
* and every comparison against NaN is false -- so an unversioned payload
|
|
47
|
+
* silently loses to whatever it is compared with. That is the SAFE
|
|
48
|
+
* direction (the seed wins) but a silent one, so it is named here and
|
|
49
|
+
* logged at the call site rather than left to fall out of the operator.
|
|
50
|
+
*/
|
|
51
|
+
export function parseCatalogVersion(raw) {
|
|
52
|
+
// AN ALLOW-LIST, because `Number()` is far too willing. Written first
|
|
53
|
+
// as `Number.isFinite(Number(raw))` and that was wrong in a way the
|
|
54
|
+
// test caught: Number(null), Number('') and Number([]) are all 0 --
|
|
55
|
+
// a real, comparable, LOWEST-possible version. The payload would then
|
|
56
|
+
// count as versioned, and refreshCatalog would overwrite a good cache
|
|
57
|
+
// with a worthless v0 list, which is precisely the poisoning this
|
|
58
|
+
// exists to stop.
|
|
59
|
+
if (typeof raw !== 'number' && typeof raw !== 'string')
|
|
60
|
+
return undefined;
|
|
61
|
+
if (typeof raw === 'string' && raw.trim() === '')
|
|
62
|
+
return undefined;
|
|
63
|
+
const n = Number(raw);
|
|
64
|
+
return Number.isFinite(n) ? n : undefined;
|
|
65
|
+
}
|
|
41
66
|
function readCache() {
|
|
42
67
|
try {
|
|
43
68
|
const parsed = JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
|
|
@@ -56,6 +81,25 @@ function writeCache(version, items) {
|
|
|
56
81
|
Logger.getInstance().write(`catalog cache write failed: ${err}`);
|
|
57
82
|
}
|
|
58
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* RECORD WHAT THE NETWORK RETURNED -- separated from the fetch so the
|
|
86
|
+
* wiring is reachable without a socket.
|
|
87
|
+
*
|
|
88
|
+
* This is not a test hatch, it is the step refreshCatalog was doing
|
|
89
|
+
* inline, and pulling it out is what makes the ACTUAL BUG SITE
|
|
90
|
+
* testable: the defect was never in how the candidates compare, it was
|
|
91
|
+
* that loadCatalog returned the live list without ever comparing it.
|
|
92
|
+
* A suite that only exercises chooseCatalog proves the comparison and
|
|
93
|
+
* says nothing about whether anyone calls it -- restoring the original
|
|
94
|
+
* one-line bug left such a suite completely green.
|
|
95
|
+
*
|
|
96
|
+
* An unusable version scores 0: present, comparable, and last.
|
|
97
|
+
*/
|
|
98
|
+
export function applyLiveCatalog(items, version) {
|
|
99
|
+
liveSource = { version: version ?? 0, items, origin: 'live' };
|
|
100
|
+
resolved = undefined;
|
|
101
|
+
resetCatalogHullIndex();
|
|
102
|
+
}
|
|
59
103
|
/** Fire-and-forget refresh from the site -- call once at session start;
|
|
60
104
|
* the promise resolves when the live list (or a fallback) is settled. */
|
|
61
105
|
export async function refreshCatalog() {
|
|
@@ -77,30 +121,144 @@ export async function refreshCatalog() {
|
|
|
77
121
|
});
|
|
78
122
|
const parsed = JSON.parse(data);
|
|
79
123
|
if (Array.isArray(parsed?.items) && parsed.items.length > 0) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
124
|
+
const version = parseCatalogVersion(parsed.version);
|
|
125
|
+
if (version === undefined) {
|
|
126
|
+
// Loud, because it costs the server its authority for this
|
|
127
|
+
// session: an unversioned list cannot be shown to be newer, so
|
|
128
|
+
// the guard below hands the seed to the player instead.
|
|
129
|
+
logger.write(`Catalog: maka-cli.com sent ${parsed.items.length} items with NO usable version `
|
|
130
|
+
+ `(${JSON.stringify(parsed.version)}) -- it cannot be compared, so the baked-in v${CATALOG_VERSION} stands.`);
|
|
131
|
+
}
|
|
132
|
+
const fetched = parsed.items;
|
|
133
|
+
applyLiveCatalog(fetched, version);
|
|
134
|
+
if (version !== undefined)
|
|
135
|
+
writeCache(version, fetched);
|
|
136
|
+
// Say what is being SERVED, not merely what arrived. The old line
|
|
137
|
+
// reported the fetch count as though it were the answer, which is
|
|
138
|
+
// how a stale server looked like a healthy one in the log while
|
|
139
|
+
// the player was being handed the seed.
|
|
140
|
+
logger.write(`Catalog: ${fetched.length} items from maka-cli.com (v${parsed.version}); `
|
|
141
|
+
+ `serving ${loadCatalog().length}.`);
|
|
83
142
|
return;
|
|
84
143
|
}
|
|
85
144
|
throw new Error('empty catalog payload');
|
|
86
145
|
}
|
|
87
146
|
catch (err) {
|
|
147
|
+
// NO ASSIGNMENT HERE ANY MORE. This branch used to set the live
|
|
148
|
+
// list from the cache, which is the same unversioned trust the
|
|
149
|
+
// fetch path was guilty of. loadCatalog reads the cache itself and
|
|
150
|
+
// weighs it, so being offline is now just "no live source".
|
|
151
|
+
resolved = undefined;
|
|
152
|
+
resetCatalogHullIndex();
|
|
88
153
|
const cache = readCache();
|
|
89
154
|
if (cache) {
|
|
90
|
-
|
|
91
|
-
|
|
155
|
+
logger.write(`Catalog: offline -- cache holds ${cache.items.length} items (v${cache.version}); `
|
|
156
|
+
+ `serving ${loadCatalog().length}. (${err.message})`);
|
|
92
157
|
}
|
|
93
158
|
else {
|
|
94
159
|
logger.write(`Catalog: offline, no cache -- serving the ${CATALOG_ITEMS.length} baked-in items. (${err.message})`);
|
|
95
160
|
}
|
|
96
161
|
}
|
|
97
162
|
}
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
163
|
+
/**
|
|
164
|
+
* The best catalog available right now: live > cached > baked-in --
|
|
165
|
+
* except that a cache OLDER than the shipped seed is not "best", it is
|
|
166
|
+
* stale, and the seed wins.
|
|
167
|
+
*
|
|
168
|
+
* THIS USED TO BE `cache?.items ?? CATALOG_ITEMS`, which handed an
|
|
169
|
+
* upgraded client its old catalog until a refresh happened to succeed.
|
|
170
|
+
* Found while wiring the drone hull lookup: this machine was serving a
|
|
171
|
+
* v6 cache of 198 items with 9 drones while the binary shipped a v10
|
|
172
|
+
* seed of 274 with 70 -- so three merged chunks were invisible offline.
|
|
173
|
+
*
|
|
174
|
+
* IT MATTERS MORE NOW THAN IT DID. When the catalog only fed shop
|
|
175
|
+
* listings, a stale cache meant "you cannot buy the new thing yet".
|
|
176
|
+
* maxDroneBoxes now reads Body from here, so staleness reaches COMBAT
|
|
177
|
+
* MATH: a drone the cache has never heard of falls through to the
|
|
178
|
+
* price-based fallback and is a different toughness than the same drone
|
|
179
|
+
* on a fresh client.
|
|
180
|
+
*
|
|
181
|
+
* A cache NEWER than the seed still wins, which is the whole point of
|
|
182
|
+
* the server being the source of truth.
|
|
183
|
+
*
|
|
184
|
+
* THE GUARD ORIGINALLY COVERED THE WRONG HALF, and the half it missed
|
|
185
|
+
* is the one nearly every player is on. It read:
|
|
186
|
+
*
|
|
187
|
+
* if (liveCatalog) return liveCatalog; // <-- unweighed
|
|
188
|
+
* ...then the version check, on the CACHE only
|
|
189
|
+
*
|
|
190
|
+
* so a FETCH SUCCEEDING was what defeated it. Measured 2026-08-31
|
|
191
|
+
* against the live endpoint: maka-cli.com served v6/198 rows while the
|
|
192
|
+
* shipped seed was v10/274. Online players got the 198 and offline
|
|
193
|
+
* players got the 274 -- being reachable was a DOWNGRADE, and the
|
|
194
|
+
* comment above about combat math was describing a live bug rather than
|
|
195
|
+
* a hypothetical: v6 carries 9 drones against v10's 70, so 61 drones
|
|
196
|
+
* missed catalogHullFor and took the price-based fallback for their
|
|
197
|
+
* condition monitor. Two clients, same drone, different toughness.
|
|
198
|
+
*
|
|
199
|
+
* The fix is to stop trusting ANY source's list because of where it
|
|
200
|
+
* came from, and weigh all three by the only thing that orders them.
|
|
201
|
+
*/
|
|
202
|
+
export function loadCatalog(readCacheFn = readCache) {
|
|
203
|
+
if (resolved)
|
|
204
|
+
return resolved.items;
|
|
205
|
+
const cached = readCacheFn();
|
|
206
|
+
const cacheVersion = parseCatalogVersion(cached?.version);
|
|
207
|
+
resolved = chooseCatalog(liveSource, cached && cacheVersion !== undefined
|
|
208
|
+
? { version: cacheVersion, items: cached.items, origin: 'cache' }
|
|
209
|
+
: undefined);
|
|
210
|
+
return resolved.items;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* WHICH OF THE THREE WINS -- pure, and separated out because this is
|
|
214
|
+
* where the bug lived. A decision buried in early returns is one nobody
|
|
215
|
+
* can put a fixture in front of; a named function taking all three
|
|
216
|
+
* candidates can be asked every question directly.
|
|
217
|
+
*
|
|
218
|
+
* Highest version wins. On a TIE the fresher origin wins (live over
|
|
219
|
+
* cache over seed), because equal versions mean equal content and the
|
|
220
|
+
* live list is the one that just came from the source of truth.
|
|
221
|
+
*
|
|
222
|
+
* The seed is not a fallback here, it is a CANDIDATE -- it is the one
|
|
223
|
+
* list whose version is knowable with certainty, since it shipped in
|
|
224
|
+
* the same binary as this function.
|
|
225
|
+
*/
|
|
226
|
+
export function chooseCatalog(live, cache) {
|
|
227
|
+
let best = { version: CATALOG_VERSION, items: CATALOG_ITEMS, origin: 'seed' };
|
|
228
|
+
// Ascending freshness, with >= so a later candidate takes a tie.
|
|
229
|
+
for (const candidate of [cache, live]) {
|
|
230
|
+
if (candidate && candidate.items.length > 0 && candidate.version >= best.version) {
|
|
231
|
+
best = candidate;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return best;
|
|
235
|
+
}
|
|
236
|
+
/** Drop every memo -- the resolved winner and the hull index with it.
|
|
237
|
+
* Exported for tests; production invalidates via refreshCatalog. */
|
|
238
|
+
export function resetCatalogForTest() {
|
|
239
|
+
liveSource = undefined;
|
|
240
|
+
resolved = undefined;
|
|
241
|
+
fetchAttempted = false;
|
|
242
|
+
resetCatalogHullIndex();
|
|
243
|
+
}
|
|
244
|
+
let hullIndex;
|
|
245
|
+
/** Drop the memo -- called when the catalog itself is replaced. */
|
|
246
|
+
export function resetCatalogHullIndex() {
|
|
247
|
+
hullIndex = undefined;
|
|
248
|
+
}
|
|
249
|
+
export function catalogHullFor(name) {
|
|
250
|
+
if (!hullIndex) {
|
|
251
|
+
hullIndex = new Map();
|
|
252
|
+
for (const item of loadCatalog()) {
|
|
253
|
+
const bod = /Bod (\d+)/.exec(item.stats ?? '');
|
|
254
|
+
if (!bod)
|
|
255
|
+
continue;
|
|
256
|
+
const hull = { body: Number(bod[1]), anthro: /anthro:/.test(item.stats ?? '') };
|
|
257
|
+
hullIndex.set(item.slug.toLowerCase(), hull);
|
|
258
|
+
hullIndex.set(item.name.toLowerCase(), hull);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return hullIndex.get(name.trim().toLowerCase());
|
|
104
262
|
}
|
|
105
263
|
/** Fuzzy lookup for source.ts: exact slug, exact name, then loose
|
|
106
264
|
* word containment -- a single unambiguous hit or nothing. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/utilities/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,aAAa,EAAgB,MAAM,mBAAmB,CAAC;AAEjF;;;;;;;;GAQG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,YAAY,GAA6B;IACpD,aAAa,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAC;IACzL,aAAa,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,gBAAgB,CAAC;IACtE,YAAY,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,CAAC;IACnG,UAAU,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC;IACpC,aAAa,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,CAAC;IACjH,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC;CACvF,CAAC;AAEF,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM,CAAC;AAC5C,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,kBAAkB,CAAC;AACxD,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AACnE,MAAM,UAAU,GAAG,IAAI,CAAC;AACxB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,yBAAyB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/utilities/catalog.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,15 @@ export declare function authToken(): string | undefined;
|
|
|
4
4
|
* the token) -- the value runner saves BIND to (account-binding
|
|
5
5
|
* ruling). Never printed; never leaves the machine. */
|
|
6
6
|
export declare function accountUserId(): string | undefined;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* `stale` is the server's monotonic guard answering 409: the stored save
|
|
9
|
+
* is NEWER than the one we pushed (§8). Distinct from 'offline' because
|
|
10
|
+
* the two demand opposite responses -- an outage means retry, a stale
|
|
11
|
+
* means STOP pushing and let a human reconcile at the next launch.
|
|
12
|
+
* Folding it into 'offline' would make this client retry forever against
|
|
13
|
+
* a server that is right to refuse it.
|
|
14
|
+
*/
|
|
15
|
+
export type CloudResult = 'ok' | 'unauthorized' | 'offline' | 'stale';
|
|
8
16
|
export interface ICloudSummary {
|
|
9
17
|
slug: string;
|
|
10
18
|
playerName: string;
|
|
@@ -60,3 +68,133 @@ export declare function reconcile(local: {
|
|
|
60
68
|
} | undefined, cloud: {
|
|
61
69
|
savedAt: string;
|
|
62
70
|
} | undefined): ReconcileVerdict;
|
|
71
|
+
/**
|
|
72
|
+
* THE COALESCING SINGLE-FLIGHT PUSH QUEUE (server-only storage, PR1).
|
|
73
|
+
*
|
|
74
|
+
* A bound runner's save lives on the server; a beat writes the SPILL
|
|
75
|
+
* synchronously (persistence.spillPathFor) and then enqueues here. Two
|
|
76
|
+
* properties, and both are about not lying to the player:
|
|
77
|
+
*
|
|
78
|
+
* SINGLE-FLIGHT: one request in the air at a time. Overlapping pushes
|
|
79
|
+
* arrive out of order, and last-writer-wins on an out-of-order pair
|
|
80
|
+
* means a later beat can be overwritten by an earlier one that happened
|
|
81
|
+
* to land second -- silently, and only visible as lost progress.
|
|
82
|
+
*
|
|
83
|
+
* COALESCING: the queue holds exactly ONE pending save, the newest.
|
|
84
|
+
* Intermediate snapshots are obsolete the moment a newer one exists, so
|
|
85
|
+
* a slow link must not build a backlog it then has to replay. Play
|
|
86
|
+
* generates beats faster than a bad connection drains them; a FIFO here
|
|
87
|
+
* would guarantee the queue never empties.
|
|
88
|
+
*
|
|
89
|
+
* The spill is what makes both safe: nothing is dropped, because
|
|
90
|
+
* whatever is not confirmed is still on disk.
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* How a queued save reaches the server. A PARAMETER rather than a hard
|
|
94
|
+
* call to pushSave, because the queue's job is ORDERING and the
|
|
95
|
+
* transport is not part of it -- and because an ES module's exports are
|
|
96
|
+
* read-only, so a test cannot substitute pushSave any other way. The
|
|
97
|
+
* seam follows the shape this codebase already uses for narrow
|
|
98
|
+
* dependencies (IEphemeralHost, attachInputHistory's `render`).
|
|
99
|
+
*/
|
|
100
|
+
export type PushFn = (saveFilePath: string, save: ISaveFileV1) => Promise<CloudResult>;
|
|
101
|
+
/** Test seam: the queue is module state, and a suite that ran two cases
|
|
102
|
+
* against one dirty queue would be testing the previous test. */
|
|
103
|
+
export declare function _resetPushQueue(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Enqueue a bound save. Returns immediately -- requestSave's contract is
|
|
106
|
+
* synchronous (game.ts) and the spill has already been written by the
|
|
107
|
+
* time this is called, so nothing here is allowed to block a beat.
|
|
108
|
+
*/
|
|
109
|
+
export declare function enqueueBoundPush(saveFilePath: string, save: ISaveFileV1, spillPath?: string, push?: PushFn): void;
|
|
110
|
+
/**
|
|
111
|
+
* Drain to completion -- the QUIT SEAL and the job-accept handoff.
|
|
112
|
+
*
|
|
113
|
+
* These are the two places where "eventually" is not good enough: quit
|
|
114
|
+
* ends the process (nothing later will retry), and accepting a job hands
|
|
115
|
+
* this character to a shared run that loads it from the SERVER, so an
|
|
116
|
+
* unpushed beat is a stale snapshot the whole table then plays against.
|
|
117
|
+
* That race exists today and this is what closes it.
|
|
118
|
+
*
|
|
119
|
+
* Resolves true when the server has the newest save. False means the
|
|
120
|
+
* spill stands and the next launch will carry it.
|
|
121
|
+
*/
|
|
122
|
+
export declare function flushPushQueue(): Promise<boolean>;
|
|
123
|
+
/** True while a bound save has not reached the server. */
|
|
124
|
+
export declare function hasPendingPush(): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Remove a spill whose contents the server has confirmed. Guarded on
|
|
127
|
+
* savedAt: a spill rewritten by a newer beat between the push and the
|
|
128
|
+
* confirmation is NOT the file we just uploaded, and deleting it would
|
|
129
|
+
* throw away the only copy of that beat.
|
|
130
|
+
*/
|
|
131
|
+
export declare function clearSpill(spillPath: string, confirmedSavedAt: string): void;
|
|
132
|
+
/**
|
|
133
|
+
* Write the spill, SYNCHRONOUSLY, and never throw.
|
|
134
|
+
*
|
|
135
|
+
* requestSave's signature is synchronous and that is load-bearing: it is
|
|
136
|
+
* called from `process.on('exit')`, where nothing asynchronous can run
|
|
137
|
+
* at all. For a bound runner the spill write IS the durability
|
|
138
|
+
* guarantee at that moment -- the push is best-effort and the process is
|
|
139
|
+
* already leaving.
|
|
140
|
+
*
|
|
141
|
+
* SWALLOWS ITS ERRORS ON PURPOSE, and this is the one place where that
|
|
142
|
+
* is right rather than lazy: a full disk or a permissions fault during
|
|
143
|
+
* the exit hook must not turn a clean quit into a crash on the way out.
|
|
144
|
+
* The player would lose the beat either way; throwing loses the exit
|
|
145
|
+
* too, and prints a stack trace over their last screen.
|
|
146
|
+
*/
|
|
147
|
+
export declare function writeSpill(spillPath: string, save: ISaveFileV1): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* ONE SESSION PER RUNNER, ACROSS MACHINES (§8).
|
|
150
|
+
*
|
|
151
|
+
* The local `.lock` already stops two terminals on one box. It cannot
|
|
152
|
+
* see another machine, and without this a second laptop playing the same
|
|
153
|
+
* bound runner is two sessions pushing to one slot -- last-push-wins,
|
|
154
|
+
* with the loser's evening silently gone.
|
|
155
|
+
*
|
|
156
|
+
* TRANSITIONAL TOLERANCE, and it is deliberately loud in the code so it
|
|
157
|
+
* gets removed: the server half of this contract does not exist yet
|
|
158
|
+
* (rollout step 1 ships it; this is step 2). A 404 therefore means
|
|
159
|
+
* "this server does not know about leases", NOT "the lease is free" --
|
|
160
|
+
* and the honest reading of an unimplemented guard is to proceed
|
|
161
|
+
* without it, exactly as this client did before the guard existed.
|
|
162
|
+
*
|
|
163
|
+
* REMOVE THIS BRANCH once the server contract is deployed everywhere.
|
|
164
|
+
* Left in place it would mask a genuinely missing endpoint forever,
|
|
165
|
+
* which is the same shape as the compat branch that hid the review
|
|
166
|
+
* queue's blindness for a day.
|
|
167
|
+
*/
|
|
168
|
+
export type LeaseResult = {
|
|
169
|
+
ok: true;
|
|
170
|
+
ttlMs?: number;
|
|
171
|
+
} | {
|
|
172
|
+
ok: false;
|
|
173
|
+
reason: 'held';
|
|
174
|
+
heldBy?: string;
|
|
175
|
+
since?: string;
|
|
176
|
+
} | {
|
|
177
|
+
ok: false;
|
|
178
|
+
reason: 'offline' | 'unauthorized' | 'unsupported';
|
|
179
|
+
};
|
|
180
|
+
export declare const acquireLease: (slug: string, sessionId: string) => Promise<LeaseResult>;
|
|
181
|
+
export declare const releaseLease: (slug: string, sessionId: string) => Promise<LeaseResult>;
|
|
182
|
+
/**
|
|
183
|
+
* Whether a lease failure should STOP the boot.
|
|
184
|
+
*
|
|
185
|
+
* ONLY a held lease does. Offline, unauthorized and unsupported all mean
|
|
186
|
+
* "we could not ask", and refusing to play because the lock server was
|
|
187
|
+
* unreachable would turn every outage into a lockout -- which is the
|
|
188
|
+
* opposite of ruling 2 (an outage degrades to the spill, it does not end
|
|
189
|
+
* the session). A HELD lease is the one case where we know the answer
|
|
190
|
+
* and the answer is no.
|
|
191
|
+
*/
|
|
192
|
+
export declare function leaseRefusesBoot(result: LeaseResult): boolean;
|
|
193
|
+
/**
|
|
194
|
+
* Keep the lease alive while playing. Renewal FAILURES ARE TOLERATED:
|
|
195
|
+
* a dropped renew during an outage must not kill a live session -- the
|
|
196
|
+
* server's TTL expires it on its own, and a crash is exactly what the
|
|
197
|
+
* TTL is for.
|
|
198
|
+
*/
|
|
199
|
+
export declare function startLeaseRenewal(slug: string, sessionId: string, everyMs?: number): void;
|
|
200
|
+
export declare function stopLeaseRenewal(): void;
|