diff --git a/extensions/pixelhero-config-editor/src/main/store.ts b/extensions/pixelhero-config-editor/src/main/store.ts new file mode 100644 index 00000000..2d0fdd63 --- /dev/null +++ b/extensions/pixelhero-config-editor/src/main/store.ts @@ -0,0 +1,85 @@ +import { join } from 'node:path'; +import { TsConfigFile } from '../io/TsConfigFile'; +import { allSchemas } from '../shared/schema/registry'; +import { ENUMS } from '../shared/schema/enums'; +import { validate, ValidationContext } from '../shared/validation'; +import { buildSkillDesc } from '../shared/desc/buildSkillDesc'; +import { RecordValue } from '../io/recordValue'; +import { TableId } from '../shared/schema/types'; + +/** 游戏配置目录(相对项目根)。主进程用 Editor.Project.path 解析到项目根的 assets 配置目录。 */ +const CONFIG_DIR = join(Editor.Project.path, 'assets/script/game/common/config'); + +interface Entry { file: TsConfigFile; } +const tables: Partial> = {}; +// skill 与 field 共享 SkillSet.ts,但用不同 exportName 的 TsConfigFile 实例 +const fileCache: Record = {}; + +function getTable(id: TableId): Entry { + if (tables[id]) return tables[id]!; + const schema = allSchemas().find(s => s.id === id)!; + const cacheKey = `${schema.sourceFile}:${schema.exportName}`; + let file = fileCache[cacheKey]; + if (!file) { file = new TsConfigFile(join(CONFIG_DIR, schema.sourceFile), schema.exportName); file.load(); fileCache[cacheKey] = file; } + const entry = { file }; + tables[id] = entry; + return entry; +} + +function recordsOf(id: TableId): Map { + const { file } = getTable(id); + const m = new Map(); + for (const k of file.getKeys()) { const v = file.read(k); if (v) m.set(k, v); } + return m; +} + +function buildContext(): ValidationContext { + const hero = recordsOf('hero'); + const skill = recordsOf('skill'); + const field = recordsOf('field'); + const heroKeys = Array.from(hero.keys()).map(Number); + const skillKeys = Array.from(skill.keys()).map(Number); + const fieldKeys = Array.from(field.keys()).map(Number); + // HeroList 读取(hero 表的 listExportName) + const heroList = new TsConfigFile(join(CONFIG_DIR, 'heroSet.ts'), 'HeroList'); + heroList.load(); + const listRaw = heroList.getText(); + const heroListKeys = new Set( + (listRaw.match(/HeroList[\s\S]*?=\s*\[([^\]]*)\]/)?.[1].match(/-?\d+/g) ?? []).map(String) + ); + return { + hasHero: (u) => heroKeys.includes(u), + hasSkill: (u) => skillKeys.includes(u), + hasField: (u) => fieldKeys.includes(u), + heroListKeys, + }; +} + +export const store = { + queryEnums: () => ENUMS, + querySchema: (id?: TableId) => id ? allSchemas().find(s => s.id === id) : allSchemas(), + queryKeys: (id: TableId) => getTable(id).file.getKeys(), + queryRecord: (id: TableId, key: string) => getTable(id).file.read(key), + queryPreviewDesc: (hero: RecordValue) => { + const skill = recordsOf('skill'); const field = recordsOf('field'); + const skillSet: Record = {}; + for (const [k, v] of skill) skillSet[Number(k)] = v; + const fieldSet: Record = {}; + for (const [k, v] of field) fieldSet[Number(k)] = v; + return buildSkillDesc(hero, skillSet, fieldSet); + }, + validate: (id: TableId) => validate(id, recordsOf(id), buildContext()), + saveRecord: (id: TableId, key: string, value: RecordValue) => { + // 先在副本上试写并校验 + const { file } = getTable(id); + file.patch(key, value); + const issues = validate(id, recordsOf(id), buildContext()).filter(i => i.key === key && i.severity === 'error'); + if (issues.length) { file.load(); return { ok: false as const, issues }; } // 回滚内存 + const r = file.save(); + if (!r.ok) { file.load(); return { ok: false as const, issues: [] }; } + void Editor.Message.request('asset-db', 'refresh-asset', `db://assets/script/game/common/config/${allSchemas().find(s => s.id === id)!.sourceFile}`); + return { ok: true as const, issues: [] }; + }, + revertRecord: (id: TableId, key: string) => { getTable(id).file.load(); return getTable(id).file.read(key); }, + reloadAll: () => { for (const k of Object.keys(fileCache)) fileCache[k].load(); }, +};