44 lines
2.5 KiB
TypeScript
44 lines
2.5 KiB
TypeScript
import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
import { resolve } from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { createHash } from 'node:crypto';
|
|
import { tileWorld } from '../../src/city/format';
|
|
import type { World } from '../../src/types';
|
|
|
|
export async function exportCity(world: World, output: string, tileSize = 256) {
|
|
let exists = false;
|
|
try { await access(resolve(output, 'manifest.json')); exists = true; } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
if (exists) throw new Error('City export is immutable: use a new directory to preserve existing rooms and HTTP caches');
|
|
const { manifest, tiles } = tileWorld(world, tileSize);
|
|
await mkdir(resolve(output, 'tiles'), { recursive: true });
|
|
for (const ref of manifest.tiles) {
|
|
const file = resolve(output, ref.url);
|
|
const json = JSON.stringify(tiles.get(ref.id));
|
|
ref.sha256 = createHash('sha256').update(json).digest('hex');
|
|
await writeFile(file + '.tmp', gzipSync(json, { level: 6 }));
|
|
await rename(file + '.tmp', file);
|
|
}
|
|
const navigation = JSON.stringify({formatVersion:1,cityId:world.id,id:'navigation',
|
|
roads:world.roads.map(({id,kind,pts})=>({id,kind,pts})),
|
|
buildings:world.buildings.map(({id,kind,floors})=>({id,kind,floors:floors.filter(f=>f.level===0).map(f=>({level:0,doors:f.doors.filter(d=>d.rooms.includes('outside'))}))})),
|
|
});
|
|
manifest.navigation={url:'navigation.json.gz',sha256:createHash('sha256').update(navigation).digest('hex')};
|
|
const navFile=resolve(output,manifest.navigation.url);
|
|
await writeFile(navFile+'.tmp',gzipSync(navigation,{level:6}));
|
|
await rename(navFile+'.tmp',navFile);
|
|
const file = resolve(output, 'manifest.json');
|
|
await writeFile(file + '.tmp', JSON.stringify(manifest, null, 2));
|
|
await rename(file + '.tmp', file);
|
|
return manifest;
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
const [input, output, size] = process.argv.slice(2);
|
|
if (!input || !output) throw new Error('Usage: npm run export-city -- world.json[.gz] output-directory [tile-size-metres]');
|
|
const bytes = await readFile(input);
|
|
const world = JSON.parse((bytes[0] === 0x1f && bytes[1] === 0x8b ? gunzipSync(bytes) : bytes).toString()) as World;
|
|
const manifest = await exportCity(world, output, size === undefined ? 256 : Number(size));
|
|
console.log(`${manifest.city.name}: ${manifest.tiles.length} tiles → ${resolve(output, 'manifest.json')}`);
|
|
}
|