├── .gitignore ├── .npmignore ├── NEWCITY.SC2 ├── package.json ├── README.md ├── sc2kparser.js ├── test.js └── simcity-2000-info.txt /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /NEWCITY.SC2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Objelisks/sc2kparser/HEAD/NEWCITY.SC2 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sc2kparser", 3 | "version": "1.0.1", 4 | "description": "SimCity 2000 save file parser", 5 | "main": "sc2kparser.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Objelisks/sc2kparser.git" 12 | }, 13 | "keywords": [ 14 | "sim", 15 | "city", 16 | "simcity", 17 | "2000", 18 | "save", 19 | "file", 20 | "parser" 21 | ], 22 | "author": "objelisks", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/Objelisks/sc2kparser/issues" 26 | }, 27 | "homepage": "https://github.com/Objelisks/sc2kparser#readme", 28 | "devDependencies": { 29 | "chai": "^3.5.0", 30 | "mocha": "^2.4.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SC2kParser.js 2 | ============= 3 | 4 | This is a module that parses SimCity 2000 save files (\*.SC2) 5 | 6 | Install 7 | ======= 8 | 9 | ``` 10 | npm install sc2kparser 11 | ``` 12 | 13 | Output 14 | ====== 15 | 16 | After the file is parsed, the parser returns a map of coordinates to tile info objects. 17 | See comments and save file documentation for more information about what each property is. 18 | 19 | The struct looks like: 20 | ``` 21 | { 22 | tiles: [ 23 | { 24 | alt: 50, 25 | building: 0, 26 | conductive: false, 27 | piped: false, 28 | powersupplied: false, 29 | saltwater: false, 30 | terrain: { 31 | slope: [0,0,0,0], 32 | waterLevel: 0 33 | }, 34 | underground: { 35 | slope: [0,0,0,0], 36 | subway: true 37 | }, 38 | water: false, 39 | watercover: false, 40 | watersupplied: false, 41 | zone: { 42 | bottomLeft: false, 43 | bottomRight: false, 44 | topLeft: false, 45 | topRight: false, 46 | type: 0 47 | } 48 | } 49 | ], 50 | cityName: "Cool City", 51 | daysElapsed: 0, 52 | founded: 34, 53 | money: 0, 54 | population: 255 55 | } 56 | ``` 57 | 58 | Usage 59 | ===== 60 | 61 | Pass a typed array of bytes to the parse function like this: 62 | ``` 63 | let sc2kparser = require('sc2kparser'); 64 | 65 | // get file bytes somehow 66 | let bytes = new Uint8Array(...); 67 | 68 | let struct = sc2kparser.parse(bytes); 69 | console.log(struct); 70 | ``` 71 | 72 | Here is an easy way to get a sc2k save file in the browser: 73 | ``` 74 | document.body.addEventListener('dragover', function(event) { 75 | event.preventDefault(); 76 | event.stopPropagation(); 77 | }, false); 78 | document.body.addEventListener('drop', function(event) { 79 | event.preventDefault(); 80 | event.stopPropagation(); 81 | 82 | let file = event.dataTransfer.files[0]; 83 | let fileReader = new FileReader(); 84 | fileReader.onload = function(e) { 85 | let bytes = new Uint8Array(e.target.result); 86 | let struct = sc2kparser.parse(bytes); 87 | console.log(struct); 88 | }; 89 | fileReader.readAsArrayBuffer(file); 90 | }, false); 91 | ``` 92 | 93 | Thanks 94 | ====== 95 | 96 | Thanks to David Moews for his documentation of the file format. (simcity-2000-info.txt) 97 | 98 | 99 | License 100 | ======= 101 | 102 | ISC 103 | -------------------------------------------------------------------------------- /sc2kparser.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict" 3 | 4 | let sc2kparser = {}; 5 | let buildingNames = require('./buildingNames.json'); 6 | 7 | /* 8 | The data in most SimCity segments is compressed using a form of run-length 9 | encoding. When this is done, the data in the segment consists of a series 10 | of chunks of two kinds. The first kind of chunk has first byte from 1 to 11 | 127; in this case the first byte is a count telling how many data bytes 12 | follow. The second kind of chunk has first byte from 129 to 255. In this 13 | case, if you subtract 127 from the first byte, you get a count telling how 14 | many times the following single data byte is repeated. Chunks with first 15 | byte 0 or 128 never seem to occur. 16 | */ 17 | sc2kparser.decompressSegment = function(bytes) { 18 | let output = []; 19 | let dataCount = 0; 20 | 21 | for(let i=0; i 0) { 23 | output.push(bytes[i]); 24 | dataCount -= 1; 25 | continue; 26 | } 27 | 28 | if(bytes[i] < 128) { 29 | // data bytes 30 | dataCount = bytes[i]; 31 | } else { 32 | // run-length encoded byte 33 | let repeatCount = bytes[i] - 127; 34 | let repeated = bytes[i+1]; 35 | for(let i=0; i 0) { 55 | let segmentTitle = Array.prototype.map.call(rest.subarray(0, 4), x => String.fromCharCode(x)).join(''); 56 | let lengthBytes = rest.subarray(4, 8); 57 | let segmentLength = new DataView(lengthBytes.buffer).getUint32(lengthBytes.byteOffset); 58 | let segmentContent = rest.subarray(8, 8+segmentLength); 59 | if(!alreadyDecompressedSegments[segmentTitle]) { 60 | segmentContent = sc2kparser.decompressSegment(segmentContent); 61 | } 62 | segments[segmentTitle] = segmentContent; 63 | rest = rest.subarray(8+segmentLength); 64 | } 65 | return segments; 66 | }; 67 | 68 | // slopes define the relative heights of corners from left to right 69 | // i.e.: [0,0,1,1] => 70 | // 0 0 top 71 | // 72 | // 1 1 bottom 73 | // which is a slope where the top side is at this tile's altitude, and the bottom 74 | // side is at the next altitude level 75 | let xterSlopeMap = { 76 | 0x0: [0,0,0,0], 77 | 0x1: [1,1,0,0], 78 | 0x2: [0,1,0,1], 79 | 0x3: [0,0,1,1], 80 | 0x4: [1,0,1,0], 81 | 0x5: [1,1,0,1], 82 | 0x6: [0,1,1,1], 83 | 0x7: [1,0,1,1], 84 | 0x8: [1,1,1,0], 85 | 0x9: [0,1,0,0], 86 | 0xA: [0,0,0,1], 87 | 0xB: [0,0,1,0], 88 | 0xC: [1,0,0,0], 89 | 0xD: [1,1,1,1] 90 | }; 91 | 92 | // NOTE: surf. water documetation inconsistency 93 | // denotes which sides have land 94 | // 0 95 | // .___. 96 | // 1 | | 2 97 | // |___| 98 | // 3 99 | let xterWaterMap = { 100 | 0x0: [1,0,0,1], // left-right open canal 101 | 0x1: [0,1,1,0], // top-bottom open canal 102 | 0x2: [1,1,0,1], // right open bay 103 | 0x3: [1,0,1,1], // left open bay 104 | 0x4: [0,1,1,1], // top open bay 105 | 0x5: [1,1,1,0] // bottom open bay 106 | }; 107 | 108 | let waterLevels = { 109 | 0x0: "dry", 110 | 0x1: "submerged", 111 | 0x2: "shore", 112 | 0x3: "surface", 113 | 0x4: "waterfall" 114 | }; 115 | 116 | sc2kparser.segmentHandlers = { 117 | 'ALTM': (data, struct) => { 118 | // NOTE: documentation is weak on this segment 119 | // NOTE: uses DataView instead of typed array, because we need non-aligned access to 16bit ints 120 | let view = new DataView(data.buffer, data.byteOffset, data.byteLength); 121 | for(let i=0; i { 129 | let view = new Uint8Array(data); 130 | let len = view[0] & 0x3F; // limit to 32 131 | let strDat = view.subarray(1,1+len); 132 | struct.cityName = Array.prototype.map.call(strDat, x => String.fromCharCode(x)).join(''); 133 | }, 134 | 'XBIT': (data, struct) => { 135 | let view = new Uint8Array(data); 136 | view.forEach((square, i) => { 137 | let tile = struct.tiles[i]; 138 | tile.saltwater = (square & 0x01) !== 0; 139 | //tile.unknown = (square & 0x02) !== 0; 140 | tile.watercover = (square & 0x04) !== 0; 141 | //tile.unknown = (square & 0x08) !== 0; 142 | tile.watersupplied = (square & 0x10) !== 0; 143 | tile.piped = (square & 0x20) !== 0; 144 | tile.powersupplied = (square & 0x40) !== 0; 145 | tile.conductive = (square & 0x80) !== 0; 146 | }); 147 | }, 148 | 'XBLD': (data, struct) => { 149 | let view = new Uint8Array(data); 150 | view.forEach((square, i) => { 151 | struct.tiles[i].building = square; 152 | struct.tiles[i].buildingName = buildingNames[square.toString(16).toUpperCase()]; 153 | }); 154 | }, 155 | 'XTER': (data, struct) => { 156 | let view = new Uint8Array(data); 157 | view.forEach((square, i) => { 158 | let terrain = {}; 159 | if(square < 0x3E) { 160 | let slope = square & 0x0F; 161 | let wetness = (square & 0xF0) >> 4; 162 | terrain.slope = xterSlopeMap[slope]; 163 | terrain.waterlevel = waterLevels[wetness]; 164 | } else if(square === 0x3E) { 165 | terrain.slope = xterSlopeMap[0]; 166 | terrain.waterlevel = waterLevels[0x4]; 167 | } else if(square >= 0x40) { 168 | let surfaceWater = square & 0x0F; 169 | terrain.slope = xterSlopeMap[0]; 170 | terrain.surfaceWater = xterWaterMap[surfaceWater]; 171 | terrain.waterlevel = waterLevels[0x3]; 172 | } 173 | struct.tiles[i].terrain = terrain; 174 | }); 175 | }, 176 | 'XUND': (data, struct) => { 177 | let view = new Uint8Array(data); 178 | view.forEach((square, i) => { 179 | let underground = {}; 180 | if(square < 0x1E) { 181 | let slope = square & 0x0F; 182 | underground.slope = xterSlopeMap[slope]; 183 | if((square & 0xF0) === 0x00) { 184 | underground.subway = true; 185 | } else if(((square & 0xF0) === 0x10) && (square < 0x1F)) { 186 | underground.pipes = true; 187 | } 188 | } else if((square === 0x1F) || (square === 0x20)) { 189 | underground.subway = true; 190 | underground.pipes = true; 191 | underground.slope = xterSlopeMap[0x0]; 192 | underground.subwayLeftRight = square === 0x1F; 193 | } else if(square === 0x23) { 194 | underground.station = true; 195 | underground.slope = xterSlopeMap[0x0]; 196 | } 197 | struct.tiles[i].underground = underground; 198 | }); 199 | }, 200 | 'XZON': (data, struct) => { 201 | let view = new Uint8Array(data); 202 | view.forEach((square, i) => { 203 | let zone = {}; 204 | zone.topLeft = (square & 0x80) !== 0; 205 | zone.topRight = (square & 0x10) !== 0; 206 | zone.bottomLeft = (square & 0x40) !== 0; 207 | zone.bottomRight = (square & 0x20) !== 0; 208 | zone.type = square & 0x0F; 209 | struct.tiles[i].zone = zone; 210 | }); 211 | }, 212 | 'XTXT': (data, struct) => { 213 | // idk 214 | let view = new Uint8Array(data); 215 | view.forEach((square, i) => { 216 | if(square !== 0) { 217 | struct.tiles[i].sign = square; 218 | } 219 | }); 220 | }, 221 | 'XLAB': (data, struct) => { 222 | // labels (1 byte len + 24 byte string) 223 | let view = new Uint8Array(data); 224 | let labels = []; 225 | for(let i=0; i<256; i++) { 226 | let labelPos = i*25; 227 | let labelLength = Math.max(0, Math.min(view[labelPos], 24)); 228 | let labelData = view.subarray(labelPos+1, labelPos+1+labelLength); 229 | labels[i] = Array.prototype.map.call(labelData, x => String.fromCharCode(x)).join(''); 230 | } 231 | struct.labels = labels; 232 | }, 233 | 'MISC': (data, struct) => { 234 | let view = new DataView(data.buffer, data.byteOffset, data.byteLength); 235 | struct.first = view.getInt32(0); 236 | struct.founded = view.getInt32(3*4); 237 | struct.daysElapsed = view.getInt32(4*4); 238 | struct.money = view.getInt32(5*4); 239 | struct.population = view.getInt32(20*4); 240 | // TODO: classify rest of misc data 241 | } 242 | // TODO: XMIC, XTHG, XGRP, XPLC, XFIR, XPOP, XROG, XPLT, XVAL, XCRM, XTRF 243 | }; 244 | 245 | // decompress and interpret bytes into a combined tiles format 246 | sc2kparser.toVerboseFormat = function(segments) { 247 | let struct = {}; 248 | struct.tiles = []; 249 | for(let i=0; i<128*128; i++) { 250 | struct.tiles.push({}); 251 | } 252 | 253 | Object.keys(segments).forEach((segmentTitle) => { 254 | let data = segments[segmentTitle]; 255 | let handler = sc2kparser.segmentHandlers[segmentTitle]; 256 | if(handler) { 257 | handler(data, struct); 258 | } 259 | }); 260 | return struct; 261 | }; 262 | 263 | // bytes -> file segments decompressed 264 | sc2kparser.parse = function(bytes, options) { 265 | let buffer = new Uint8Array(bytes); 266 | let fileHeader = buffer.subarray(0, 12); 267 | let rest = buffer.subarray(12); 268 | let segments = sc2kparser.splitIntoSegments(rest); 269 | let struct = sc2kparser.toVerboseFormat(segments); 270 | return struct; 271 | }; 272 | 273 | // check header bytes 274 | sc2kparser.isSimCity2000SaveFile = function(bytes) { 275 | // check IFF header 276 | if(bytes[0] !== 0x46 || 277 | bytes[1] !== 0x4F || 278 | bytes[2] !== 0x52 || 279 | bytes[3] !== 0x4D) { 280 | return false; 281 | } 282 | 283 | // check sc2k header 284 | if(bytes[8] !== 0x53 || 285 | bytes[9] !== 0x43 || 286 | bytes[10] !== 0x44 || 287 | bytes[11] !== 0x48) { 288 | return false; 289 | } 290 | 291 | return true; 292 | } 293 | 294 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 295 | module.exports = sc2kparser; 296 | } else { 297 | window.sc2kparser = sc2kparser; 298 | } 299 | 300 | })(); 301 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | let mocha = require('mocha'); 3 | let assert = require('chai').assert; 4 | let parser = require('./sc2kparser.js'); 5 | 6 | describe('decompressSegment', () => { 7 | let compressedSegment = Uint8Array.from([2, 34, 55, 130, 255]); 8 | let decompressed = parser.decompressSegment(compressedSegment); 9 | let expected = Uint8Array.from([34, 55, 255, 255, 255]); 10 | it('should decompress correctly', () => { 11 | assert.deepEqual(expected, decompressed); 12 | }); 13 | }); 14 | 15 | describe('splitIntoSegments', () => { 16 | let arr = [65,66,67,68, 0,0,0,4, 3,1,2,3]; 17 | let fileBytes = Uint8Array.from(arr); 18 | let segments = parser.splitIntoSegments(fileBytes); 19 | let expected = { 20 | "ABCD": Uint8Array.from([1, 2, 3]) 21 | }; 22 | it('should split into segments', () => { 23 | assert.deepEqual(expected, segments); 24 | }); 25 | }); 26 | 27 | describe('segment handlers', () => { 28 | 29 | describe('altm', () => { 30 | it('should parse altitude data', () => { 31 | let struct = {tiles:[{}, {}, {}]}; 32 | let handler = parser.segmentHandlers.ALTM; 33 | let segment = Uint8Array.from([0x00, 0x81, 0x00, 0x08]); 34 | handler(segment, struct); 35 | assert.equal(50, struct.tiles[0].alt); 36 | assert.isTrue(struct.tiles[0].water); 37 | assert.equal(400, struct.tiles[1].alt); 38 | assert.isNotTrue(struct.tiles[1].water); 39 | }); 40 | }); 41 | 42 | describe('cnam', () => { 43 | it('should parse name data', () => { 44 | let struct = {tiles:[{}, {}, {}]}; 45 | let handler = parser.segmentHandlers.CNAM; 46 | let segment = Uint8Array.from([5, 104, 101, 108, 108, 111]); 47 | handler(segment, struct); 48 | assert.equal('hello', struct.cityName); 49 | }); 50 | }); 51 | 52 | describe('xbit', () => { 53 | it('should handle flags on', () => { 54 | let struct = {tiles:[{}, {}, {}]}; 55 | let handler = parser.segmentHandlers.XBIT; 56 | let segment = Uint8Array.from([0xFF, 0]); 57 | handler(segment, struct); 58 | assert.isTrue(struct.tiles[0].saltwater); 59 | assert.isTrue(struct.tiles[0].watercover); 60 | assert.isTrue(struct.tiles[0].watersupplied); 61 | assert.isTrue(struct.tiles[0].piped); 62 | assert.isTrue(struct.tiles[0].powersupplied); 63 | assert.isTrue(struct.tiles[0].conductive); 64 | }); 65 | 66 | it('should handle flags off', () => { 67 | let struct = {tiles:[{}, {}, {}]}; 68 | let handler = parser.segmentHandlers.XBIT; 69 | let segment = Uint8Array.from([0, 0xFF]); 70 | handler(segment, struct); 71 | assert.isNotTrue(struct.tiles[0].saltwater); 72 | assert.isNotTrue(struct.tiles[0].watercover); 73 | assert.isNotTrue(struct.tiles[0].watersupplied); 74 | assert.isNotTrue(struct.tiles[0].piped); 75 | assert.isNotTrue(struct.tiles[0].powersupplied); 76 | assert.isNotTrue(struct.tiles[0].conductive); 77 | }); 78 | }); 79 | 80 | describe('xbld', () => { 81 | it('should parse building code', () => { 82 | let struct = {tiles:[{}, {}, {}]}; 83 | let handler = parser.segmentHandlers.XBLD; 84 | let segment = Uint8Array.from([0xCC, 0xFF]); 85 | handler(segment, struct); 86 | assert.equal(0xCC, struct.tiles[0].building); 87 | }); 88 | it('should parse building name', () => { 89 | let struct = {tiles:[{}, {}, {}]}; 90 | let handler = parser.segmentHandlers.XBLD; 91 | let segment = Uint8Array.from([0xCC, 0xFF]); 92 | handler(segment, struct); 93 | assert.equal('Solar power plant', struct.tiles[0].buildingName); 94 | }); 95 | }); 96 | 97 | describe('xter', () => { 98 | it('should parse dry slopes', () => { 99 | let struct = {tiles:[{}, {}, {}]}; 100 | let handler = parser.segmentHandlers.XTER; 101 | let segment = Uint8Array.from([0x00]); 102 | handler(segment, struct); 103 | assert.deepEqual({slope: [0,0,0,0], waterlevel: "dry"}, struct.tiles[0].terrain); 104 | }); 105 | it('should parse submerged slopes', () => { 106 | let struct = {tiles:[{}, {}, {}]}; 107 | let handler = parser.segmentHandlers.XTER; 108 | let segment = Uint8Array.from([0x1D]); 109 | handler(segment, struct); 110 | assert.deepEqual({slope: [1,1,1,1], waterlevel: "submerged"}, struct.tiles[0].terrain); 111 | }); 112 | it('should parse shore slopes', () => { 113 | let struct = {tiles:[{}, {}, {}]}; 114 | let handler = parser.segmentHandlers.XTER; 115 | let segment = Uint8Array.from([0x25]); 116 | handler(segment, struct); 117 | assert.deepEqual({slope: [1,1,0,1], waterlevel: "shore"}, struct.tiles[0].terrain); 118 | }); 119 | it('should parse a waterfall', () => { 120 | let struct = {tiles:[{}, {}, {}]}; 121 | let handler = parser.segmentHandlers.XTER; 122 | let segment = Uint8Array.from([0x3E]); 123 | handler(segment, struct); 124 | assert.deepEqual({slope: [0,0,0,0], waterlevel: "waterfall"}, struct.tiles[0].terrain); 125 | }); 126 | it('should parse surface water slope', () => { 127 | let struct = {tiles:[{}, {}, {}]}; 128 | let handler = parser.segmentHandlers.XTER; 129 | let segment = Uint8Array.from([0x33]); 130 | handler(segment, struct); 131 | assert.deepEqual({slope: [0,0,1,1], waterlevel: "surface"}, struct.tiles[0].terrain); 132 | }); 133 | it('should parse surface water canal', () => { 134 | let struct = {tiles:[{}, {}, {}]}; 135 | let handler = parser.segmentHandlers.XTER; 136 | let segment = Uint8Array.from([0x41]); 137 | handler(segment, struct); 138 | assert.deepEqual({slope: [0,0,0,0], waterlevel: "surface", surfaceWater: [0,1,1,0]}, struct.tiles[0].terrain); 139 | }); 140 | }); 141 | 142 | describe('xund', () => { 143 | it('should parse subways', () => { 144 | let struct = {tiles:[{}, {}, {}]}; 145 | let handler = parser.segmentHandlers.XUND; 146 | let segment = Uint8Array.from([0x09]); 147 | handler(segment, struct); 148 | assert.deepEqual({slope: [0,1,0,0], subway: true}, struct.tiles[0].underground); 149 | }); 150 | it('should parse pipes', () => { 151 | let struct = {tiles:[{}, {}, {}]}; 152 | let handler = parser.segmentHandlers.XUND; 153 | let segment = Uint8Array.from([0x18]); 154 | handler(segment, struct); 155 | assert.deepEqual({slope: [1,1,1,0], pipes: true}, struct.tiles[0].underground); 156 | }); 157 | it('should parse crossovers', () => { 158 | let struct = {tiles:[{}, {}, {}]}; 159 | let handler = parser.segmentHandlers.XUND; 160 | let segment = Uint8Array.from([0x1F]); 161 | handler(segment, struct); 162 | assert.deepEqual({slope: [0,0,0,0], subway: true, pipes: true, subwayLeftRight: true}, struct.tiles[0].underground); 163 | }); 164 | it('should parse stations', () => { 165 | let struct = {tiles:[{}, {}, {}]}; 166 | let handler = parser.segmentHandlers.XUND; 167 | let segment = Uint8Array.from([0x23]); 168 | handler(segment, struct); 169 | assert.deepEqual({slope: [0,0,0,0], station: true}, struct.tiles[0].underground); 170 | }); 171 | }); 172 | 173 | describe('xzon', () => { 174 | it('should parse a 1x1 zone', () => { 175 | let struct = {tiles:[{}, {}, {}]}; 176 | let handler = parser.segmentHandlers.XZON; 177 | let segment = Uint8Array.from([0xF4]); 178 | handler(segment, struct); 179 | assert.deepEqual({topLeft: true, topRight: true, bottomLeft: true, bottomRight: true, type: 4}, struct.tiles[0].zone); 180 | }); 181 | it('should parse a 4x4 zone', () => { 182 | let struct = {tiles:[{}, {}, {}, {}]}; 183 | let handler = parser.segmentHandlers.XZON; 184 | let segment = Uint8Array.from([0x87, 0x17, 0x47, 0x27]); 185 | handler(segment, struct); 186 | assert.deepEqual({topLeft: true, topRight: false, bottomLeft: false, bottomRight: false, type: 7}, struct.tiles[0].zone); 187 | assert.deepEqual({topLeft: false, topRight: true, bottomLeft: false, bottomRight: false, type: 7}, struct.tiles[1].zone); 188 | assert.deepEqual({topLeft: false, topRight: false, bottomLeft: true, bottomRight: false, type: 7}, struct.tiles[2].zone); 189 | assert.deepEqual({topLeft: false, topRight: false, bottomLeft: false, bottomRight: true, type: 7}, struct.tiles[3].zone); 190 | }); 191 | }); 192 | 193 | describe('xtxt', () => { 194 | it('should recognize no sign set', () => { 195 | let struct = {tiles:[{}, {}, {}]}; 196 | let handler = parser.segmentHandlers.XTXT; 197 | let segment = Uint8Array.from([0x00]); 198 | handler(segment, struct); 199 | assert.isUndefined(struct.tiles[0].sign); 200 | }); 201 | it('should recognize sign set', () => { 202 | let struct = {tiles:[{}, {}, {}]}; 203 | let handler = parser.segmentHandlers.XTXT; 204 | let segment = Uint8Array.from([0x08]); 205 | handler(segment, struct); 206 | assert.isDefined(struct.tiles[0].sign); 207 | assert.equal(struct.tiles[0].sign, 8); 208 | }); 209 | }); 210 | 211 | describe('xlab', () => { 212 | it('should parse empty label', () => { 213 | let struct = {tiles:[{}, {}, {}]}; 214 | let handler = parser.segmentHandlers.XLAB; 215 | let segment = Uint8Array.from([ 216 | 0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 217 | 0x0B, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]); 218 | handler(segment, struct); 219 | assert.isDefined(struct.labels[0]); 220 | assert.isDefined(struct.labels[1]); 221 | assert.equal(struct.labels[0], ''); 222 | assert.equal(struct.labels[1], 'hello world'); 223 | }); 224 | it('should parse short label', () => { 225 | let struct = {tiles:[{}, {}, {}]}; 226 | let handler = parser.segmentHandlers.XLAB; 227 | let segment = Uint8Array.from([ 228 | 0x06, 102, 103, 115, 102, 100, 115, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 229 | 0x0B, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]); 230 | handler(segment, struct); 231 | assert.isDefined(struct.labels[0]); 232 | assert.isDefined(struct.labels[1]); 233 | assert.equal(struct.labels[0], 'fgsfds'); 234 | assert.equal(struct.labels[1], 'hello world'); 235 | }); 236 | it('should parse max length label', () => { 237 | let struct = {tiles:[{}, {}, {}]}; 238 | let handler = parser.segmentHandlers.XLAB; 239 | let segment = Uint8Array.from([ 240 | 0x18, 116, 119, 101, 110, 116, 121, 32, 102, 111, 117, 114, 32, 99, 104, 97, 114, 97, 99, 116, 101, 114, 115, 32, 120, 241 | 0x0B, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]); 242 | handler(segment, struct); 243 | assert.isDefined(struct.labels[0]); 244 | assert.isDefined(struct.labels[1]); 245 | assert.equal(struct.labels[0], 'twenty four characters x'); 246 | assert.equal(struct.labels[1], 'hello world'); 247 | }); 248 | it('should parse overflow label', () => { 249 | let struct = {tiles:[{}, {}, {}]}; 250 | let handler = parser.segmentHandlers.XLAB; 251 | let segment = Uint8Array.from([ 252 | 0xFF, 116, 119, 101, 110, 116, 121, 32, 102, 111, 117, 114, 32, 99, 104, 97, 114, 97, 99, 116, 101, 114, 115, 32, 120, 253 | 0x0B, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]); 254 | handler(segment, struct); 255 | assert.isDefined(struct.labels[0]); 256 | assert.isDefined(struct.labels[1]); 257 | assert.equal(struct.labels[0], 'twenty four characters x'); 258 | assert.equal(struct.labels[1], 'hello world'); 259 | }); 260 | }); 261 | 262 | describe('misc', () => { 263 | it('should parse some of the misc data', () => { 264 | let struct = {tiles:[{}, {}, {}]}; 265 | let handler = parser.segmentHandlers.MISC; 266 | let segment = new Int32Array(21); 267 | let view = new DataView(segment.buffer); 268 | view.setInt32(3*4, 1950); 269 | view.setInt32(5*4, 9999); 270 | view.setInt32(20*4, 100000); 271 | handler(segment, struct); 272 | assert.equal(1950, struct.founded); 273 | assert.equal(9999, struct.money); 274 | assert.equal(100000, struct.population); 275 | }); 276 | }); 277 | }); 278 | -------------------------------------------------------------------------------- /simcity-2000-info.txt: -------------------------------------------------------------------------------- 1 | 2 | SIMCITY 2000 FILE FORMAT (MS-DOS VERSION) 9-XII-1995 3 | 4 | DISCLAIMER: The information in this file does not originate from Maxis, 5 | any employee of Maxis, or any signatory of a non-disclosure agreement with 6 | Maxis, and neither I nor (I presume) Maxis make any claims as to its accuracy 7 | or usability for any purpose. 8 | 9 | 10 | 11 | First of all, SimCity files are in `IFF format', which means that 12 | they consist of an 12-byte file header followed by a series of segments. 13 | The file header format is as follows: 14 | 15 | Bytes 1-4: 'FORM' (indicates an IFF file) 16 | Bytes 5-8: Total count of bytes in file, except for the first 8 bytes 17 | in this header 18 | Bytes 9-12: File type: in the case of SimCity 2000, 'SCDH' 19 | 20 | Each segment has an 8-byte header: 21 | 22 | Bytes 1-4: Type of segment 23 | Byres 5-8: Number of bytes in this segment, except for this 8-byte header 24 | 25 | The remaining bytes in each segment are data. 26 | 27 | The data in most SimCity segments is compressed using a form of run-length 28 | encoding. When this is done, the data in the segment consists of a series 29 | of chunks of two kinds. The first kind of chunk has first byte from 1 to 30 | 127; in this case the first byte is a count telling how many data bytes 31 | follow. The second kind of chunk has first byte from 129 to 255. In this 32 | case, if you subtract 127 from the first byte, you get a count telling how 33 | many times the following single data byte is repeated. Chunks with first 34 | byte 0 or 128 never seem to occur. 35 | 36 | SimCity files consist of the following segment types, in order, with the 37 | following lengths. Except as noted, segments are compressed as above, and 38 | the length given for them is the length after uncompression; the compressed 39 | length may lety. 40 | 41 | Segment type Length 42 | 43 | MISC 4800 44 | ALTM 32768 (uncompressed) 45 | XTER 16384 46 | XBLD 16384 47 | XZON 16384 48 | XUND 16384 49 | XTXT 16384 50 | XLAB 6400 51 | XMIC 1200 52 | XTHG 480 53 | XBIT 16384 54 | XTRF 4096 55 | XPLT 4096 56 | XVAL 4096 57 | XCRM 4096 58 | XPLC 1024 59 | XFIR 1024 60 | XPOP 1024 61 | XROG 1024 62 | XGRP 3328 63 | CNAM 32 (uncompressed; optional?) 64 | 65 | Some remarks about the data in the individual segments: 66 | 67 | ALTM: Altitude map. Uncompressed. Contains two bytes for each square. 68 | (For our purposes, we will define `left', `right', `top', and `bottom' 69 | by saying that squares are scanned by rows from top to bottom, and 70 | from right to left within each row.) 71 | Taking each two bytes as a 16-bit integer, MSB first, bits 4-0 72 | give the altitude of the square, from 50 to 3150 feet. Bit 7 seems 73 | to be set if the square is covered with water. I do not know what 74 | bits 15-8 and 6-5 do. 75 | 76 | CNAM: City name. Uncompressed. Seems to be optional. When it is present, 77 | it consists of a length byte from 0 to 31, followed by that many 78 | bytes of city name. It is padded out to 32 bytes with zeroes. 79 | 80 | XBIT: One byte of flags per square. 81 | 82 | Bit 7: Electrically conductive? 83 | 6: Powered? 84 | 5: Piped? (i.e., permeable to water?) 85 | 4: Supplied with water? 86 | 3: ??? 87 | 2: Covered with water? (i.e., part of a lake, river, or ocean?) 88 | 1: ??? 89 | 0: Does placing water here give salt water? 90 | 91 | XBLD: One code byte per square, describing what's on it. (In general, 92 | to put a building or whatever up and have a consistent 93 | resultant simulation, you have to do a lot more than change 94 | this array.) 95 | 96 | The codes in this list are in hexadecimal. 97 | 98 | Non-buildings: 99 | 100 | 00: Clear terrain (empty) 101 | 01-04: Rubble 102 | 05: Radioactive waste 103 | 06-0C: Trees (density increases as code increases) 104 | 0D: Small park (set XZON as for a 1x1 building) 105 | 0E-1C: Power lines (letious directions, slopes) 106 | The difference X between the code and 0E, the first code, tells 107 | what direction(s) and slope the power line takes. 108 | X (in hex) Direction 109 | 0 Left-right [for definition of directions, see note with ALTM] 110 | 1 Top-bottom 111 | 2 Top-bottom; slopes upwards towards top 112 | 3 Left-right; slopes upwards towards right 113 | 4 Top-bottom; slopes upwards towards bottom 114 | 5 Left-right; slopes upwards towards left 115 | 6 From bottom side to right side 116 | 7 Bottom to left 117 | 8 Left to top 118 | 9 Top to right 119 | A T junction between top, right and bottom 120 | B T between left, bottom and right 121 | C T between top, left and bottom 122 | D T between top, left and right 123 | E Intersection connecting top, left, bottom, and right 124 | 1D-2B: Roads (letious directions, slopes; same coding as for 0E-1C) 125 | 2C-3A: Rails (letious directions, slopes; same coding as for 0E-1C) 126 | 3B-3E: More sloping rails. These are used as preparation before ascending. 127 | The 2C-3A rail codes are used on the actual sloping square. This is why 128 | rails don't look right when ascending a 1:1 grade. 129 | 3B: Top-bottom; slopes upwards towards top 130 | 3C: Left-right; slopes upwards towards right 131 | 3D: Top-bottom; slopes upwards towards bottom 132 | 3E: Left-right; slopes upwards towards left 133 | 3F-42: Tunnel entrances 134 | 3F: Tunnel to the top 135 | 40: Tunnel to the right 136 | 41: Tunnel to the bottom 137 | 42: Tunnel to the left 138 | 43-44: Crossovers (roads/power lines) 139 | 43: Road left-right, power top-bottom 140 | 44: Road top-bottom, power left-right 141 | 45-46: Crossovers (roads/rails) 142 | 45: Road left-right, rails top-bottom 143 | 46: Road top-bottom, rails left-right 144 | 47-48: Crossovers (rails/power lines) 145 | 47: Rails left-right, power lines top-bottom 146 | 48: Rails top-bottom, power lines left-right 147 | 49-4A: Highways (set XZON as for a 1x1 building) 148 | 49: Highway left-right 149 | 4A: Highway top-bottom 150 | 4B-4C: Crossovers (roads/highways; set XZON as for a 1x1 building) 151 | 4B: Highway left-right, road top-bottom 152 | 4C: Highway top-bottom, road left-right 153 | 4D-4E: Crossovers (rails/highways; set XZON as for a 1x1 building) 154 | 4D: Highway left-right, rails top-bottom 155 | 4E: Highway top-bottom, rails left-right 156 | 4F-50: Crossovers (highways/power lines; set XZON as for a 1x1 building) 157 | 4F: Highway left-right, power lines top-bottom 158 | 50: Highway top-bottom, power lines left-right 159 | 51-55: Suspension bridge pieces 160 | 56-59: Other road bridge pieces 161 | 5A-5B: Rail bridge pieces 162 | 5C: Elevated power lines 163 | 5D-60: Highway entrances (on-ramps) 164 | 5D: Highway at top, road at left OR highway at right, road at bottom 165 | 5E: H right, R top OR H top, R right 166 | 5F: R right, H bottom OR H left, R top 167 | 60: R left, H bottom OR H left, R bottom 168 | 61-69: Highways (letious directions, slopes; 2x2 tiles; XZON should be set 169 | as for a 2x2 building) 170 | 61: Highway top-bottom, slopes up to the top 171 | 62: Highway left-right, slopes up to the right 172 | 63: Highway top-bottom, slopes up to the bottom 173 | 64: Highway left-right, slopes up to the left 174 | 65: Highway joining the bottom to the right 175 | 66: Highway joining the bottom to the left 176 | 67: Highway joining the left to the top 177 | 68: Highway joining the top to the right 178 | 69: Cloverleaf intersection connecting top, left, bottom and right 179 | 6A-6B: Highway bridges (2x2 tiles; set XZON as for a 2x2 building.) This 180 | is a reinforced bridge. Use 49/4A for the `Hiway' bridge. 181 | 6C-6F: Sub/rail connections (set XZON as for a 1x1 building) 182 | 6C: Sub/rail connection, rail at bottom 183 | 6D: Sub/rail connection, rail at left 184 | 6E: Sub/rail connection, rail at top 185 | 6F: Sub/rail connection, rail at right 186 | 187 | Buildings: 188 | 189 | Residential, 1x1: 190 | 70-73: Lower-class homes 191 | 74-77: Middle-class homes 192 | 78-7B: Luxury homes 193 | Commercial, 1x1: 194 | 7C: Gas station 195 | 7D: Bed & breakfast inn 196 | 7E: Convenience store 197 | 7F: Gas station 198 | 80: Small office building 199 | 81: Office building 200 | 82: Warehouse 201 | 83: Cassidy's Toy Store 202 | Industrial, 1x1: 203 | 84: Warehouse 204 | 85: Chemical storage 205 | 86: Warehouse 206 | 87: Industrial substation 207 | Miscellaneous, 1x1: 208 | 88-89: Construction 209 | 8A-8B: Abandoned building 210 | Residential, 2x2: 211 | 8C: Cheap apartments 212 | 8D-8E: Apartments 213 | 8F-90: Nice apartments 214 | 91-93: Condominium 215 | Commercial, 2x2: 216 | 94: Shopping center 217 | 95: Grocery store 218 | 96: Office building 219 | 97: Resort hotel 220 | 98: Office building 221 | 99: Office / Retail 222 | 9A-9D: Office building 223 | Industrial, 2x2: 224 | 9E: Warehouse 225 | 9F: Chemical processing 226 | A0-A5: Factory 227 | Miscellaneous, 2x2: 228 | A6-A9: Construction 229 | AA-AD: Abandoned building 230 | Residential, 3x3: 231 | AE-AF: Large apartment building 232 | B0-B1: Condominium 233 | Commercial, 3x3: 234 | B2: Office park 235 | B3: Office tower 236 | B4: Mini-mall 237 | B5: Theater square 238 | B6: Drive-in theater 239 | B7-B8: Office tower 240 | B9: Parking lot 241 | BA: Historic office building 242 | BB: Corporate headquarters 243 | Industrial, 3x3: 244 | BC: Chemical processing 245 | BD: Large factory 246 | BE: Industrial thingamajig 247 | BF: Factory 248 | C0: Large warehouse 249 | C1: Warehouse 250 | Miscellaneous, 3x3: 251 | C2-C3: Construction 252 | C4-C5: Abandoned building 253 | Power plants: 254 | C6-C7: Hydroelectric power (1x1) 255 | C8: Wind power (1x1) 256 | C9: Natural gas power plant (4x4) 257 | CA: Oil power plant (4x4) 258 | CB: Nuclear power plant (4x4) 259 | CC: Solar power plant (4x4) 260 | CD: Microwave power receiver (4x4) 261 | CE: Fusion power plant (4x4) 262 | CF: Coal power plant (4x4) 263 | City services: 264 | D0: City hall 265 | D1: Hospital 266 | D2: Police station 267 | D3: Fire station 268 | D4: Museum 269 | D5: Park (big) 270 | D6: School 271 | D7: Stadium 272 | D8: Prison 273 | D9: College 274 | DA: Zoo 275 | DB: Statue 276 | Seaports, airports, transportation, military bases, and more city services: 277 | DC: Water pump 278 | DD-DE: Runway 279 | DF: Pier 280 | E0: Crane 281 | E1-E2: Control tower 282 | E3: Warehouse (for seaport) 283 | E4-E5: Building (for airport) 284 | E6: Tarmac 285 | E7: F-15b 286 | E8: Hangar 287 | E9: Subway station 288 | EA: Radar 289 | EB: Water tower 290 | EC: Bus station 291 | ED: Rail station 292 | EE-EF: Parking lot 293 | F0: Loading bay 294 | F1: Top secret 295 | F2: Cargo yard 296 | F3: man (aka the mayor's house) 297 | F4: Water treatment plant 298 | F5: Library 299 | F6: Hangar 300 | F7: Church 301 | F8: Marina 302 | F9: Missile silo 303 | FA: Desalination plant 304 | Arcologies: 305 | FB: Plymouth arcology 306 | FC: Forest arcology 307 | FD: Darco arcology 308 | FE: Launch arcology 309 | Braun Llama-dome: 310 | FF: Braun Llama-dome 311 | 312 | XTER: One code byte per square. Tells whether there is land or water in 313 | the square and how the terrain slopes. To describe here how 314 | terrain slopes, we write four numbers in a square: 315 | 316 | a b 317 | c d 318 | 319 | Here, a, b, c and d are the relative heights of the corners of 320 | the square. 321 | 322 | Codes in hex: 323 | 324 | 00-0D: Dry land, with letious slopes: 325 | 00: 00 01: 11 02: 01 03: 00 04: 10 05: 11 326 | 00 00 01 11 10 01 327 | 06: 01 07: 10 08: 11 09: 01 0A: 00 0B: 00 328 | 11 11 10 00 01 10 329 | 0C: 10 0D: 11 330 | 00 11 331 | 0E-0F: Unused? 332 | 10-1D: Slopes as for 00-0D. However, instead of being dry land, 333 | the square is totally submerged in water. 334 | 1E-1F: Unused? 335 | 20-2D: As for 10-1D, but the square is submerged only to a level 336 | slightly less than height 1, so that for e.g. square type 21 337 | the square to the top would probably be dry land. 338 | 2E: Unused? 339 | 2F: Unused? 340 | 30-3D: As for 20-2D, but the square is not submerged at all; 341 | it just has water on its surface, as when you place it by 342 | the `water' tool in SimCity 2000. It is still true that 343 | for e.g. square type 31, the square to the top would be 344 | dry land and the other adjacent squares water. 345 | 3E: Waterfall. 346 | 3F: Unused? 347 | 40: Surf. water `canal', running left-right. Land at top and bottom. 348 | 41: Surf. water `canal', running top-bottom. Land at left and right. 349 | 42: Surf. water `bay', open to the bottom. Land at left, top & right. 350 | 43: Surf. water `bay', open to the left. Land at top, right & bottom. 351 | 44: Surf. water `bay', open to the top. Land at left, right & bottom. 352 | 45: Surf. water `bay', open to the bottom. Land at top, left & right. 353 | 46-FF: Unused? 354 | 355 | XUND: One code byte per square. Tells what's in the square under the 356 | ground. 357 | 358 | Codes in hex: 359 | 360 | 00: Nothing 361 | 01-0F: Subway, letious directions & slopes (coded as for XTER, codes 0E-1C) 362 | 10-1E: Pipes, letious directions & slopes (coded as for XTER, codes 0E-1C) 363 | 1F: Pipe/subway crossover: pipe top-bottom, subway left-right 364 | 20: Pipe/subway crossover: pipe left-right, subway top-bottom 365 | 21-22: ??? 366 | 23: Sub/rail or subway station 367 | 24-FF: Unused? 368 | 369 | XZON: One byte per square. Bits 7, 6, 5, and 4 should be set at the 370 | upper left, lower left, lower right, and upper right corners of a 371 | building. For a 1x1 building, of course, all these bits should be set 372 | at the location of the building. Bits 3-0 code the zoning for the 373 | square, as follows: 374 | 375 | 0: None 376 | 1: Light residential 377 | 2: Dense residential 378 | 3: Light commercial 379 | 4: Dense commercial 380 | 5: Light industrial 381 | 6: Dense industrial 382 | 7: Military base 383 | 8: Airport 384 | 9: Seaport 385 | 10-15: Unused? 386 | 387 | XTXT: One byte per square. It gives the number of the user-defined sign 388 | attached there, or the label used for a microsimulator/city-wide 389 | simulator pertaining to that square, as follows: 390 | 391 | Codes in hex: 392 | 393 | 00: No sign 394 | 01-32: User-attached signs (up to 50, decimal, of them) 395 | 33: ??? microsimulator 396 | 34: Bus system microsimulator 397 | 35: Rail system microsimulator 398 | 36: Sub system microsimulator 399 | 37: Wind power microsimulator 400 | 38: Hydro power microsimulator 401 | 39: Park system microsimulator 402 | 3A: Museum system microsimulator 403 | 3B: Library system microsimulator 404 | 3C: Marina system microsimulator 405 | 3D-C8: Other microsimulators (up to 140 of them here) 406 | (FOR: Police stations, fire stations, schools, zoos, stadiums, hospitals, 407 | prisons, colleges, power plants, water treatment plants, 408 | desalination plants, the mayor's house, and city hall) 409 | 410 | FA: Connection-to-neighbor-city sign 411 | FB: Football team name (default `Llamas') 412 | FC: Baseball team name (default `Alpacas') 413 | FD: Soccer team name (default `Camels') 414 | FE: Cricket team name (default `Dromedaries') 415 | FF: Rugby team name (default `Army ants') 416 | 417 | XLAB: 256 labels, taking up 25 bytes each. The label indices are as in XBIT. 418 | Label 00 is the mayor's name. Labels consist of a 1-byte count followed 419 | by the appropriate number of bytes of string, padded with 00's to 24 420 | bytes in length. 421 | 422 | XMIC: 150 8-byte records, one for each microsimulator. The first byte 423 | of each microsimulator record is the tile type it applies to. The 424 | first ten microsimulators correspond to the city-wide microsimulators 425 | (labels 33-3C.) The next 140 correspond to individual structure 426 | microsimulators (labels 3D-C8.) 427 | 428 | XTHG: Unknown contents. 480 bytes long. 429 | 430 | XGRP: Unknown contents. 3328 = 32*104 bytes long. 431 | 432 | MISC: Miscellaneous statistics. 1200 4-byte integers, which we call here #0 433 | through #1199. These integers are stored big-endian, i.e., most 434 | significant byte first, least significant byte last. The contents of 435 | a few of these integers: 436 | 437 | #3: Year of founding of the city. 438 | 439 | #4: Number of days that have elapsed since the founding. (In SimNation, 440 | all months have 25 days.) 441 | 442 | #5: Money supply 443 | (This is the location modified by the hex-edit cheat described in 444 | SIMCITY 2000: POWER, POLITICS, AND PLANNING. Now you know how it 445 | works!) 446 | 447 | #20: SimNation population (in 1,000s) 448 | 449 | #124-#379: Number of squares with a given tile (i.e., XBLD) type (from 00 to 450 | FF) 451 | 452 | #439: Neighbor city 1 population 453 | #443: Neighbor city 2 population 454 | #447: Neighbor city 3 population 455 | #451: Neighbor city 4 population 456 | 457 | The following appear to be statistical maps of the city: 458 | 459 | XPLC: Police power 460 | XFIR: Fire power 461 | XPOP: Population (?) 462 | XROG: Rate of growth of population (?) 463 | XPLT: Pollution (?) 464 | XVAL: Property values (?) 465 | XCRM: Crime rate 466 | XTRF: Traffic (?) 467 | -- 468 | David Moews dmoews@xraysgi.ims.uconn.edu 469 | --------------------------------------------------------------------------------