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')}`); }