feat(config-editor): main-process store (in-memory truth + message impls + asset-db refresh)
Task 11 of plan 2026-06-20-config-editor-foundation. Holds three TsConfigFile instances (hero/skill/field), implements query*/validate/saveRecord/revertRecord message handlers. saveRecord validates before persisting, rolls back on error, and refreshes asset-db on success. HeroList read via regex.
This commit is contained in:
85
extensions/pixelhero-config-editor/src/main/store.ts
Normal file
85
extensions/pixelhero-config-editor/src/main/store.ts
Normal file
@@ -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<Record<TableId, Entry>> = {};
|
||||||
|
// skill 与 field 共享 SkillSet.ts,但用不同 exportName 的 TsConfigFile 实例
|
||||||
|
const fileCache: Record<string, TsConfigFile> = {};
|
||||||
|
|
||||||
|
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<string, RecordValue> {
|
||||||
|
const { file } = getTable(id);
|
||||||
|
const m = new Map<string, RecordValue>();
|
||||||
|
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<string>(
|
||||||
|
(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<number, RecordValue> = {};
|
||||||
|
for (const [k, v] of skill) skillSet[Number(k)] = v;
|
||||||
|
const fieldSet: Record<number, RecordValue> = {};
|
||||||
|
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(); },
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user