endfield-mcp
Version:
MCP Server for Arknights: Endfield (TypeScript / stdio + Streamable HTTP)
203 lines • 8.05 kB
JavaScript
/**
* Worldview reader core — store binding, RichContent bridge, catalog loading.
*
* This module is the shared spine of the worldview (PRTS archive + in-game
* wiki) domain, mirroring `storyCore.ts`'s role in the story domain. It owns
* the JsonStore reference, the RichContentTable bridge cache, and the
* eager-loaded catalog caches (categories, first-level groups, wiki
* structure). The categories/documents/search sub-modules import the lazy
* loaders exported here.
*
* ## The RichContent bridge (the one worldview-specific capability)
*
* Unlike the tables domain (where `{id, text}` carries a direct int64 hash),
* PRTS documents carry a **string `contentId`** (e.g. "text_v0d8_10"). That
* key indexes RichContentTable.json, whose entry is a titled list of content
* segments:
*
* `RichContentTable[contentId]` = `{ title: {id, text}, contentList: [{content: {id, text}}, ...] }`
*
* Each segment's `content.id` is an int64 hash resolved through i18n.
* `resolveContent(contentId)` walks that structure and concatenates the
* resolved segments into one body string (image tags like `<image>...</image>`
* are passed through verbatim — the caller decides whether to render them).
*
* This keeps `texts.ts` hash-only (its design contract) while giving the
* worldview domain a contentId resolver. The bridge is local to this module.
*
* ## Loading strategy
*
* Catalogs (categories / firstLv / wiki categories+groups) load eagerly on
* first access — together they are <1.5MB. The RichContentTable (~390KB,
* 586 entries) also loads eagerly because document bodies and the search
* index both need it. All parsed once and cached for the process lifetime;
* invalidated together on sync refresh via `clearWorldviewCatalogCaches()`.
*
* ## Cache lifecycle
*
* This module owns the RichContent + catalog caches. `clearWorldviewCatalogCaches()`
* resets just those. The full-domain orchestrators (`bindWorldviewStore`,
* `clearWorldviewCaches`) live in `./worldview.ts` (the barrel facade) so
* they can clear sibling caches atomically without this module reaching into
* its siblings (preserving one-directional layering).
*/
import { resolveText as _resolveText } from "./texts.js";
// ---------------------------------------------------------------------------
// Store reference + catalog caches (module-level singletons)
// ---------------------------------------------------------------------------
let _store = null;
let _richContent = null;
let _prtsCategories = null;
let _firstLvGroups = null;
let _allItems = null;
let _wikiCategories = null;
let _wikiGroups = null;
/**
* Wire the JsonStore. Called once at startup by the barrel's
* `bindWorldviewStore` orchestrator, which also clears all sibling caches.
*/
export function setWorldviewStore(store) {
_store = store;
}
/** Reset only the caches owned by this module (RichContent + catalogs). */
export function clearWorldviewCatalogCaches() {
_richContent = null;
_prtsCategories = null;
_firstLvGroups = null;
_allItems = null;
_wikiCategories = null;
_wikiGroups = null;
}
/** Whether the worldview bundle is present in the bound store. */
export function hasWorldviewData() {
try {
return store().exists("RichContentTable.json");
}
catch {
return false;
}
}
// ---------------------------------------------------------------------------
// Internal accessors — exported for sibling modules (same domain, same layer)
// ---------------------------------------------------------------------------
export function store() {
if (_store === null) {
throw new Error("Worldview reader used before bindWorldviewStore() — call it once at startup.");
}
return _store;
}
/**
* The RichContentTable as a `Map<string, RawRichContentEntry>`.
*
* Loaded eagerly and cached. Built with `readJsonInt64Safe` because the
* `id` values in title/content are int64 hashes stored as numeric literals
* upstream (plain readJson would truncate them — the same hazard texts.ts
* guards against).
*/
export function richContentTable() {
if (_richContent !== null)
return _richContent;
const raw = store().readJsonInt64Safe("RichContentTable.json");
const map = new Map();
for (const [key, entry] of Object.entries(raw)) {
if (entry !== null && entry !== undefined) {
map.set(key, entry);
}
}
_richContent = map;
return _richContent;
}
/** PRTS categories, sorted by `order`. */
export function prtsCategories() {
if (_prtsCategories !== null)
return _prtsCategories;
const raw = store().readJsonInt64Safe("prts/PrtsCategory.json");
_prtsCategories = Object.values(raw)
.filter((c) => c !== null && c !== undefined)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return _prtsCategories;
}
/** First-level groups, sorted by `order`. */
export function firstLvGroups() {
if (_firstLvGroups !== null)
return _firstLvGroups;
const raw = store().readJsonInt64Safe("prts/PrtsFirstLv.json");
_firstLvGroups = Object.values(raw)
.filter((g) => g !== null && g !== undefined)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return _firstLvGroups;
}
/**
* All PRTS narrative items, keyed by id.
*
* Built as a Map because both document reads and search look items up by id
* repeatedly — an O(1) lookup beats a linear scan over 414 entries each time.
*/
export function allItems() {
if (_allItems !== null)
return _allItems;
const raw = store().readJsonInt64Safe("prts/PrtsAllItem.json");
const map = new Map();
for (const [id, item] of Object.entries(raw)) {
if (item !== null && item !== undefined) {
map.set(id, item);
}
}
_allItems = map;
return _allItems;
}
/** Wiki categories, sorted by `categoryPriority` (lower first). */
export function wikiCategories() {
if (_wikiCategories !== null)
return _wikiCategories;
const raw = store().readJsonInt64Safe("wiki/WikiCategoryTable.json");
_wikiCategories = Object.values(raw)
.filter((c) => c !== null && c !== undefined)
.sort((a, b) => (a.categoryPriority ?? 0) - (b.categoryPriority ?? 0));
return _wikiCategories;
}
/** Wiki groups keyed by category id. */
export function wikiGroups() {
if (_wikiGroups !== null)
return _wikiGroups;
const raw = store().readJsonInt64Safe("wiki/WikiGroupTable.json");
const map = new Map();
for (const [categoryId, bucket] of Object.entries(raw)) {
if (bucket && Array.isArray(bucket.list)) {
map.set(categoryId, bucket.list);
}
}
_wikiGroups = map;
return _wikiGroups;
}
// ---------------------------------------------------------------------------
// Content resolution — the RichContent bridge
// ---------------------------------------------------------------------------
/**
* Resolve a PRTS item's body text via its `contentId` string key.
*
* Walks the RichContent entry: resolves each `contentList` segment's int64
* hash through i18n and joins them with newlines. The entry's `title` is
* intentionally NOT included here — the item's own `name` field already
* carries the title (resolved separately by callers), so folding it in would
* duplicate it.
*
* Returns the concatenated body, or `""` when the contentId is missing/empty
* or has no resolvable segments (some items — e.g. multimedia with no
* transcript — legitimately have none).
*/
export function resolveContent(contentId) {
if (!contentId || contentId.length === 0)
return "";
const entry = richContentTable().get(contentId);
if (entry === undefined || !entry.contentList)
return "";
const parts = [];
for (const segment of entry.contentList) {
const text = _resolveText(segment.content);
if (text.length > 0)
parts.push(text);
}
return parts.join("\n");
}
//# sourceMappingURL=worldviewCore.js.map