import fs from 'node:fs/promises'; import path from 'node:path'; import { parseScenarioDefaults, parseScenarioDefinition, type ScenarioDefaults, type ScenarioDefinition, } from '@sammo-ts/logic'; import { resolveWorkspaceRoot } from '../paths.js'; import { composeScenarioResource } from './scenarioComposition.js'; const REPO_ROOT = resolveWorkspaceRoot(); const DEFAULT_SCENARIO_ROOT = path.resolve(REPO_ROOT, 'resources', 'scenario'); export interface ScenarioLoaderOptions { scenarioRoot?: string; defaultsFileName?: string; } const readJsonFile = async (filePath: string): Promise => { const raw = await fs.readFile(filePath, 'utf8'); return JSON.parse(raw) as unknown; }; const resolveScenarioRoot = (options?: ScenarioLoaderOptions): string => options?.scenarioRoot ?? DEFAULT_SCENARIO_ROOT; export const resolveScenarioDefaultsPath = (options?: ScenarioLoaderOptions): string => path.resolve(resolveScenarioRoot(options), options?.defaultsFileName ?? 'default.json'); const resolveScenarioPath = (options: ScenarioLoaderOptions | undefined, scenarioId: number): string => path.resolve(resolveScenarioRoot(options), `scenario_${scenarioId}.json`); const loadScenarioDefaults = async (defaultsPath: string): Promise => { // 기본 시나리오 파일을 읽고 정규화한다. const raw = await readJsonFile(defaultsPath); return parseScenarioDefaults(raw); }; const loadScenarioDefinition = async ( scenarioPath: string, defaults: ScenarioDefaults ): Promise => { // 시나리오 확장 조각을 먼저 합성한 뒤 기본값과 함께 정규화한다. const scenarioRoot = path.dirname(scenarioPath); const raw = await composeScenarioResource(path.basename(scenarioPath), async (relativePath) => { const resolvedPath = path.resolve(scenarioRoot, relativePath); const rootPrefix = `${path.resolve(scenarioRoot)}${path.sep}`; if (!resolvedPath.startsWith(rootPrefix)) { throw new Error(`Scenario resource path escapes the configured root: ${relativePath}.`); } return readJsonFile(resolvedPath); }); return parseScenarioDefinition(raw, defaults); }; export const loadScenarioDefinitionById = async ( scenarioId: number, options?: ScenarioLoaderOptions ): Promise => { // 시나리오 번호로 파일을 찾고 파싱한다. const defaultsPath = resolveScenarioDefaultsPath(options); const scenarioPath = resolveScenarioPath(options, scenarioId); const defaults = await loadScenarioDefaults(defaultsPath); return loadScenarioDefinition(scenarioPath, defaults); };