278 lines
10 KiB
TypeScript
278 lines
10 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { readdirSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { PRESET_CHARACTERS } from '../src/data/characterPresets.ts';
|
|
import { MONSTER_PRESETS_BY_WORLD } from '../src/data/hostileNpcPresets.ts';
|
|
import { buildItemCatalogId } from '../src/data/itemCatalog.ts';
|
|
import { getScenePresetsByWorld } from '../src/data/scenePresets.ts';
|
|
import { buildStateFunctionDefinitions } from '../src/data/stateFunctions.ts';
|
|
import { WorldType } from '../src/types.ts';
|
|
|
|
function readJsonFile<T>(relativePath: string): T {
|
|
const absolutePath = path.resolve(process.cwd(), relativePath);
|
|
return JSON.parse(readFileSync(absolutePath, 'utf8')) as T;
|
|
}
|
|
|
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function isKnownGender(value: unknown): value is 'male' | 'female' {
|
|
return value === 'male' || value === 'female';
|
|
}
|
|
|
|
function expectPlainObject(errors: string[], label: string, value: unknown) {
|
|
if (!isPlainObject(value)) {
|
|
errors.push(`[override] ${label} must be an object map`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function validateCharacterOverrides(errors: string[]) {
|
|
const overrides = readJsonFile<Record<string, unknown>>('src/data/characterOverrides.json');
|
|
if (!expectPlainObject(errors, 'characterOverrides', overrides)) return;
|
|
|
|
const characterIds = new Set(PRESET_CHARACTERS.map(character => character.id));
|
|
const sceneIds = new Set(
|
|
[WorldType.WUXIA, WorldType.XIANXIA].flatMap(worldType => getScenePresetsByWorld(worldType).map(scene => scene.id)),
|
|
);
|
|
|
|
Object.entries(overrides).forEach(([characterId, override]) => {
|
|
if (!characterIds.has(characterId)) {
|
|
errors.push(`[override] characterOverrides contains unknown character id "${characterId}"`);
|
|
return;
|
|
}
|
|
|
|
if (!isPlainObject(override)) {
|
|
errors.push(`[override] characterOverrides["${characterId}"] must be an object`);
|
|
return;
|
|
}
|
|
|
|
const gender = override.gender;
|
|
if (gender !== undefined && !isKnownGender(gender)) {
|
|
errors.push(`[override] characterOverrides["${characterId}"].gender must be "male" or "female"`);
|
|
}
|
|
|
|
const sceneBindings = override.sceneBindings;
|
|
if (sceneBindings !== undefined) {
|
|
if (!isPlainObject(sceneBindings)) {
|
|
errors.push(`[override] characterOverrides["${characterId}"].sceneBindings must be an object`);
|
|
} else {
|
|
Object.entries(sceneBindings).forEach(([worldKey, binding]) => {
|
|
if (!isPlainObject(binding)) {
|
|
errors.push(`[override] characterOverrides["${characterId}"].sceneBindings["${worldKey}"] must be an object`);
|
|
return;
|
|
}
|
|
|
|
const homeSceneId = binding.homeSceneId;
|
|
if (homeSceneId !== undefined && (typeof homeSceneId !== 'string' || !sceneIds.has(homeSceneId))) {
|
|
errors.push(`[override] characterOverrides["${characterId}"] has invalid homeSceneId "${String(homeSceneId)}"`);
|
|
}
|
|
|
|
const npcSceneIds = binding.npcSceneIds;
|
|
if (npcSceneIds !== undefined) {
|
|
if (!Array.isArray(npcSceneIds) || npcSceneIds.some(sceneId => typeof sceneId !== 'string' || !sceneIds.has(sceneId))) {
|
|
errors.push(`[override] characterOverrides["${characterId}"] has invalid npcSceneIds`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function validateMonsterOverrides(errors: string[]) {
|
|
const overrides = readJsonFile<Record<string, unknown>>('src/data/monsterOverrides.json');
|
|
if (!expectPlainObject(errors, 'monsterOverrides', overrides)) return;
|
|
|
|
const hostilePresetIds = new Set(
|
|
[...MONSTER_PRESETS_BY_WORLD[WorldType.WUXIA], ...MONSTER_PRESETS_BY_WORLD[WorldType.XIANXIA]].map(monster => monster.id),
|
|
);
|
|
|
|
Object.entries(overrides).forEach(([monsterId, override]) => {
|
|
if (!hostilePresetIds.has(monsterId)) {
|
|
errors.push(`[override] monsterOverrides contains unknown monster id "${monsterId}"`);
|
|
return;
|
|
}
|
|
if (!isPlainObject(override)) {
|
|
errors.push(`[override] monsterOverrides["${monsterId}"] must be an object`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function validateSceneOverrides(errors: string[]) {
|
|
const overrides = readJsonFile<Record<string, unknown>>('src/data/sceneOverrides.json');
|
|
if (!expectPlainObject(errors, 'sceneOverrides', overrides)) return;
|
|
|
|
const sceneIds = new Set(
|
|
[WorldType.WUXIA, WorldType.XIANXIA].flatMap(worldType => getScenePresetsByWorld(worldType).map(scene => scene.id)),
|
|
);
|
|
Object.entries(overrides).forEach(([sceneId, override]) => {
|
|
if (!sceneIds.has(sceneId)) {
|
|
errors.push(`[override] sceneOverrides contains unknown scene id "${sceneId}"`);
|
|
return;
|
|
}
|
|
if (!isPlainObject(override)) {
|
|
errors.push(`[override] sceneOverrides["${sceneId}"] must be an object`);
|
|
return;
|
|
}
|
|
|
|
const forwardSceneId = override.forwardSceneId;
|
|
if (forwardSceneId !== undefined && (typeof forwardSceneId !== 'string' || !sceneIds.has(forwardSceneId))) {
|
|
errors.push(`[override] sceneOverrides["${sceneId}"] has invalid forwardSceneId "${String(forwardSceneId)}"`);
|
|
}
|
|
|
|
const connectedSceneIds = override.connectedSceneIds;
|
|
if (connectedSceneIds !== undefined) {
|
|
if (!Array.isArray(connectedSceneIds) || connectedSceneIds.some(id => typeof id !== 'string' || !sceneIds.has(id))) {
|
|
errors.push(`[override] sceneOverrides["${sceneId}"] has invalid connectedSceneIds`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function validateSceneNpcOverrides(errors: string[]) {
|
|
const overrides = readJsonFile<Record<string, unknown>>('src/data/sceneNpcOverrides.json');
|
|
if (!expectPlainObject(errors, 'sceneNpcOverrides', overrides)) return;
|
|
|
|
const npcIds = new Set(
|
|
[WorldType.WUXIA, WorldType.XIANXIA].flatMap(worldType =>
|
|
getScenePresetsByWorld(worldType).flatMap(scene => scene.npcs.map(npc => npc.id)),
|
|
),
|
|
);
|
|
const characterIds = new Set(PRESET_CHARACTERS.map(character => character.id));
|
|
|
|
Object.entries(overrides).forEach(([npcId, override]) => {
|
|
if (!npcIds.has(npcId)) {
|
|
errors.push(`[override] sceneNpcOverrides contains unknown npc id "${npcId}"`);
|
|
return;
|
|
}
|
|
if (!isPlainObject(override)) {
|
|
errors.push(`[override] sceneNpcOverrides["${npcId}"] must be an object`);
|
|
return;
|
|
}
|
|
|
|
const gender = override.gender;
|
|
if (gender !== undefined && !isKnownGender(gender)) {
|
|
errors.push(`[override] sceneNpcOverrides["${npcId}"].gender must be "male" or "female"`);
|
|
}
|
|
|
|
const characterId = override.characterId;
|
|
if (characterId !== undefined && (typeof characterId !== 'string' || !characterIds.has(characterId))) {
|
|
errors.push(`[override] sceneNpcOverrides["${npcId}"] has invalid characterId "${String(characterId)}"`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function validateStateFunctionOverrides(errors: string[]) {
|
|
const overrides = readJsonFile<Record<string, unknown>>('src/data/stateFunctionOverrides.json');
|
|
if (!expectPlainObject(errors, 'stateFunctionOverrides', overrides)) return;
|
|
|
|
const functionIds = new Set(buildStateFunctionDefinitions().map(definition => definition.id));
|
|
Object.entries(overrides).forEach(([functionId, override]) => {
|
|
if (!functionIds.has(functionId)) {
|
|
errors.push(`[override] stateFunctionOverrides contains unknown function id "${functionId}"`);
|
|
return;
|
|
}
|
|
if (!isPlainObject(override)) {
|
|
errors.push(`[override] stateFunctionOverrides["${functionId}"] must be an object`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function validateNpcVisualOverrides(errors: string[]) {
|
|
const overrides = readJsonFile<Record<string, unknown>>('src/data/npcVisualOverrides.json');
|
|
if (!expectPlainObject(errors, 'npcVisualOverrides', overrides)) return;
|
|
|
|
const npcIds = new Set(
|
|
[WorldType.WUXIA, WorldType.XIANXIA].flatMap(worldType =>
|
|
getScenePresetsByWorld(worldType).flatMap(scene => scene.npcs.map(npc => npc.id)),
|
|
),
|
|
);
|
|
|
|
Object.entries(overrides).forEach(([npcId, override]) => {
|
|
if (!npcIds.has(npcId)) {
|
|
errors.push(`[override] npcVisualOverrides contains unknown npc id "${npcId}"`);
|
|
return;
|
|
}
|
|
if (!isPlainObject(override)) {
|
|
errors.push(`[override] npcVisualOverrides["${npcId}"] must be an object`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function collectItemAssetPaths(rootDir: string, relativeDir = 'Icons'): string[] {
|
|
const entries = readdirSync(rootDir, { withFileTypes: true });
|
|
const collected: string[] = [];
|
|
|
|
entries.forEach(entry => {
|
|
const absolutePath = path.join(rootDir, entry.name);
|
|
const relativePath = `${relativeDir}/${entry.name}`.replace(/\\/g, '/');
|
|
|
|
if (entry.isDirectory()) {
|
|
collected.push(...collectItemAssetPaths(absolutePath, relativePath));
|
|
return;
|
|
}
|
|
|
|
if (entry.isFile() && entry.name.toLowerCase().endsWith('.png')) {
|
|
collected.push(relativePath);
|
|
}
|
|
});
|
|
|
|
return collected;
|
|
}
|
|
|
|
function validateItemOverrides(errors: string[]) {
|
|
const overrides = readJsonFile<Record<string, unknown>>('src/data/itemOverrides.json');
|
|
if (!expectPlainObject(errors, 'itemOverrides', overrides)) return;
|
|
|
|
const validItemIds = new Set(
|
|
collectItemAssetPaths(path.resolve(process.cwd(), 'public/Icons'))
|
|
.map(assetPath => buildItemCatalogId(assetPath)),
|
|
);
|
|
|
|
Object.entries(overrides).forEach(([itemId, override]) => {
|
|
if (!validItemIds.has(itemId)) {
|
|
errors.push(`[override] itemOverrides contains unknown item id "${itemId}"`);
|
|
return;
|
|
}
|
|
|
|
if (!isPlainObject(override)) {
|
|
errors.push(`[override] itemOverrides["${itemId}"] must be an object`);
|
|
return;
|
|
}
|
|
|
|
const tags = override.tags;
|
|
if (tags !== undefined) {
|
|
if (!Array.isArray(tags) || tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
|
errors.push(`[override] itemOverrides["${itemId}"] has invalid tags`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function main() {
|
|
const errors: string[] = [];
|
|
|
|
validateCharacterOverrides(errors);
|
|
validateMonsterOverrides(errors);
|
|
validateSceneOverrides(errors);
|
|
validateSceneNpcOverrides(errors);
|
|
validateStateFunctionOverrides(errors);
|
|
validateNpcVisualOverrides(errors);
|
|
validateItemOverrides(errors);
|
|
|
|
if (errors.length > 0) {
|
|
console.error(`Override validation failed with ${errors.length} issue(s):`);
|
|
errors.forEach(error => console.error(`- ${error}`));
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
console.log('Override validation passed.');
|
|
}
|
|
|
|
main();
|