feat: initialize sunrise with 100-player shared rooms
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { gzipSync, gunzipSync } from 'node:zlib';
|
||||
import { Mesh } from 'three';
|
||||
import { createBuildings } from '../src/scene/buildings.ts';
|
||||
import { BUILDINGS } from '../src/constants/buildings.ts';
|
||||
import type { BuildingAssetManifest } from '../src/scene/buildingAssets.ts';
|
||||
import type { BuildingHeightSource } from '../src/scene/buildingScene.ts';
|
||||
|
||||
const root = new URL('../', import.meta.url);
|
||||
const file = (path: string) => new URL(path, root);
|
||||
const hash = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('hex');
|
||||
const inputs = [
|
||||
'public/buildings/gwacheon.geojson', 'public/terrain/gwanaksan.json', 'public/terrain/gwanaksan.f32',
|
||||
'src/scene/buildings.ts', 'src/scene/buildingLightSpill.ts', 'src/scene/buildingScene.ts',
|
||||
'src/scene/buildingAssets.ts', 'src/scene/structures.ts', 'src/scene/terrainGeometry.ts',
|
||||
'src/constants/buildings.ts', 'src/constants/terrain.ts', 'package-lock.json',
|
||||
'scripts/prepare-building-assets.ts', 'src/controls/summitCoordinates.ts',
|
||||
];
|
||||
const sourceDigest = createHash('sha256');
|
||||
for (const path of inputs) {
|
||||
const bytes = readFileSync(file(path));
|
||||
// Checkouts may convert LF/CRLF without changing generation inputs.
|
||||
sourceDigest.update(path).update('\0').update(path.endsWith('.f32') ? bytes : bytes.toString('utf8').replace(/\r\n/g, '\n'));
|
||||
}
|
||||
const sourceHash = sourceDigest.digest('hex');
|
||||
const manifestPath = file(`public${BUILDINGS.assetManifestUrl}`);
|
||||
const binaryPath = file(`public${BUILDINGS.assetBufferUrl}`);
|
||||
const gzipPath = file(`public${BUILDINGS.assetBufferUrl}.gz`);
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as BuildingAssetManifest;
|
||||
const buffer = readFileSync(binaryPath);
|
||||
if (manifest.version !== BUILDINGS.assetVersion || manifest.sourceHash !== sourceHash ||
|
||||
manifest.byteLength !== buffer.byteLength || manifest.bufferHash !== hash(buffer)) throw new Error('stale');
|
||||
if (!gunzipSync(readFileSync(gzipPath)).equals(buffer)) throw new Error('stale gzip');
|
||||
console.log('Prebuilt buildings are up to date.');
|
||||
} catch {
|
||||
console.error('건물 사전 생성 파일이 없거나 오래되었습니다. npm run prepare:buildings 를 실행해 주세요.');
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} else {
|
||||
const metadata = JSON.parse(readFileSync(file('public/terrain/gwanaksan.json'), 'utf8'));
|
||||
const heights = readFileSync(file('public/terrain/gwanaksan.f32'));
|
||||
const data = JSON.parse(readFileSync(file(`public${BUILDINGS.url}`), 'utf8'));
|
||||
const city = createBuildings(data, { metadata, heights: new Float32Array(heights.buffer, heights.byteOffset, heights.byteLength / 4) });
|
||||
try {
|
||||
const chunks: Buffer[] = [];
|
||||
let byteLength = 0;
|
||||
const append = (array: ArrayBufferView) => {
|
||||
const padding = (4 - byteLength % 4) % 4;
|
||||
if (padding) { chunks.push(Buffer.alloc(padding)); byteLength += padding; }
|
||||
const offset = byteLength;
|
||||
const bytes = Buffer.from(array.buffer, array.byteOffset, array.byteLength);
|
||||
chunks.push(bytes); byteLength += bytes.byteLength;
|
||||
return offset;
|
||||
};
|
||||
const meshes: BuildingAssetManifest['meshes'] = city.group.children.map(child => {
|
||||
const mesh = child as Mesh, geometry = mesh.geometry;
|
||||
geometry.computeBoundingBox(); geometry.computeBoundingSphere();
|
||||
const box = geometry.boundingBox!, sphere = geometry.boundingSphere!;
|
||||
return {
|
||||
source: mesh.name.replace('building-height-', '') as BuildingHeightSource,
|
||||
vertices: geometry.getAttribute('position').count,
|
||||
position: append(geometry.getAttribute('position').array),
|
||||
normal: append(geometry.getAttribute('normal').array),
|
||||
lightHeight: append(geometry.getAttribute('buildingLightHeight').array),
|
||||
index: append(geometry.index!.array), indexCount: geometry.index!.count,
|
||||
indexType: geometry.index!.array instanceof Uint32Array ? 'uint32' : 'uint16',
|
||||
box: [...box.min.toArray(), ...box.max.toArray()],
|
||||
sphere: [...sphere.center.toArray(), sphere.radius],
|
||||
};
|
||||
});
|
||||
const spill = { offset: append(city.spill.texture.image.data), size: city.spill.texture.image.width,
|
||||
bounds: city.spill.bounds.toArray() as [number, number, number, number] };
|
||||
const buffer = Buffer.concat(chunks);
|
||||
const manifest: BuildingAssetManifest = {
|
||||
version: BUILDINGS.assetVersion, sourceHash, bufferHash: hash(buffer), byteLength,
|
||||
terrain: { origin: metadata.origin, observerSurfaceElevation: metadata.observerSurfaceElevation },
|
||||
counts: city.counts, target: city.target.toArray(), meshes, spill,
|
||||
};
|
||||
writeFileSync(binaryPath, buffer);
|
||||
const compressed = gzipSync(buffer, { level: 9 });
|
||||
writeFileSync(gzipPath, compressed);
|
||||
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
||||
console.log(`Saved ${data.features.length} buildings: ${byteLength.toLocaleString()} bytes (including light map).`);
|
||||
console.log(`Gzip transfer: ${compressed.byteLength.toLocaleString()} bytes.`);
|
||||
console.log(fileURLToPath(manifestPath));
|
||||
} finally { city.dispose(); }
|
||||
}
|
||||
Reference in New Issue
Block a user