endfield-mcp
Version:
MCP Server for Arknights: Endfield (TypeScript / stdio + Streamable HTTP)
161 lines • 6.12 kB
JavaScript
/**
* Worldview document reading — single PRTS item and wiki entry reads.
*
* The "read" half of the worldview domain, mirroring `storyScenes.ts`'s role:
* turn a content id into a fully-resolved, human-readable document. This is
* where the TextTable bridge gets exercised — `resolveContent()` walks
* contentId → TextTable → i18n to produce the body text.
*
* Depends on `./worldviewCore.js` for catalog loaders and content resolution.
*/
import { resolveText } from "./texts.js";
import { allItems, firstLvGroups, resolveContent, store, wikiGroups, } from "./worldviewCore.js";
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/** Build a firstLvId → categoryId index lazily (private to this module). */
let _firstLvCategory = null;
function firstLvCategoryIndex() {
if (_firstLvCategory !== null)
return _firstLvCategory;
const map = new Map();
for (const g of firstLvGroups()) {
map.set(g.firstLvId, g.categoryId);
}
_firstLvCategory = map;
return map;
}
/** Reset the private caches. Called by the barrel orchestrator on sync. */
export function clearDocumentCaches() {
_firstLvCategory = null;
_wikiEntryData = null;
}
function toView(item) {
return {
itemId: item.id,
title: resolveText(item.name, undefined, item.id),
body: resolveContent(item.contentId),
type: item.type,
firstLvId: item.firstLvId,
categoryId: firstLvCategoryIndex().get(item.firstLvId) ?? "",
order: item.order,
// id === 0 is upstream's "no description" marker — treat as empty.
description: isEmptyLoc(item.desc) ? "" : resolveText(item.desc),
};
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Read a single PRTS narrative item by id.
*
* Returns `null` when the id is unknown. The body text is resolved through
* the TextTable bridge (contentId → hash → i18n).
*/
export function readLoreDocument(itemId) {
const item = allItems().get(itemId);
if (item === undefined)
return null;
return toView(item);
}
/**
* List the narrative items belonging to a first-level group.
*
* Items are returned in their upstream `order`. Useful for browsing a
* multi-document group in sequence.
*/
export function listItemsByGroup(firstLvId) {
const lower = firstLvId.toLowerCase();
const group = firstLvGroups().find((g) => g.firstLvId.toLowerCase() === lower);
if (!group)
return [];
const items = group.itemIds
.map((id) => allItems().get(id))
.filter((item) => item !== undefined)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return items.map(toView);
}
/**
* Read a wiki entry by id, with its group name resolved.
*
* Returns `null` when the id is unknown. The `prtsId` cross-link is surfaced
* verbatim so the caller can feed it to `ef_read_lore_document` for the
* associated lore text.
*/
export function readWikiEntry(entryId) {
const raw = readWikiEntryData();
const entry = raw.get(entryId);
if (entry === undefined)
return null;
// Resolve group name by scanning the category→group tree once.
let groupName = entry.groupId;
for (const [, groups] of wikiGroups()) {
const found = groups.find((g) => g.groupId === entry.groupId);
if (found) {
groupName = resolveText(found.groupName, undefined, found.groupId);
break;
}
}
return {
entryId: entry.id,
groupId: entry.groupId,
groupName,
prtsId: entry.prtsId && entry.prtsId.length > 0 ? entry.prtsId : null,
order: entry.order,
// id === 0 is upstream's "no description" marker — treat as empty rather
// than letting resolveText return the literal "0" as a fallback.
description: isEmptyLoc(entry.desc) ? "" : resolveText(entry.desc),
refItemId: entry.refItemId && entry.refItemId.length > 0 ? entry.refItemId : null,
refMonsterTemplateId: entry.refMonsterTemplateId && entry.refMonsterTemplateId.length > 0
? entry.refMonsterTemplateId
: null,
};
}
/**
* True when a LocalizedText is the upstream "absent" marker ({id:0, text:""}).
*
* `id` is coerced via `String()` because readJsonInt64Safe leaves small ids
* (≤2^53) as bare JSON numbers, so `desc.id` can arrive as the number `0`
* rather than the string `"0"`. A strict `=== "0"` would miss the numeric
* form — the same int64/coercion trap STYLE.md's "已知陷阱" warns about.
*/
function isEmptyLoc(loc) {
if (!loc)
return true;
return loc.text === "" && (String(loc.id) === "0" || loc.id === "");
}
/**
* Reverse-lookup: which wiki entries point at a given PRTS document?
*
* Used by `ef_read_lore_document` to surface "this document is referenced by
* these wiki entries". Returns entry ids only (the caller can expand them
* via `readWikiEntry` if needed).
*/
export function wikiEntriesForPrts(prtsId) {
const raw = readWikiEntryData();
const out = [];
for (const [entryId, entry] of raw) {
if (entry.prtsId && entry.prtsId === prtsId) {
out.push(entryId);
}
}
return out;
}
// ---------------------------------------------------------------------------
// On-demand table reads (cached privately)
// ---------------------------------------------------------------------------
let _wikiEntryData = null;
function readWikiEntryData() {
if (_wikiEntryData !== null)
return _wikiEntryData;
const raw = store().readJsonInt64Safe("wiki/WikiEntryDataTable.json");
const map = new Map();
for (const [id, entry] of Object.entries(raw)) {
if (entry !== null && entry !== undefined) {
map.set(id, entry);
}
}
_wikiEntryData = map;
return map;
}
//# sourceMappingURL=worldviewDocuments.js.map