sc2.ts•3.51 kB
import { ScpgReadStream } from './scpg.js';
import type { MapEntity, TerrainType } from '../data/types.js';
export type { MapEntity };
interface HierarchyNode {
'##name'?: string;
name?: string;
'#hierarchy'?: HierarchyNode[];
components?: Record<string, unknown>;
}
interface SC2Data {
'#hierarchy'?: HierarchyNode[];
[key: string]: unknown;
}
export class Sc2ReadStream extends ScpgReadStream {
header() {
return {
name: this.ascii(4),
version: this.uint32(),
nodeCount: this.uint32(),
};
}
versionTags() {
return this.ka();
}
descriptor() {
const size = this.uint32();
const fileType = this.uint32();
this.seek(size - 4);
return { size, fileType };
}
sc2(): SC2Data {
this.header();
this.versionTags();
this.descriptor();
return this.ka() as SC2Data;
}
}
function classifyEntity(name: string): TerrainType {
const lower = name.toLowerCase();
if (lower.includes('bush') || lower.includes('shrub')) return 'bush';
if (lower.includes('tree') || lower.includes('palm') || lower.includes('pine') || lower.includes('_k1') || lower.includes('_k2')) return 'tree';
if (lower.includes('bld_') || lower.includes('house') || lower.includes('izba') || lower.includes('barn') || lower.includes('church') || lower.includes('mill') || lower.includes('factory')) return 'building';
if (lower.includes('rock') || lower.includes('stone') || lower.includes('boulder') || lower.includes('stn_')) return 'rock';
if (lower.includes('wall') || lower.includes('fence') || lower.includes('fag_')) return 'wall';
if (lower.includes('bridge') || lower.includes('env_') && lower.includes('bridge')) return 'bridge';
if (lower.includes('water') || lower.includes('river') || lower.includes('lake')) return 'water';
if (lower.includes('road') || lower.includes('street')) return 'road';
if (lower.includes('hill') || lower.includes('mountain')) return 'elevation';
if (lower.includes('flag') || lower.includes('capture') || lower.includes('cap_')) return 'capture_point';
if (lower.includes('spawn')) return 'spawn';
if (lower.includes('dec_') || lower.includes('debris') || lower.includes('crate')) return 'decoration';
return 'other';
}
function extractPosition(components: Record<string, unknown>): [number, number, number] | null {
for (const comp of Object.values(components)) {
if (typeof comp !== 'object' || comp === null) continue;
const c = comp as Record<string, unknown>;
if (c['comp.typename'] === 'TransformComponent') {
const pos = c['tc.worldTranslation'] as number[] | undefined;
if (pos && pos.length >= 3) {
return [pos[0], pos[1], pos[2]];
}
}
}
return null;
}
function traverseHierarchy(nodes: HierarchyNode[], entities: MapEntity[]) {
for (const node of nodes) {
if (node.name && node.components) {
const position = extractPosition(node.components as Record<string, unknown>);
if (position) {
const type = classifyEntity(node.name);
if (type !== 'other') {
entities.push({
name: node.name,
position,
type,
});
}
}
}
if (node['#hierarchy']) {
traverseHierarchy(node['#hierarchy'], entities);
}
}
}
export function extractMapEntities(data: SC2Data): MapEntity[] {
const entities: MapEntity[] = [];
if (data['#hierarchy']) {
traverseHierarchy(data['#hierarchy'], entities);
}
return entities;
}