fix: 커맨드 차등 생명주기와 로그 그래프를 보강
장수 55종과 수뇌 35종의 상태, 로그, 메시지, 예약 턴 비교를 닫습니다. 실제 시나리오 프로필과 대표 PostgreSQL 수명주기, 즉시 외교와 출병 회귀를 추가하고 발견된 Ref 로그 및 생성 장수 저장 차이를 교정합니다.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { GENERAL_TURN_COMMAND_KEYS, isGeneralTurnCommandKey, type GeneralTurnCommandKey } from './general/index.js';
|
||||
import { NATION_TURN_COMMAND_KEYS, isNationTurnCommandKey, type NationTurnCommandKey } from './nation/index.js';
|
||||
import { asStringArray } from '@sammo-ts/common';
|
||||
import { asStringArray, isRecord } from '@sammo-ts/common';
|
||||
import { TurnCommandProfileInputSchema } from '../../resources/turnCommandSchema.js';
|
||||
|
||||
export interface TurnCommandProfile {
|
||||
@@ -8,31 +8,43 @@ export interface TurnCommandProfile {
|
||||
nation: NationTurnCommandKey[];
|
||||
}
|
||||
|
||||
const asStringArrayOrNull = (value: unknown): string[] | null => {
|
||||
if (!Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const list = asStringArray(value);
|
||||
return list.length > 0 ? list : null;
|
||||
};
|
||||
export interface TurnCommandGroup<Key extends string> {
|
||||
category: string;
|
||||
commands: Key[];
|
||||
}
|
||||
|
||||
export interface ScenarioTurnCommandProfileResolution {
|
||||
profile: TurnCommandProfile;
|
||||
generalGroups: Array<TurnCommandGroup<GeneralTurnCommandKey>> | null;
|
||||
nationGroups: Array<TurnCommandGroup<NationTurnCommandKey>> | null;
|
||||
}
|
||||
|
||||
const parseKeyList = <T extends string>(options: {
|
||||
raw: unknown;
|
||||
defaults: T[];
|
||||
isKey: (value: string) => value is T;
|
||||
label: string;
|
||||
}): T[] => {
|
||||
const rawList = asStringArrayOrNull(options.raw);
|
||||
if (!rawList) {
|
||||
return options.defaults;
|
||||
if (!Array.isArray(options.raw) || options.raw.length === 0) {
|
||||
throw new Error(`${options.label} command profile must be a non-empty array.`);
|
||||
}
|
||||
const parsed: T[] = [];
|
||||
for (const value of rawList) {
|
||||
const seen = new Set<T>();
|
||||
for (const value of asStringArray(options.raw)) {
|
||||
if (!options.isKey(value)) {
|
||||
throw new Error(`Unknown ${options.label} command key: ${value}`);
|
||||
}
|
||||
if (seen.has(value)) {
|
||||
throw new Error(`Duplicate ${options.label} command key: ${value}`);
|
||||
}
|
||||
seen.add(value);
|
||||
parsed.push(value);
|
||||
}
|
||||
if (parsed.length !== options.raw.length) {
|
||||
throw new Error(`${options.label} command profile contains a non-string key.`);
|
||||
}
|
||||
if (!parsed.includes('휴식' as T)) {
|
||||
throw new Error(`${options.label} command profile must include 휴식.`);
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
@@ -41,27 +53,101 @@ export const DEFAULT_TURN_COMMAND_PROFILE: TurnCommandProfile = {
|
||||
nation: [...NATION_TURN_COMMAND_KEYS],
|
||||
};
|
||||
|
||||
export const parseTurnCommandProfile = (
|
||||
raw: unknown,
|
||||
fallback: TurnCommandProfile = DEFAULT_TURN_COMMAND_PROFILE
|
||||
): TurnCommandProfile => {
|
||||
export const parseTurnCommandProfile = (raw: unknown): TurnCommandProfile => {
|
||||
const parsed = TurnCommandProfileInputSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
return fallback;
|
||||
throw new Error(`Invalid turn command profile: ${parsed.error.message}`);
|
||||
}
|
||||
const data = parsed.data;
|
||||
return {
|
||||
general: parseKeyList({
|
||||
raw: data.general,
|
||||
defaults: fallback.general,
|
||||
isKey: isGeneralTurnCommandKey,
|
||||
label: 'general',
|
||||
}),
|
||||
nation: parseKeyList({
|
||||
raw: data.nation,
|
||||
defaults: fallback.nation,
|
||||
isKey: isNationTurnCommandKey,
|
||||
label: 'nation',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const parseScenarioCommandGroups = <Key extends string>(options: {
|
||||
raw: unknown;
|
||||
isKey: (value: string) => value is Key;
|
||||
label: string;
|
||||
}): Array<TurnCommandGroup<Key>> | null => {
|
||||
if (options.raw === undefined || options.raw === null) {
|
||||
return null;
|
||||
}
|
||||
if (!isRecord(options.raw)) {
|
||||
throw new Error(`Scenario ${options.label} command groups must be an object.`);
|
||||
}
|
||||
|
||||
const groups: Array<TurnCommandGroup<Key>> = [];
|
||||
const seen = new Set<Key>();
|
||||
for (const [category, rawCommands] of Object.entries(options.raw)) {
|
||||
if (!category.trim()) {
|
||||
throw new Error(`Scenario ${options.label} command category must be non-empty.`);
|
||||
}
|
||||
if (!Array.isArray(rawCommands)) {
|
||||
throw new Error(`Scenario ${options.label} command category ${category} must be an array.`);
|
||||
}
|
||||
const commands: Key[] = [];
|
||||
for (const value of asStringArray(rawCommands)) {
|
||||
if (!options.isKey(value)) {
|
||||
throw new Error(`Unknown scenario ${options.label} command key: ${value}`);
|
||||
}
|
||||
if (seen.has(value)) {
|
||||
throw new Error(`Duplicate scenario ${options.label} command key: ${value}`);
|
||||
}
|
||||
seen.add(value);
|
||||
commands.push(value);
|
||||
}
|
||||
if (commands.length !== rawCommands.length) {
|
||||
throw new Error(`Scenario ${options.label} command category ${category} contains a non-string key.`);
|
||||
}
|
||||
groups.push({ category, commands });
|
||||
}
|
||||
|
||||
const flattened = groups.flatMap((group) => group.commands);
|
||||
if (!flattened.includes('휴식' as Key)) {
|
||||
throw new Error(`Scenario ${options.label} command groups must include 휴식.`);
|
||||
}
|
||||
return groups;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ref replaces GameConst::$availableGeneralCommand/$availableChiefCommand with
|
||||
* the scenario const when present. Resolve that same public/executable profile
|
||||
* here so the API and daemon cannot silently use the default profile instead.
|
||||
*/
|
||||
export const resolveScenarioTurnCommandProfile = (
|
||||
scenarioConst: unknown,
|
||||
fallback: TurnCommandProfile
|
||||
): ScenarioTurnCommandProfileResolution => {
|
||||
if (scenarioConst !== undefined && scenarioConst !== null && !isRecord(scenarioConst)) {
|
||||
throw new Error('Scenario const must be an object.');
|
||||
}
|
||||
const config = isRecord(scenarioConst) ? scenarioConst : {};
|
||||
const generalGroups = parseScenarioCommandGroups({
|
||||
raw: config.availableGeneralCommand,
|
||||
isKey: isGeneralTurnCommandKey,
|
||||
label: 'general',
|
||||
});
|
||||
const nationGroups = parseScenarioCommandGroups({
|
||||
raw: config.availableChiefCommand,
|
||||
isKey: isNationTurnCommandKey,
|
||||
label: 'nation',
|
||||
});
|
||||
|
||||
return {
|
||||
profile: {
|
||||
general: generalGroups ? generalGroups.flatMap((group) => group.commands) : [...fallback.general],
|
||||
nation: nationGroups ? nationGroups.flatMap((group) => group.commands) : [...fallback.nation],
|
||||
},
|
||||
generalGroups,
|
||||
nationGroups,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user