UNPKG

sc4

Version:

A command line utility for automating SimCity 4 modding tasks & modifying savegames

348 lines (347 loc) 10.2 kB
// # s3d.ts import { hex } from 'sc4/utils'; import FileType from './file-types.js'; import Stream from './stream.js'; import { kFileType } from './symbols.js'; import Vector3 from './vector-3.js'; import WriteBuffer from './write-buffer.js'; // # S3D // An implementation of the SimGlide (S3D) format. export default class S3D { static [kFileType] = FileType.S3D; version = '1.5'; vertexGroups = []; indexGroups = []; primGroups = []; materialGroups = []; animations = new AnimationSection(); properties = []; regpGroups = []; parse(rs) { section(rs, '3DMD'); section(rs, 'HEAD'); this.version = rs.version(2); let [major, minor] = this.version.split('.').map(Number); section(rs, 'VERT'); this.vertexGroups = rs.array(() => new VertexGroup().parse(rs)); section(rs, 'INDX'); this.indexGroups = rs.array(() => { let flags = rs.uint16(); if (flags !== 0) throw new Error(`Unknown flags were not 0: ${hex(flags, 4)}`); rs.uint16(); return rs.array(() => rs.uint16(), rs.uint16()); }); section(rs, 'PRIM'); this.primGroups = rs.array(() => { return rs.array(() => new PrimGroup().parse(rs), rs.uint16()); }); section(rs, 'MATS'); this.materialGroups = rs.array(() => { let group = new MaterialGroup(); group.parse(rs, [major, minor]); return group; }); section(rs, 'ANIM'); this.animations = new AnimationSection().parse(rs); section(rs, 'PROP'); this.properties = rs.array(() => new PropGroup().parse(rs)); section(rs, 'REGP'); this.regpGroups = rs.array(() => new RegpGroup().parse(rs)); rs.assert(); return this; } // # toBuffer() toBuffer() { return wrap('3DMD', ws => { ws.writeBuffer(wrap('HEAD', ws => ws.version(this.version))); ws.writeBuffer(wrap('VERT', ws => ws.array(this.vertexGroups))); ws.writeBuffer(wrap('INDX', ws => { ws.array(this.indexGroups, group => { ws.uint16(0); ws.uint16(0x0002); ws.uint16(group.length); ws.tuple(group, ws.uint16); }); })); ws.writeBuffer(wrap('PRIM', ws => { ws.array(this.primGroups, group => { ws.uint16(group.length); ws.tuple(group); }); })); ws.writeBuffer(wrap('MATS', ws => ws.array(this.materialGroups))); ws.writeBuffer(wrap('ANIM', ws => ws.write(this.animations))); ws.writeBuffer(wrap('PROP', ws => ws.array(this.properties))); ws.writeBuffer(wrap('REGP', ws => ws.array(this.regpGroups))); }); } } // # wrap() // This is a helper function that will wrap a buffer - that is dynamically // constructed within the specified callback - by prepending the given signature // to it and inserting the size of the buffer. Note that the size seems to be // ignored by SimCity actually because the sizes that we found don't really // correspond to the sizes if the actual buffers, so we could've just randomly // inserted it as well apparently. const encoder = new TextEncoder(); function wrap(signature, fn) { let bytes = encoder.encode(signature); let ws = new WriteBuffer(); ws.writeBuffer(bytes); ws.zeroes(4); fn(ws); ws.writeUInt32LE(ws.length, 4); return ws.toUint8Array(); } // # section(rs, signature) // Parses a section identifier & size. We don't do anything with it though. function section(rs, signature) { let id = rs.string(4); if (id !== signature) { throw new Error(`${signature} signature was ${id}`); } return rs.size(); } const VERTEX_FORMAT = 0x80004001; class VertexGroup { flags = 0; format = VERTEX_FORMAT; vertices = []; parse(rs) { this.flags = rs.uint16(); let numVertices = rs.uint16(); this.format = rs.uint32(); this.vertices = rs.array(() => new Vertex().parse(rs), numVertices); return this; } write(ws) { ws.uint16(this.flags); ws.uint16(this.vertices.length); ws.uint32(this.format); ws.tuple(this.vertices); } } class Vertex { x = 0; y = 0; z = 0; u = 0; v = 0; parse(rs) { this.x = rs.float(); this.y = rs.float(); this.z = rs.float(); this.u = rs.float(); this.v = rs.float(); return this; } write(ws) { ws.float(this.x); ws.float(this.y); ws.float(this.z); ws.float(this.u); ws.float(this.v); } } class PrimGroup { type = 0; first = 0; numIndex = 0; parse(rs) { this.type = rs.uint32(); this.first = rs.uint32(); this.numIndex = rs.uint32(); return this; } write(ws) { ws.uint32(this.type); ws.uint32(this.first); ws.uint32(this.numIndex); } } class MaterialGroup { flags = 0; alphaFunc = 0; depthFunc = 0; sourceBlend = 0; destBlend = 0; alphaThreshold = 0; matClass = 0; reserved = 0; textures = []; parse(rs, version) { this.flags = rs.uint32(); this.alphaFunc = rs.byte(); this.depthFunc = rs.byte(); this.sourceBlend = rs.byte(); this.destBlend = rs.byte(); this.alphaThreshold = rs.uint16(); this.matClass = rs.uint32(); this.reserved = rs.byte(); this.textures = rs.array(() => { let texture = new MaterialGroupTexture(); texture.parse(rs, version); return texture; }, rs.byte()); return this; } write(ws) { ws.uint32(this.flags); ws.byte(this.alphaFunc); ws.byte(this.depthFunc); ws.byte(this.sourceBlend); ws.byte(this.destBlend); ws.uint16(this.alphaThreshold); ws.uint32(this.matClass); ws.byte(this.reserved); ws.byte(this.textures.length); ws.tuple(this.textures); } } class MaterialGroupTexture { minor = 3; id = 0; wrapU = 0; wrapV = 0; magFilter = 0; minFilter = 0; animRate = 0; animMode = 0; name = ''; parse(rs, version) { [, this.minor] = version; this.id = rs.uint32(); this.wrapU = rs.byte(); this.wrapV = rs.byte(); this.magFilter = this.minor < 5 ? 0 : rs.byte(); this.minFilter = this.minor < 5 ? 0 : rs.byte(); this.animRate = rs.uint16(); this.animMode = rs.uint16(); this.name = rs.string(rs.byte()); return this; } write(ws) { ws.uint32(this.id); ws.byte(this.wrapU); ws.byte(this.wrapV); if (this.minor >= 5) { ws.byte(this.magFilter); ws.byte(this.minFilter); } ws.uint16(this.animRate); ws.uint16(this.animMode); ws.byte(this.name.length); ws.writeString(this.name); } } class AnimationSection { numFrames = 0; frameRate = 0; playMode = 0; flags = 0; displacement = 0.0; groups = []; parse(rs) { this.numFrames = rs.uint16(); this.frameRate = rs.uint16(); this.playMode = rs.uint16(); this.flags = rs.uint32(); this.displacement = rs.float(); this.groups = rs.array(() => new AnimationGroup().parse(rs, this), rs.uint16()); return this; } write(ws) { ws.uint16(this.numFrames); ws.uint16(this.frameRate); ws.uint16(this.playMode); ws.uint32(this.flags); ws.float(this.displacement); ws.uint16(this.groups.length); ws.tuple(this.groups); } *[Symbol.iterator]() { yield* this.groups; } map(...args) { return this.groups.map(...args); } } class AnimationGroup { flags = 0; name = ''; blocks = []; parse(rs, section) { let nameLength = rs.byte(); this.flags = rs.byte(); this.name = rs.string(nameLength); this.blocks = rs.array(() => { return { vertex: rs.uint16(), index: rs.uint16(), prim: rs.uint16(), material: rs.uint16(), }; }, section.numFrames); return this; } write(ws) { ws.byte(this.name.length); ws.byte(this.flags); ws.writeString(this.name); ws.tuple(this.blocks, block => { ws.uint16(block.vertex); ws.uint16(block.index); ws.uint16(block.prim); ws.uint16(block.material); }); } } class PropGroup { meshIndex = 0; frameIndex = 0; assignmentType = ''; assignedValue = ''; parse(rs) { this.meshIndex = rs.uint16(); this.frameIndex = rs.uint16(); this.assignmentType = rs.string(rs.byte()); this.assignedValue = rs.string(rs.byte()); return this; } write(ws) { ws.uint16(this.meshIndex); ws.uint16(this.frameIndex); ws.byte(this.assignmentType.length); ws.writeString(this.assignmentType); ws.byte(this.assignedValue.length); ws.writeString(this.assignedValue); } } class RegpGroup { name = ''; groups; parse(rs) { this.name = rs.string(rs.byte()); this.groups = rs.array(() => new RegpSubgroup().parse(rs), rs.uint16()); return this; } write(ws) { ws.byte(this.name.length); ws.uint16(this.groups.length); ws.tuple(this.groups); } } class RegpSubgroup { translation = new Vector3(); orientation = [0, 0, 0, 0]; parse(rs) { this.translation = rs.vector3(); this.orientation = [rs.float(), rs.float(), rs.float(), rs.float()]; return this; } write(ws) { ws.vector3(this.translation); ws.tuple(this.orientation, ws.float); } }