UNPKG

endfield-mcp

Version:

MCP Server for Arknights: Endfield (TypeScript / stdio + Streamable HTTP)

253 lines • 9.11 kB
/** * Dataset specifications for Endfield-MCP. * * Each dataset describes one GitHub Release asset from the self-hosted * EndFieldGameData mirror. The sync layer (`./sync.ts`) consumes these * specs without knowing anything about Endfield's data shape. * * ## Mirror layout * * The mirror publishes one Release asset: `endfield-tables.zip`. Its * internal structure (mirrored from endfield_research_kit's * `export_full/structured/StreamingAssets/Table/`, with a cleaner top-level * split between game tables and localization): * * ``` * endfield-tables.zip * ├── tables/ # 10 core game tables * │ ├── CharacterTable.json * │ ├── EnemyTable.json * │ ├── EnemyTemplateTable.json * │ ├── EnemyDisplayInfoTable.json * │ ├── EquipTable.json * │ ├── EquipItemTable.json * │ ├── CharProfessionTable.json * │ ├── CharTypeTable.json * │ ├── CharacterTagTable.json * │ └── ItemTable.json * └── i18n/ # 5 localization tables * ├── CN.json (← I18nTextTable_CN.json, renamed for brevity) * ├── EN.json * ├── JP.json * ├── TC.json * └── KR.json * ``` * * ## Language coverage * * Five languages chosen for the fan-creation communities they serve: * CN (Simplified Chinese, project default), EN, JP, TC (Traditional * Chinese), KR. Each is ~9-12MB; combined with the 10 core tables the * asset is ~64MB uncompressed (~15-20MB zipped). * * ## Schema notes * * Endfield's data architecture separates values from localization: most * tables store `{id, text}` objects where `text` is empty and `id` is an * int64 hash. The actual string lives in the i18n table under that hash. * The `data/texts.ts` module owns that lookup; readers stay ignorant of * which language is active. */ import AdmZip from "adm-zip"; // --------------------------------------------------------------------------- // Mirror repository coordinates // --------------------------------------------------------------------------- /** * GitHub coordinates of the self-hosted Endfield data mirror. * * The mirror is live (3aKHP/EndFieldGameData) with two published Releases: * v0.2.0 (endfield-tables.zip) and v0.3.0 (endfield-story-CN.zip). These * coordinates feed the sync layer's download URL construction; changing * them here re-points all dataset fetches. */ export const MIRROR_OWNER = "3aKHP"; export const MIRROR_REPO = "EndFieldGameData"; // --------------------------------------------------------------------------- // Required-file lists // --------------------------------------------------------------------------- /** * The 10 core game tables every release must ship. * * Pinned from the first endfield_research_kit export (2026-06). These cover * the v0.2 character-focused scope; items/enemies/equips ship too so later * minor versions can add domains without a mirror re-release. */ export const TABLE_FILES = [ "tables/CharacterTable.json", "tables/EnemyTable.json", "tables/EnemyTemplateTable.json", "tables/EnemyDisplayInfoTable.json", "tables/EquipTable.json", "tables/EquipItemTable.json", "tables/CharProfessionTable.json", "tables/CharTypeTable.json", "tables/CharacterTagTable.json", "tables/ItemTable.json", ]; /** * The five localization tables every release must ship. * * File names are shortened from the upstream `I18nTextTable_<LANG>.json` * convention to keep zip paths tidy. Language codes match the upstream * suffix: CN, EN, JP, TC, KR. */ export const I18N_FILES = [ "i18n/CN.json", "i18n/EN.json", "i18n/JP.json", "i18n/TC.json", "i18n/KR.json", ]; /** All files the release asset must contain (tables + localization). */ export const ALL_REQUIRED_FILES = [ ...TABLE_FILES, ...I18N_FILES, ]; /** The five languages the mirror ships, in canonical order. */ export const SUPPORTED_LANGUAGES = [ "CN", "EN", "JP", "TC", "KR", ]; /** Default language when no preference is given. */ export const DEFAULT_LANGUAGE = "CN"; /** Build a {@link ReleaseSpec} for plain-zip download (no extraction). */ export function releaseSpecForDataset(dataset, localZip) { return { owner: dataset.owner, repo: dataset.repo, assetName: dataset.assetName, localZip, validateZip: (zipPath) => validateDatasetZip(dataset, zipPath), }; } /** Build a {@link ReleaseArchiveSpec} for download-and-extract. */ export function archiveSpecForDataset(dataset, localZip, localRoot) { return { owner: dataset.owner, repo: dataset.repo, assetName: dataset.assetName, localZip, localRoot, requiredFiles: dataset.requiredFiles, }; } // --------------------------------------------------------------------------- // Dataset definitions // --------------------------------------------------------------------------- /** * GameData tables — the core v0.2.0 dataset. * * Bundles the 10 core game tables and 5 localization tables into one * ~15-20MB zip asset. One Release per upstream game update; tag is bare * semver (v0.2.0, v0.2.1, ...). */ export const GAMEDATA_TABLES = { datasetId: "gamedata.tables", owner: MIRROR_OWNER, repo: MIRROR_REPO, assetName: "endfield-tables.zip", requiredFiles: ALL_REQUIRED_FILES, }; /** * Story dialogue bundle (CN) — the v0.3.0 dataset. * * Ships the catalog (index/missions/actors/search) and all 9271 conv * files. ~19MB zipped. requiredFiles only lists the catalog — conv/ * entries are read on-demand by story reader (per-scene fetch), not * validated at startup (9271 files would make the check expensive). */ export const STORY_REQUIRED_FILES = [ "index.json", "missions.json", "actors.json", "search.json", ]; export const STORY_CN = { datasetId: "story.cn", owner: MIRROR_OWNER, repo: MIRROR_REPO, assetName: "endfield-story-CN.zip", requiredFiles: STORY_REQUIRED_FILES, }; /** * Worldview bundle — the v0.4.0 dataset. * * In-game PRTS archive system (documents/records/multimedia/investigations) * plus the in-game encyclopedia (wiki entries/categories/groups). Schema * differs from the tables/story bundles in one way: most Prts entries carry * a `contentId` that is a **string key** (e.g. "text_v0d8_10"), not a direct * int64 hash. That key resolves through the root-level RichContentTable.json: * `RichContentTable[contentId]` = `{title: {id, text}, contentList: [{content: {id, text}}, ...]}`, * and each segment's `content.id` is an int64 hash looked up in i18n/CN.json * (shipped in the v0.2.0 tables bundle). The i18n table is intentionally NOT * re-bundled here. * * Layout (see local_dev/docs/worldview-mirror-build.md for the build spec): * * ``` * endfield-worldview.zip * ├── prts/ (11 narrative tables) * ├── wiki/ (4 encyclopedia tables — narrative subset only) * └── RichContentTable.json (contentId → {title, contentList[]} bridge, root-level) * ``` * * ~2.7MB raw. The 6 mechanistic Wiki tables (craft/drop/tutorial) are * deliberately excluded — v0.4 is lore-first (ROADMAP decision principle). */ export const WORLDVIEW_REQUIRED_FILES = [ // PRTS narrative hierarchy "prts/PrtsCategory.json", "prts/PrtsPage.json", "prts/PrtsFirstLv.json", "prts/PrtsAllItem.json", "prts/PrtsDocument.json", "prts/PrtsRecord.json", "prts/PrtsMultimedia.json", "prts/PrtsNote.json", "prts/PrtsReading.json", "prts/PrtsInvestigate.json", "prts/PrtsInvestigateCategory.json", // In-game encyclopedia (narrative subset) "wiki/WikiCategoryTable.json", "wiki/WikiGroupTable.json", "wiki/WikiEntryTable.json", "wiki/WikiEntryDataTable.json", // contentId → {title, contentList[]} rich-content bridge (shared resolver, root-level) "RichContentTable.json", ]; export const WORLDVIEW = { datasetId: "worldview", owner: MIRROR_OWNER, repo: MIRROR_REPO, assetName: "endfield-worldview.zip", requiredFiles: WORLDVIEW_REQUIRED_FILES, }; /** * Validate a downloaded dataset zip against its required-file list. * * Returns the list of missing required entries (empty = valid). Errors * opening the zip are reported as a single descriptive entry so callers * can surface them without distinguishing "corrupt" from "incomplete". */ export function validateDatasetZip(dataset, zipPath) { if (dataset.requiredFiles.length === 0) return []; let zip; try { zip = new AdmZip(zipPath); } catch (err) { return [ `${dataset.assetName} is not a valid zip: ${err instanceof Error ? err.message : String(err)}`, ]; } const entries = new Set(zip .getEntries() .filter((e) => !e.isDirectory) .map((e) => e.entryName)); return dataset.requiredFiles.filter((file) => !entries.has(file)); } //# sourceMappingURL=datasets.js.map