142 lines
3.8 KiB
TypeScript
142 lines
3.8 KiB
TypeScript
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
import { Router } from 'express';
|
|
|
|
import type { AppConfig } from '../../config.js';
|
|
import { badRequest, notFound } from '../../errors.js';
|
|
import { asyncHandler } from '../../http.js';
|
|
|
|
const EDITOR_JSON_RESOURCE_FILES = {
|
|
'item-overrides': 'src/data/itemOverrides.json',
|
|
'npc-visual-overrides': 'src/data/npcVisualOverrides.json',
|
|
'npc-layout-config': 'src/data/npcLayoutConfig.json',
|
|
'character-overrides': 'src/data/characterOverrides.json',
|
|
'monster-overrides': 'src/data/monsterOverrides.json',
|
|
'scene-overrides': 'src/data/sceneOverrides.json',
|
|
'scene-npc-overrides': 'src/data/sceneNpcOverrides.json',
|
|
'state-function-overrides': 'src/data/stateFunctionOverrides.json',
|
|
} as const;
|
|
|
|
type EditorJsonResourceId = keyof typeof EDITOR_JSON_RESOURCE_FILES;
|
|
|
|
function isEditorJsonPayload(value: unknown): value is Record<string, unknown> {
|
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function resolveEditorJsonFile(
|
|
config: AppConfig,
|
|
resourceId: string,
|
|
) {
|
|
const relativePath =
|
|
EDITOR_JSON_RESOURCE_FILES[
|
|
resourceId as EditorJsonResourceId
|
|
];
|
|
if (!relativePath) {
|
|
throw notFound('未知的编辑器资源。');
|
|
}
|
|
|
|
return path.resolve(config.projectRoot, relativePath);
|
|
}
|
|
|
|
async function readEditorJsonFile(filePath: string) {
|
|
try {
|
|
const content = await readFile(filePath, 'utf8');
|
|
return JSON.parse(content) as Record<string, unknown>;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
return {};
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function collectPngAssetPaths(
|
|
rootDir: string,
|
|
relativeDir = 'Icons',
|
|
): Promise<string[]> {
|
|
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
const collected: string[] = [];
|
|
|
|
for (const entry of entries) {
|
|
const absolutePath = path.join(rootDir, entry.name);
|
|
const relativePath = `${relativeDir}/${entry.name}`.replace(/\\/g, '/');
|
|
|
|
if (entry.isDirectory()) {
|
|
collected.push(
|
|
...(await collectPngAssetPaths(absolutePath, relativePath)),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (entry.isFile() && entry.name.toLowerCase().endsWith('.png')) {
|
|
collected.push(relativePath);
|
|
}
|
|
}
|
|
|
|
return collected.sort((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
export function createEditorRoutes(config: AppConfig) {
|
|
const router = Router();
|
|
|
|
router.use((request, response, next) => {
|
|
if (
|
|
request.path !== '/api/editor' &&
|
|
!request.path.startsWith('/api/editor/')
|
|
) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
if (!config.editorApiEnabled) {
|
|
response.status(403).json({
|
|
error: {
|
|
message: '编辑器接口当前未启用。',
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
next();
|
|
});
|
|
|
|
router.get(
|
|
'/api/editor/catalog/items',
|
|
asyncHandler(async (_request, response) => {
|
|
response.json({
|
|
assetPaths: await collectPngAssetPaths(
|
|
path.resolve(config.projectRoot, 'public/Icons'),
|
|
),
|
|
});
|
|
}),
|
|
);
|
|
|
|
router.get(
|
|
'/api/editor/json/:resourceId',
|
|
asyncHandler(async (request, response) => {
|
|
const filePath = resolveEditorJsonFile(config, request.params.resourceId);
|
|
response.json(await readEditorJsonFile(filePath));
|
|
}),
|
|
);
|
|
|
|
router.post(
|
|
'/api/editor/json/:resourceId',
|
|
asyncHandler(async (request, response) => {
|
|
if (!isEditorJsonPayload(request.body)) {
|
|
throw badRequest('编辑器保存请求必须是 JSON 对象。');
|
|
}
|
|
|
|
const filePath = resolveEditorJsonFile(config, request.params.resourceId);
|
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
await writeFile(
|
|
filePath,
|
|
JSON.stringify(request.body, null, 2) + '\n',
|
|
'utf8',
|
|
);
|
|
response.json({ ok: true });
|
|
}),
|
|
);
|
|
|
|
return router;
|
|
}
|