import type { MapDefinition, SpawnPoint } from './types'; export class MapValidationError extends Error { constructor(message: string) { super(message); this.name = 'MapValidationError'; } } function object(value: unknown, field: string): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new MapValidationError(`${field} 必须是对象`); return value as Record; } function text(value: unknown, field: string): string { if (typeof value !== 'string' || !value.trim()) throw new MapValidationError(`${field} 必须是非空字符串`); return value.trim(); } function identifier(value: unknown, field: string): string { const result = text(value, field); if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(result)) throw new MapValidationError(`${field} 只能包含英文字母、数字、下划线和连字符`); return result; } function optionalBoolean(value: unknown, field: string, fallback: boolean): boolean { if (value === undefined) return fallback; if (typeof value !== 'boolean') throw new MapValidationError(`${field} 必须是布尔值`); return value; } function finite(value: unknown, field: string): number { if (typeof value !== 'number' || !Number.isFinite(value)) throw new MapValidationError(`${field} 必须是有限数字`); return value; } function vector3(value: unknown, field: string): [number, number, number] { if (!Array.isArray(value) || value.length !== 3) throw new MapValidationError(`${field} 必须包含 3 个数字`); return [ finite(value[0], `${field}[0]`), finite(value[1], `${field}[1]`), finite(value[2], `${field}[2]`), ]; } function spawnPoint(value: unknown, index: number): SpawnPoint { const source = object(value, `spawnPoints[${index}]`); return { id: identifier(source.id, `spawnPoints[${index}].id`), name: text(source.name ?? source.id, `spawnPoints[${index}].name`), position: vector3(source.position, `spawnPoints[${index}].position`), yawDeg: finite(source.yawDeg ?? 0, `spawnPoints[${index}].yawDeg`), }; } export function parseMapDefinition(value: unknown): MapDefinition { const source = object(value, 'map.json'); if (source.schemaVersion !== 1 && source.schemaVersion !== 2) throw new MapValidationError('仅支持 schemaVersion: 1 或 2'); const coordinates = object(source.coordinateSystem, 'coordinateSystem'); if (coordinates.units !== 'm' || coordinates.up !== 'Z' || coordinates.forward !== '+X') throw new MapValidationError('coordinateSystem 必须为 units=m、up=Z、forward=+X'); const physicsSource = source.physics ? text(object(source.physics, 'physics').source, 'physics.source') : undefined; const visualObject = source.visual ? object(source.visual, 'visual') : undefined; const visualSource = visualObject ? text(visualObject.source, 'visual.source') : undefined; const authoringSource = source.authoring ? text(object(source.authoring, 'authoring').source, 'authoring.source') : undefined; if (authoringSource && source.schemaVersion !== 2) throw new MapValidationError('authoring 仅支持 schemaVersion: 2'); if (authoringSource && !physicsSource) throw new MapValidationError('可编辑地图必须同时声明 physics.source'); if (!physicsSource && !visualSource) throw new MapValidationError('physics.source 和 visual.source 至少需要一个'); if (physicsSource && !/\.xml$/i.test(physicsSource)) throw new MapValidationError('physics.source 必须是 XML 文件'); if (visualSource && !/\.glb$/i.test(visualSource)) throw new MapValidationError('visual.source 必须是自包含 GLB 文件'); if (authoringSource && !/\.scene\.json$/i.test(authoringSource)) throw new MapValidationError('authoring.source 必须是 .scene.json 文件'); const spawnValues = source.spawnPoints ?? []; if (!Array.isArray(spawnValues)) throw new MapValidationError('spawnPoints 必须是数组'); const spawnPoints = spawnValues.map(spawnPoint); const spawnIds = new Set(); for (const spawn of spawnPoints) { if (spawnIds.has(spawn.id)) throw new MapValidationError(`出生点 id 重复:${spawn.id}`); spawnIds.add(spawn.id); } let bounds: MapDefinition['bounds']; if (source.bounds !== undefined) { const value = object(source.bounds, 'bounds'); const minimum = vector3(value.min, 'bounds.min'); const maximum = vector3(value.max, 'bounds.max'); if (minimum.some((component, index) => component >= maximum[index])) throw new MapValidationError('bounds.min 必须在每个轴上小于 bounds.max'); bounds = { min: minimum, max: maximum }; } return { schemaVersion: source.schemaVersion, id: identifier(source.id, 'id'), name: text(source.name, 'name'), coordinateSystem: { units: 'm', up: 'Z', forward: '+X' }, physics: physicsSource ? { source: physicsSource } : undefined, visual: visualSource ? { source: visualSource, castShadow: optionalBoolean(visualObject?.castShadow, 'visual.castShadow', true), receiveShadow: optionalBoolean(visualObject?.receiveShadow, 'visual.receiveShadow', true), } : undefined, authoring: authoringSource ? { source: authoringSource } : undefined, spawnPoints, bounds, }; } export function decodeMapDefinition(data: Uint8Array): MapDefinition { let value: unknown; try { value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(data)); } catch (error) { throw new MapValidationError( `map.json 无法解析:${error instanceof Error ? error.message : String(error)}`, ); } return parseMapDefinition(value); }