feat: initialize sunrise with 100-player shared rooms

This commit is contained in:
2026-10-30 16:36:35 +09:00
commit 31d6eac4c2
257 changed files with 66063 additions and 0 deletions
+92
View File
@@ -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(); }
}