├── Icons └── icon_128x128.png ├── SC4Parser ├── packages.config ├── Structures │ ├── SimGrid.cs │ ├── OccupancyGroup.cs │ ├── DatabaseDirectoryResource.cs │ ├── IndexEntry.cs │ ├── DatabasePackedFileHeader.cs │ ├── TypeGroupInstance.cs │ ├── Lot.cs │ ├── SaveGameProperty.cs │ ├── Building.cs │ └── BridgeNetworkTile.cs ├── Logging │ ├── LogLevels.cs │ ├── ILogger.cs │ ├── Logger.cs │ ├── ConsoleLogger.cs │ └── FileLogger.cs ├── SC4Parser.csproj ├── Utils.cs ├── Extensions.cs ├── Files │ └── DatabaseDirectoryFile.cs ├── SubFiles │ ├── BridgeNetworkSubfile.cs │ ├── LotSubFile.cs │ ├── BuildingSubFile.cs │ ├── NetworkSubfile2.cs │ ├── NetworkSubfile1.cs │ ├── TerrainMapSubfile.cs │ ├── PrebuiltNetworkSubfile.cs │ ├── ItemIndexSubfile.cs │ ├── RegionViewSubfile.cs │ └── NetworkIndex.cs ├── Exceptions.cs ├── Region │ ├── Region.cs │ └── Bitmap.cs ├── Compression │ └── QFS.cs └── Constants.cs ├── LICENSE ├── SC4Parser.sln ├── .gitignore └── README.md /Icons/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Killeroo/SC4Parser/HEAD/Icons/icon_128x128.png -------------------------------------------------------------------------------- /SC4Parser/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SC4Parser/Structures/SimGrid.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace SC4Parser.Structures 6 | { 7 | //https://wiki.sc4devotion.com/index.php?title=SimGrid 8 | public class SimGrid 9 | { 10 | public uint Size { get; private set; } 11 | public uint CRC; 12 | public uint MemoryAddress; 13 | public uint MajorVersion; 14 | public uint TypeId; 15 | public uint DataId; 16 | public uint Resolution; 17 | public uint ResolutionPower; 18 | public uint SizeX; 19 | public uint SizeY; 20 | public object[][] Values; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SC4Parser/Logging/LogLevels.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace SC4Parser.Logging 3 | { 4 | /// 5 | /// Log levels used in log output 6 | /// 7 | public enum LogLevel 8 | { 9 | /// 10 | /// Debug messages 11 | /// 12 | Debug, 13 | /// 14 | /// General messages 15 | /// 16 | Info, 17 | /// 18 | /// Warning messages 19 | /// 20 | Warning, 21 | /// 22 | /// Error messages 23 | /// 24 | Error, 25 | /// 26 | /// Fatal error messages 27 | /// 28 | Fatal 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Matthew Carney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SC4Parser.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32328.378 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SC4Parser", "SC4Parser\SC4Parser.csproj", "{9D36649C-5BAD-4C7B-8503-E82A2E20FE2A}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {9D36649C-5BAD-4C7B-8503-E82A2E20FE2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {9D36649C-5BAD-4C7B-8503-E82A2E20FE2A}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {9D36649C-5BAD-4C7B-8503-E82A2E20FE2A}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {9D36649C-5BAD-4C7B-8503-E82A2E20FE2A}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {80EECD54-387B-4989-9FB2-BBEA17331D08} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /SC4Parser/Structures/OccupancyGroup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SC4Parser 4 | { 5 | public class OccupancyGroup 6 | { 7 | public int Index { get; private set; } 8 | public uint GroupId { get; private set; } 9 | public uint Population { get; private set; } 10 | public string Name { get; private set; } 11 | 12 | /// 13 | /// Constructs a Occupancy group 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// Intended to be used as part of the RegionView Subfile 20 | public OccupancyGroup(int index, string name, uint groupId, uint population) 21 | { 22 | Index = index; 23 | GroupId = groupId; 24 | Population = population; 25 | Name = name; 26 | } 27 | 28 | public void Dump() 29 | { 30 | Console.WriteLine("Index={0} Name=\"{1}\" GroupId=0x{2} Population={3}", 31 | Index, 32 | Name, 33 | GroupId.ToString("x2"), 34 | Population); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SC4Parser/Logging/ILogger.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace SC4Parser.Logging 3 | { 4 | /// 5 | /// Logger interface, used to create new logging implementations that can be used to print out 6 | /// internal logging from the library 7 | /// 8 | /// 9 | /// See ConsoleLogger to see how the logging interface can be implemented 10 | /// 11 | /// 12 | /// 13 | public interface ILogger 14 | { 15 | /// 16 | /// Enable a log channel to be included in log output 17 | /// 18 | /// Log level to be enabled 19 | /// 20 | /// 21 | /// // Enable any message using Debug log level to show up in log's output 22 | /// myLogger.EnableChannel(LogLevel.Debug); 23 | /// 24 | /// 25 | /// 26 | void EnableChannel(LogLevel level); 27 | 28 | void DisableChannel(LogLevel level); 29 | 30 | /// 31 | /// Log a message 32 | /// 33 | /// Message log level 34 | /// Format of message 35 | /// Message arguments 36 | /// 37 | /// 38 | /// myLogger.Log(LogLevel.Error, "This is a test log message it can include {0} {1} {2}", 39 | /// "strings!", 40 | /// 123, 41 | /// "Or any other type you want to pass!" 42 | /// ); 43 | /// 44 | /// 45 | /// 46 | void Log(LogLevel level, string format, params object[] args); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SC4Parser/SC4Parser.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | True 7 | https://github.com/Killeroo/SC4Parser 8 | https://github.com/Killeroo/SC4Parser 9 | git 10 | simcity;simcity4;sc4;parser;savegame;save;maxis;qfs;dbpf;parser;databasepackedfile;simcity 4;save game; 11 | LICENSE 12 | 1.1.5.0 13 | 1.1.5.0 14 | SC4Parser 15 | Matthew Carney 16 | Copyright (c) Matthew Carney 2023 17 | 1.1.5 18 | 19 | SC4Parser is a general purpose library for parsing, finding, and loading files from Simcity 4 save game files (Maxis Database Packed Files (DBPF)). 20 | 21 | The library was mainly intended for extracting items, from save games. It contains some partial implementation of specific Simcity 4 subfiles but mainly provides solid structures for loading data from save game files. 22 | README.md 23 | en-GB 24 | icon_128x128.png 25 | - Included xml documentation in package 26 | 27 | 28 | 29 | 30 | True 31 | \ 32 | 33 | 34 | True 35 | 36 | 37 | 38 | True 39 | \ 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /SC4Parser/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | using SC4Parser.Logging; 5 | 6 | namespace SC4Parser 7 | { 8 | class Utils 9 | { 10 | /// 11 | /// Converts datetime to unixtime used as timestamps in saves 12 | /// 13 | /// Unix timestamp to convert 14 | /// Converted Datatime 15 | /// 16 | /// Based on https://stackoverflow.com/a/250400 17 | /// 18 | /// Could use DateTimeOffset.FromUnixTimeSeconds from .NET 4.6 > but thought it was new enough 19 | /// That I would ensure a bit of backwards compatability 20 | /// 21 | public static DateTime UnixTimestampToDateTime(long unixTimestamp) 22 | { 23 | DateTime unixDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc); 24 | DateTime convertedDateTime = unixDateTime.AddSeconds(unixTimestamp); 25 | return convertedDateTime; 26 | } 27 | 28 | /// 29 | /// Save a byte array to a file 30 | /// 31 | /// Data to save 32 | /// Name of file to save 33 | /// Path to save file to 34 | public static void SaveByteArrayToFile(byte[] data, string path, string name) 35 | { 36 | try 37 | { 38 | // Write buffer to specified path 39 | using (FileStream stream = new FileStream(Path.Combine(path, name), FileMode.OpenOrCreate)) 40 | { 41 | stream.Write(data, 0, data.Length); 42 | } 43 | 44 | Logger.Log(LogLevel.Info, "Byte array (size {0} bytes) written to path: {1}", 45 | data.Length, 46 | Path.Combine(path, name)); 47 | } 48 | catch (Exception e) 49 | { 50 | Logger.Log(LogLevel.Error, "Exception ({0}) occured while trying to save byte array to file {1}. msg={2} trace={3}", 51 | e.GetType().ToString(), 52 | Path.Combine(path), 53 | e.Message, 54 | e.StackTrace); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SC4Parser/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SC4Parser 4 | { 5 | /// 6 | /// Class for extension and helper methods 7 | /// 8 | public static class Extensions 9 | { 10 | /// 11 | /// Helper method for reading and copying bytes to a new array while updating the offset 12 | /// with the number of bytes read 13 | /// 14 | /// 15 | /// This method of reading bytes is not used everywhere in the code, only in Network subfile parsing 16 | /// 17 | /// data to read from 18 | /// number of bytes to read 19 | /// offset to read from and update 20 | /// New array with data copied from the source byte array 21 | /// 22 | /// Thrown when trying to read from data outside of the bounds of the byte array 23 | /// 24 | /// 25 | public static byte[] ReadBytes(byte[] buffer, uint count, ref uint offset) 26 | { 27 | byte[] readData = new byte[count]; 28 | Buffer.BlockCopy(buffer, (int) offset, readData, 0, (int) count); 29 | offset += count; 30 | return readData; 31 | } 32 | 33 | /// 34 | /// Helper method for reading a single byte from an array while updating an offset 35 | /// 36 | /// 37 | /// This method of reading bytes is not used everywhere in the code, only in Network subfile parsing 38 | /// 39 | /// data to read from 40 | /// offset to read from and update 41 | /// New array with data copied from the source byte array 42 | /// 43 | /// Thrown when trying to read from data outside of the bounds of the byte array 44 | /// 45 | /// 46 | public static byte ReadByte(byte[] buffer, ref uint offset) 47 | { 48 | byte data = buffer[offset]; 49 | offset++; 50 | return data; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SC4Parser/Structures/DatabaseDirectoryResource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using SC4Parser.Logging; 4 | 5 | namespace SC4Parser 6 | { 7 | /// 8 | /// Implementation of a Database Directory Resource (DIR record). 9 | /// A Database Directory Resource represents a compressed file within a SimCity 4 savegame (DBPF) 10 | /// The uncompressed size of the record can be used to determine if a file has been decompressed properly. 11 | /// 12 | /// 13 | /// Implemented from https://wiki.sc4devotion.com/index.php?title=DBDF 14 | /// 15 | /// 16 | public class DatabaseDirectoryResource 17 | { 18 | /// 19 | /// TypeGroupInstance (TGI) of resource 20 | /// 21 | /// 22 | public TypeGroupInstance TGI { get; private set; } 23 | /// 24 | /// Decompressed size of resource's file 25 | /// 26 | public uint DecompressedFileSize { get; private set; } 27 | 28 | /// 29 | /// Reads an individual resource from a byte array 30 | /// 31 | /// Data to load resource from 32 | /// 33 | /// Byte array given to method should only contain the data for one resource 34 | /// 35 | /// 36 | /// Thrown when trying to parse an element that is out of bounds in the data array 37 | /// 38 | public void Parse(byte[] buffer) 39 | { 40 | if (buffer.Length < 16) 41 | { 42 | Logger.Log(LogLevel.Warning, "DatabaseDirectoryResource is too small to parse"); 43 | return; 44 | } 45 | 46 | TGI = new TypeGroupInstance( 47 | BitConverter.ToUInt32(buffer, 0), 48 | BitConverter.ToUInt32(buffer, 4), 49 | BitConverter.ToUInt32(buffer, 8) 50 | ); 51 | DecompressedFileSize = BitConverter.ToUInt32(buffer, 12); 52 | } 53 | 54 | /// 55 | /// Prints out the values of a resource 56 | /// 57 | public void Dump() 58 | { 59 | Console.WriteLine("TypeID: {0}", TGI.Type.ToString("x8")); 60 | Console.WriteLine("GroupID: {0}", TGI.Group.ToString("x8")); 61 | Console.WriteLine("InstanceID: {0}", TGI.Instance.ToString("x8")); 62 | Console.WriteLine("Decompressed File Size: {0} bytes", DecompressedFileSize); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /SC4Parser/Files/DatabaseDirectoryFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SC4Parser 5 | { 6 | /// 7 | /// Represents a DatabaseDirectoryfile (DBDF or DIR file) 8 | /// 9 | /// A DBDF (not to be confused with DBPF, thanks maxis...) is an IndexEntry within a SimCity 4 savegame (DBPF) that holds a list of 10 | /// all compressed files (DatabaseDirectoryResources) within a save game. 11 | /// 12 | /// The TypegroupInstance of a DBDF is always E86B1EEF E86B1EEF 286B1F03 in a save 13 | /// 14 | /// 15 | /// Implemented from https://wiki.sc4devotion.com/index.php?title=DBDF 16 | /// 17 | /// 18 | public class DatabaseDirectoryFile : IndexEntry 19 | { 20 | /// 21 | /// List of all compressed resources in save 22 | /// 23 | /// 24 | public List Resources { get; private set; } = new List(); 25 | /// 26 | /// Number of resources in file 27 | /// 28 | public uint ResourceCount { get; private set; } 29 | 30 | /// 31 | /// Default constructor for DatabaseDirectoryFile 32 | /// 33 | public DatabaseDirectoryFile() { } 34 | /// 35 | /// Constructor for a DatabaseDirectoryFile that uses it's 36 | /// associated IndexEntry 37 | /// 38 | /// 39 | public DatabaseDirectoryFile(IndexEntry entry) 40 | { 41 | TGI = entry.TGI; 42 | FileLocation = entry.FileLocation; 43 | FileSize = entry.FileSize; 44 | 45 | ResourceCount = FileSize / 16; 46 | Resources = new List(); 47 | } 48 | 49 | /// 50 | /// Adds a Database Directory Resource to Database Directory File's (DBDF) Resources 51 | /// 52 | /// Resource to add 53 | /// 54 | public void AddResource(DatabaseDirectoryResource resource) 55 | { 56 | Resources.Add(resource); 57 | } 58 | 59 | /// 60 | /// Prints out the contents of DBDF 61 | /// 62 | public override void Dump() 63 | { 64 | base.Dump(); 65 | 66 | foreach (DatabaseDirectoryResource resource in Resources) 67 | { 68 | Console.WriteLine("--------------"); 69 | resource.Dump(); 70 | } 71 | } 72 | } 73 | 74 | 75 | } 76 | -------------------------------------------------------------------------------- /SC4Parser/Logging/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SC4Parser.Logging 5 | { 6 | /// 7 | /// Static logger class, used to pass log messages from library components to any attached/implemented log interfaces 8 | /// 9 | public static class Logger 10 | { 11 | private static List logOutputs = new List(); 12 | 13 | /// 14 | /// Add a logger interface to send log output to 15 | /// 16 | /// Logger interface to add 17 | /// 18 | /// 19 | /// 20 | /// MyOwnLogger myLogger = new MyOwnLogger(); 21 | /// Logger.AddLogOutput(myLogger); 22 | /// 23 | /// // Your logger will now be used as an output for any log message.. 24 | /// 25 | /// 26 | /// 27 | public static void AddLogOutput(ILogger logOutput) 28 | { 29 | logOutputs.Add(logOutput); 30 | } 31 | 32 | /// 33 | /// Enable a log level on all log outputs 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// // Enable any message using Debug log level to show up in all logging outputs 39 | /// Logger.EnableChannel(LogLevel.Debug); 40 | /// 41 | /// 42 | /// 43 | public static void EnableLogChannel(LogLevel level) 44 | { 45 | foreach (var output in logOutputs) 46 | { 47 | output.EnableChannel(level); 48 | } 49 | } 50 | 51 | /// 52 | /// Log a message 53 | /// 54 | /// Message log level 55 | /// Format of message 56 | /// Message arguments 57 | /// 58 | /// 59 | /// 60 | /// Logger.Log(LogLevel.Error, "This is a test log message it can include {0} {1} {2}", 61 | /// "strings!", 62 | /// 123, 63 | /// "Or any other type you want to pass!" 64 | /// ); 65 | /// 66 | /// 67 | /// 68 | public static void Log(LogLevel level, string format, params object[] args) 69 | { 70 | // Send log message to each output 71 | foreach (var output in logOutputs) 72 | { 73 | output.Log(level, format, args); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /SC4Parser/SubFiles/BridgeNetworkSubfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using SC4Parser.Logging; 5 | 6 | namespace SC4Parser 7 | { 8 | /// 9 | /// Implementation of the Bridge Network Subfile. This file contains all bridge network tiles in a city. 10 | /// 11 | /// 12 | /// 13 | public class BridgeNetworkSubfile 14 | { 15 | /// 16 | /// Contains all network tiles in the bridge network subfile 17 | /// 18 | /// 19 | public List NetworkTiles { get; private set; } = new List(); 20 | 21 | /// 22 | /// Reads bridge network subfile from a byte array 23 | /// 24 | /// Data to read subfile from 25 | /// Size of the subfile 26 | /// 27 | /// Thrown when trying to parse an element that is out of bounds in the data array 28 | /// 29 | public void Parse(byte[] buffer, int size) 30 | { 31 | Logger.Log(LogLevel.Info, "Parsing Bridge Network Subfile..."); 32 | 33 | uint bytesToRead = Convert.ToUInt32(size); 34 | uint offset = 0; 35 | 36 | // Loop through each byte in the file 37 | while (bytesToRead > 0) 38 | { 39 | // Get the current tile size 40 | // (each tile is stored one after another in the file with the size of tile at the beginning) 41 | uint recordSize = BitConverter.ToUInt32(buffer, (int)offset); 42 | 43 | // Copy tile data to it's own array 44 | byte[] tileBuffer = new byte[recordSize]; 45 | Array.Copy(buffer, offset, tileBuffer, 0, (int)recordSize); 46 | 47 | // Parse the tile and add it to the list of tiles 48 | BridgeNetworkTile tile = new BridgeNetworkTile(); 49 | tile.Parse(tileBuffer, 0); 50 | NetworkTiles.Add(tile); 51 | 52 | // Record how much we have read and how far we have gone and move on 53 | // (deep.. again...) 54 | offset += recordSize; 55 | bytesToRead -= recordSize; 56 | 57 | Logger.Log(LogLevel.Debug, $"Network tile read ({recordSize}) got {bytesToRead}/{size} bytes left"); 58 | } 59 | 60 | if (bytesToRead != 0) 61 | { 62 | Logger.Log(LogLevel.Warning, $"Not all network tiles read from Bridge Network Subfile ({bytesToRead} left)"); 63 | } 64 | 65 | Logger.Log(LogLevel.Info, "Bridge Network Subfile parsed"); 66 | } 67 | 68 | /// 69 | /// Prints out the contents of the subfile 70 | /// 71 | public void Dump() 72 | { 73 | foreach (var tile in NetworkTiles) 74 | { 75 | Console.WriteLine("--------------------"); 76 | tile.Dump(); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /SC4Parser/SubFiles/LotSubFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using SC4Parser.Logging; 5 | 6 | namespace SC4Parser 7 | { 8 | /// 9 | /// Implementation of the Lots Subfile. Lot Subfile contains all logs in a SimCity 4 savegame (Partial implementation). 10 | /// 11 | /// 12 | /// The implementation of the lots is only partially complete and will not contain all data associated with the lots 13 | /// 14 | /// Actual reading of individual builds is done in DataStructure\Lot.cs 15 | /// 16 | /// Implemented from https://wiki.sc4devotion.com/index.php?title=Lot_Subfile 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// // Simple usage 23 | /// // (Just assume the lot subfile has already been read, see SC4SaveGame.GetLotSubfile()) 24 | /// 25 | /// // Access a lot 26 | /// Lot firstLot = lotSubfile.Lots.First(); 27 | /// 28 | /// // Do something with it 29 | /// firstLot.Dump(); 30 | /// 31 | /// 32 | public class LotSubfile 33 | { 34 | /// 35 | /// All lots stored in the subfile 36 | /// 37 | /// 38 | public List Lots = new List(); 39 | 40 | /// 41 | /// Reads the Lots Subfile from byte array 42 | /// 43 | /// Data to read subfile from 44 | /// Size of data to be read 45 | /// 46 | /// Thrown when trying to parse an element that is out of bounds in the data array 47 | /// 48 | public void Parse(byte[] buffer, int size) 49 | { 50 | Logger.Log(LogLevel.Info, "Parsing Lot subfile..."); 51 | 52 | uint bytesToRead = Convert.ToUInt32(size); 53 | uint offset = 0; 54 | 55 | while (bytesToRead > 0) 56 | { 57 | uint currentSize = BitConverter.ToUInt32(buffer, (int)offset); 58 | 59 | Lot lot = new Lot(); 60 | byte[] b = new byte[currentSize]; 61 | Array.Copy(buffer, offset, b, 0, (int)currentSize); 62 | lot.Parse(b, offset); 63 | Lots.Add(lot); 64 | 65 | offset += currentSize; 66 | bytesToRead -= currentSize; 67 | 68 | Logger.Log(LogLevel.Debug, $"lot read ({currentSize} bytes), offset {offset} got {bytesToRead}/{size} bytes left"); 69 | } 70 | 71 | if (bytesToRead != 0) 72 | { 73 | Logger.Log(LogLevel.Warning, "Not all lots have been read from lot subfile (" + bytesToRead + " bytes left)"); 74 | } 75 | 76 | Logger.Log(LogLevel.Info, "Lot subfile parsed"); 77 | } 78 | 79 | /// 80 | /// Prints out the contents of the Lot Subfile 81 | /// 82 | public void Dump() 83 | { 84 | foreach (Lot lot in Lots) 85 | { 86 | Console.WriteLine("--------------------"); 87 | lot.Dump(); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /SC4Parser/Logging/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SC4Parser.Logging 5 | { 6 | /// 7 | /// Console Logger implementation, logs output to standard output 8 | /// 9 | /// 10 | /// 11 | /// // Setup logger 12 | /// // This will automatically add it to list of log outputs 13 | /// ConsoleLogger logger = new ConsoleLogger(); 14 | /// 15 | /// // Run some operations and generate some logs 16 | /// 17 | /// // Load save game 18 | /// SC4SaveFile savegame; 19 | /// try 20 | /// { 21 | /// savegame = new SC4SaveFile(@"C:\Path\To\Save\Game.sc4"); 22 | /// } 23 | /// catch (DBPFParsingException) 24 | /// { 25 | /// Console.Writeline("Issue occured while parsing DBPF"); 26 | /// return; 27 | /// } 28 | /// 29 | /// TerrainMapSubfile terrainMap = null 30 | /// try 31 | /// { 32 | /// terrainMap = savegame.GetTerrainMapSubfile(); 33 | /// } 34 | /// catch (SubfileNotFoundException) 35 | /// { 36 | /// Console.Writeline("Could not find subfile"); 37 | /// } 38 | /// 39 | /// 40 | public class ConsoleLogger : ILogger 41 | { 42 | private static List EnabledChannels = new List 43 | { 44 | LogLevel.Info, 45 | LogLevel.Warning, 46 | LogLevel.Error, 47 | LogLevel.Fatal 48 | }; 49 | 50 | private static readonly Dictionary LogLevelText = new Dictionary 51 | { 52 | { LogLevel.Debug, "DEBUG" }, 53 | { LogLevel.Info, "INFO" }, 54 | { LogLevel.Warning, "WARNING" }, 55 | { LogLevel.Error, "ERROR" }, 56 | { LogLevel.Fatal, "FATAL" } 57 | }; 58 | 59 | private static readonly Dictionary LogLevelColors = new Dictionary 60 | { 61 | { LogLevel.Debug, ConsoleColor.DarkGray }, 62 | { LogLevel.Info, ConsoleColor.White }, 63 | { LogLevel.Warning, ConsoleColor.Yellow }, 64 | { LogLevel.Error, ConsoleColor.Red }, 65 | { LogLevel.Fatal, ConsoleColor.Magenta } 66 | }; 67 | 68 | public ConsoleLogger() 69 | { 70 | Logger.AddLogOutput(this); 71 | } 72 | 73 | public void EnableChannel(LogLevel level) 74 | { 75 | EnabledChannels.Add(level); 76 | } 77 | 78 | public void DisableChannel(LogLevel level) 79 | { 80 | if (EnabledChannels.Contains(level)) 81 | { 82 | EnabledChannels.Remove(level); 83 | } 84 | } 85 | 86 | public void Log(LogLevel level, string format, params object[] args) 87 | { 88 | if (EnabledChannels.Contains(level) == false) 89 | return; 90 | 91 | string message = args.Length == 0 ? format : string.Format(format, args); 92 | message = string.Format("[{0}] [{1}] {2}", 93 | DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss.ff"), 94 | LogLevelText[level], 95 | message); 96 | 97 | Console.ForegroundColor = LogLevelColors[level]; 98 | Console.WriteLine(message); 99 | Console.ResetColor(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /SC4Parser/SubFiles/BuildingSubFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using SC4Parser.Logging; 5 | 6 | namespace SC4Parser 7 | { 8 | /// 9 | /// Implmentation of the Building Subfile. Building subfile stores all building data in a SimCity 4 savegame. 10 | /// 11 | /// 12 | /// Actual reading of individual builds is done in DataStructure\Buildings.cs 13 | /// 14 | /// Implemented from https://wiki.sc4devotion.com/index.php?title=Building_Subfile 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// // Simple usage 21 | /// // (Just assume the building subfile has already been read, see SC4SaveGame.GetBuildingSubfile()) 22 | /// 23 | /// // Access a building 24 | /// Building firstBuilding = buildingSubfile.Buildings.First(); 25 | /// 26 | /// // Do something with it 27 | /// firstBuilding.Dump(); 28 | /// 29 | /// 30 | public class BuildingSubfile 31 | { 32 | /// 33 | /// Stores all building in the subfile 34 | /// 35 | /// 36 | public List Buildings { get; private set; } = new List(); 37 | 38 | /// 39 | /// Reads the Building Subfile from a byte array 40 | /// 41 | /// Data to read subfile from 42 | /// Size of data that is being read 43 | /// 44 | /// Thrown when trying to parse an element that is out of bounds in the data array 45 | /// 46 | public void Parse(byte[] buffer, int size) 47 | { 48 | Logger.Log(LogLevel.Info, "Parsing Building subfile..."); 49 | 50 | uint bytesToRead = Convert.ToUInt32(size); 51 | uint offset = 0; 52 | 53 | while (bytesToRead > 0) 54 | { 55 | uint currentSize = BitConverter.ToUInt32(buffer, (int) offset); 56 | 57 | // Read building at current position 58 | Building building = new Building(); 59 | byte[] buildingBuffer = new byte[currentSize]; 60 | Array.Copy(buffer, offset, buildingBuffer, 0, (int)currentSize); 61 | building.Parse(buildingBuffer, offset); 62 | Buildings.Add(building); 63 | 64 | // Update offset and bytes read and move on 65 | offset += currentSize; 66 | bytesToRead -= currentSize; 67 | 68 | Logger.Log(LogLevel.Debug, $"building read ({currentSize} bytes), offset {offset} got {bytesToRead}/{size} bytes left"); 69 | } 70 | 71 | if (bytesToRead != 0) 72 | { 73 | Logger.Log(LogLevel.Warning, "Not all building have been read from Building Subfile (" + bytesToRead + " bytes left)"); 74 | } 75 | 76 | Logger.Log(LogLevel.Info, "Parsed Building subfile"); 77 | } 78 | 79 | /// 80 | /// Prints out the contents of the Building Subfile 81 | /// 82 | public void Dump() 83 | { 84 | foreach (Building building in Buildings) 85 | { 86 | Console.WriteLine("--------------------"); 87 | building.Dump(); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /SC4Parser/Structures/IndexEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using SC4Parser.Logging; 4 | 5 | namespace SC4Parser 6 | { 7 | /// 8 | /// Implementation of an Index Entry in a save game 9 | /// 10 | /// An Index Entry represents a file stored within a SimCity 4 savegame (DBPF). 11 | /// It stores the TGI (identifier), the location of the file with in the savegame and the size of the file. 12 | /// 13 | /// 14 | /// Implemented from https://wiki.sc4devotion.com/index.php?title=DBPF#DBPF_1.x.2C_Index_Table_7.0 15 | /// 16 | public class IndexEntry 17 | { 18 | /// 19 | /// TypeGroupInstance (TGI) of Index entry 20 | /// 21 | /// 22 | public TypeGroupInstance TGI { get; protected set; } 23 | /// 24 | /// Location of the file in the DBPF that the index entry refers to 25 | /// 26 | public uint FileLocation { get; protected set; } 27 | /// 28 | /// The size of the index entry's file 29 | /// 30 | public uint FileSize { get; protected set; } 31 | 32 | /// 33 | /// Default constructor 34 | /// 35 | public IndexEntry() { } 36 | /// 37 | /// Constructor meant for copying over an Index Entry to a new object 38 | /// 39 | /// Entry to copy over to new object 40 | public IndexEntry(IndexEntry entry) 41 | { 42 | TGI = entry.TGI; 43 | FileLocation = entry.FileLocation; 44 | FileSize = entry.FileSize; 45 | } 46 | /// 47 | /// Constructor for manually constructing an Index Entry without parsing it 48 | /// 49 | /// TypeGroupInstance (TGI) of Index Entry 50 | /// File location of Index Entry 51 | /// File size of Index Entry 52 | public IndexEntry(TypeGroupInstance tgi, uint location, uint size) 53 | { 54 | TGI = tgi; 55 | FileLocation = location; 56 | FileSize = size; 57 | } 58 | 59 | /// 60 | /// Loads an individual entry from a byte array 61 | /// 62 | /// Data to load the index entry from 63 | /// 64 | /// Buffer should only contain data for a single entry 65 | /// 66 | /// 67 | /// Thrown when trying to parse an element that is out of bounds in the data array 68 | /// 69 | public void Parse(byte[] buffer) 70 | { 71 | if (buffer.Length < 20) 72 | { 73 | Logger.Log(LogLevel.Warning, "IndexEntry buffer is too small to parse"); 74 | return; 75 | } 76 | 77 | TGI = new TypeGroupInstance( 78 | BitConverter.ToUInt32(buffer, 0), 79 | BitConverter.ToUInt32(buffer, 4), 80 | BitConverter.ToUInt32(buffer, 8) 81 | ); 82 | FileLocation = BitConverter.ToUInt32(buffer, 12); 83 | FileSize = BitConverter.ToUInt32(buffer, 16); 84 | } 85 | 86 | /// 87 | /// Dumps the contents of an entry 88 | /// 89 | public virtual void Dump() 90 | { 91 | Console.WriteLine("TypeID: {0}", TGI.Type.ToString("x8")); 92 | Console.WriteLine("GroupID: {0}", TGI.Group.ToString("x8")); 93 | Console.WriteLine("InstanceID: {0}", TGI.Instance.ToString("x8")); 94 | Console.WriteLine("File Location: {0}", FileLocation); 95 | Console.WriteLine("File Size: {0}", FileSize); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SC4Parser/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SC4Parser 4 | { 5 | /// 6 | /// Exception thrown when an Index Entry cannot be found 7 | /// 8 | /// 9 | public class IndexEntryNotFoundException : Exception { } 10 | /// 11 | /// Exception thrown when Database Directory (DBDF) Resource cannot be found 12 | /// 13 | /// 14 | public class DatabaseDirectoryResourceNotFoundException : Exception { } 15 | /// 16 | /// Exception thrown when Subfile cannot be found 17 | /// 18 | /// 19 | /// Inner exception contains specific exception that occured 20 | /// 21 | public class SubfileNotFoundException : Exception 22 | { 23 | /// 24 | /// Constructor that constructs with an exception message 25 | /// 26 | /// Exception message 27 | public SubfileNotFoundException(string message) 28 | : base(message) { } 29 | /// 30 | /// Construcotr that constructs with an exception message and an inner exception 31 | /// 32 | /// Exception message 33 | /// Inner exception 34 | public SubfileNotFoundException(string message, Exception e) 35 | : base(message, e) { } 36 | } 37 | /// 38 | /// Exception thrown when there are issues parsing a Database Packed File (DBPF) 39 | /// 40 | /// 41 | /// Inner exception contains specific exception that occured 42 | /// 43 | /// 44 | public class DBPFParsingException : Exception 45 | { 46 | /// 47 | /// Construcotr that constructs with an exception message and an inner exception 48 | /// 49 | /// Exception message 50 | /// Inner exception 51 | public DBPFParsingException(string message, Exception e) 52 | : base(message, e) { } 53 | } 54 | /// 55 | /// Exception thrown when there is an issue with loading an Index Entry 56 | /// 57 | /// 58 | /// Inner exception contains specific exception that occured 59 | /// 60 | /// 61 | public class IndexEntryLoadingException : Exception 62 | { 63 | /// 64 | /// Constructor that constructs with an exception message 65 | /// 66 | /// Exception message 67 | public IndexEntryLoadingException(string message) 68 | : base(message) { } 69 | /// 70 | /// Construcotr that constructs with an exception message and an inner exception 71 | /// 72 | /// Exception message 73 | /// Inner exception 74 | public IndexEntryLoadingException(string message, Exception e) 75 | : base (message, e) { } 76 | } 77 | /// 78 | /// Exception thrown when an error occurs while preforming a QFS/Refpack decompression 79 | /// 80 | /// 81 | /// Inner exception contains specific exception that occured 82 | /// 83 | /// 84 | public class QFSDecompressionException : Exception 85 | { 86 | /// 87 | /// Construcotr that constructs with an exception message and an inner exception 88 | /// 89 | /// Exception message 90 | /// Inner exception 91 | public QFSDecompressionException(string message, Exception e) 92 | : base (message, e) { } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /SC4Parser/SubFiles/NetworkSubfile2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using SC4Parser.Logging; 5 | 6 | namespace SC4Parser 7 | { 8 | /// 9 | /// Implementation of Network Subfile 2. Network subfile 2 seems to contain all the network tiles that are below (Subways). 10 | /// 11 | /// 12 | /// Actual implementation of tiles found in this file can be found in DataStructure\NetworkTile2.cs 13 | /// 14 | /// Implemented and references additional data from https://wiki.sc4devotion.com/index.php?title=Network_Subfiles. 15 | /// 16 | /// 17 | /// 18 | public class NetworkSubfile2 19 | { 20 | /// 21 | /// Contains all network tiles in the network subfile 22 | /// 23 | /// 24 | public List NetworkTiles { get; private set; } = new List(); 25 | 26 | /// 27 | /// Read network subfile 2 from a byte array 28 | /// 29 | /// Data to read subfile from 30 | /// Size of the subfile 31 | /// 32 | /// Thrown when trying to parse an element that is out of bounds in the data array 33 | /// 34 | public void Parse(byte[] buffer, int size) 35 | { 36 | Logger.Log(LogLevel.Info, "Parsing Network Subfile 2..."); 37 | 38 | uint bytesToRead = Convert.ToUInt32(size); 39 | uint offset = 0; 40 | 41 | // Loop through each byte in the subfile 42 | while (bytesToRead > 0) 43 | { 44 | // Work out the current tile size 45 | // (each tile is stored one after another in the file with the size of tile at the beginning) 46 | uint recordSize = BitConverter.ToUInt32(buffer, (int)offset); 47 | 48 | // Copy tile data out into it's own array 49 | byte[] tileBuffer = new byte[recordSize]; 50 | Array.Copy(buffer, offset, tileBuffer, 0, (int)recordSize); 51 | 52 | // Parse and add to list 53 | NetworkTile2 tile = new NetworkTile2(); 54 | tile.Parse(tileBuffer, 0); 55 | NetworkTiles.Add(tile); 56 | 57 | // Record how much we have read and how far we have gone and move on 58 | // (deep) 59 | offset += recordSize; 60 | bytesToRead -= recordSize; 61 | 62 | Logger.Log(LogLevel.Debug, $"Network tile read ({recordSize}) got {bytesToRead}/{size} bytes left"); 63 | } 64 | 65 | if (bytesToRead != 0) 66 | { 67 | Logger.Log(LogLevel.Warning, $"Not all network tiles read from Network Subfile 2 ({bytesToRead} left)"); 68 | } 69 | 70 | Logger.Log(LogLevel.Info, "Network Subfile 2 parsed"); 71 | } 72 | 73 | /// 74 | /// Checks to see if a tile with a given memory address is present in the file 75 | /// 76 | /// Memory address to look for 77 | /// Tile that has the given memory address, null if nothing is found 78 | public NetworkTile2 FindTile(uint memoryReference) 79 | { 80 | foreach (var tile in NetworkTiles) 81 | { 82 | if (tile.Memory == memoryReference) 83 | { 84 | Console.WriteLine("found reference"); 85 | return tile; 86 | } 87 | } 88 | 89 | return null; 90 | } 91 | 92 | /// 93 | /// Prints out the contents of the subfile 94 | /// 95 | public void Dump() 96 | { 97 | foreach (var tile in NetworkTiles) 98 | { 99 | Console.WriteLine("--------------------"); 100 | tile.Dump(); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /SC4Parser/SubFiles/NetworkSubfile1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using SC4Parser.Logging; 5 | 6 | namespace SC4Parser 7 | { 8 | /// 9 | /// Implementation of Network Subfile 1. Network subfile 1 seems to contain all the network tiles in a city that are at ground level. 10 | /// 11 | /// 12 | /// Actual implementation of tiles found in this file can be found in DataStructure\NetworkTile1.cs 13 | /// 14 | /// Implemented and references additional data from https://wiki.sc4devotion.com/index.php?title=Network_Subfiles. 15 | /// 16 | /// 17 | /// 18 | public class NetworkSubfile1 19 | { 20 | /// 21 | /// Contains all network tiles in the network subfile 22 | /// 23 | /// 24 | public List NetworkTiles { get; private set; } = new List(); 25 | 26 | /// 27 | /// Read network subfile 1 from a byte array 28 | /// 29 | /// Data to read subfile from 30 | /// Size of the subfile 31 | /// 32 | /// Thrown when trying to parse an element that is out of bounds in the data array 33 | /// 34 | public void Parse(byte[] buffer, int size) 35 | { 36 | Logger.Log(LogLevel.Info, "Parsing Network Subfile 1..."); 37 | 38 | uint bytesToRead = Convert.ToUInt32(size); 39 | uint offset = 0; 40 | 41 | // Loop through each byte in the subfile 42 | while (bytesToRead > 0) 43 | { 44 | // Work out the current tile size 45 | // (each tile is stored one after another in the file with the size of tile at the beginning) 46 | uint recordSize = BitConverter.ToUInt32(buffer, (int)offset); 47 | 48 | // Copy tile data out into it's own array 49 | byte[] tileBuffer = new byte[recordSize]; 50 | Array.Copy(buffer, offset, tileBuffer, 0, (int)recordSize); 51 | 52 | // Parse and add to list 53 | NetworkTile1 tile = new NetworkTile1(); 54 | tile.Parse(tileBuffer, 0); 55 | NetworkTiles.Add(tile); 56 | 57 | // Record how much we have read and how far we have gone and move on 58 | // (deep) 59 | offset += recordSize; 60 | bytesToRead -= recordSize; 61 | 62 | Logger.Log(LogLevel.Debug, $"Network tile read ({recordSize}) got {bytesToRead}/{size} bytes left"); 63 | } 64 | 65 | if (bytesToRead != 0) 66 | { 67 | Logger.Log(LogLevel.Warning, $"Not all network tiles read from Network Subfile 1 ({bytesToRead} left)"); 68 | } 69 | 70 | Logger.Log(LogLevel.Info, "Network Subfile 1 parsed"); 71 | } 72 | 73 | /// 74 | /// Checks to see if a tile with a given memory address is present in the file 75 | /// 76 | /// Memory address to look for 77 | /// Tile that has the given memory address, null if nothing is found 78 | public NetworkTile1 FindTile(uint memoryReference) 79 | { 80 | foreach (var tile in NetworkTiles) 81 | { 82 | if (tile.Memory == memoryReference) 83 | { 84 | Console.WriteLine("found reference"); 85 | return tile; 86 | } 87 | } 88 | 89 | return null; 90 | } 91 | 92 | /// 93 | /// Prints out the contents of the subfile 94 | /// 95 | public void Dump() 96 | { 97 | foreach (var tile in NetworkTiles) 98 | { 99 | Console.WriteLine("--------------------"); 100 | tile.Dump(); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /SC4Parser/SubFiles/TerrainMapSubfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using SC4Parser.Logging; 4 | 5 | namespace SC4Parser 6 | { 7 | /// 8 | /// Implementation of TerrainMap Subfile, contains height information for each tile of a city. 9 | /// 10 | /// 11 | /// Based off the implmentation here: 12 | /// https://github.com/sebamarynissen/sc4/blob/master/lib/terrain-map.js 13 | /// 14 | /// 15 | /// 16 | /// // Simple usage 17 | /// // (Just assume the terrain map subfile has already been read, see SC4SaveGame.GetTerrainMapSubfile()) 18 | /// 19 | /// // print out terrain map 20 | /// foreach (float x in terrainMapSubfile.Map) 21 | /// { 22 | /// foreach (float y in terrainMapSubfile.Map[x]) 23 | /// { 24 | /// Console.WriteLine(terrainMapSubfile.Map[x][y]); 25 | /// } 26 | /// } 27 | /// 28 | /// 29 | public class TerrainMapSubfile 30 | { 31 | /// 32 | /// Major version of the subfile 33 | /// 34 | public ushort MajorVersion { get; private set; } 35 | /// 36 | /// X size of the city 37 | /// 38 | /// 39 | /// Not included in actual file but borrowed from Region View Subfile for convience 40 | /// 41 | public uint SizeX { get; private set; } 42 | /// 43 | /// Y size of the city 44 | /// 45 | /// 46 | /// Not included in actual file but borrowed from Region View Subfile for convience 47 | /// 48 | public uint SizeY { get; private set; } 49 | /// 50 | /// Actual terrain map, contains a height value for each tile in the sity 51 | /// 52 | /// 53 | /// Stored in x and y of tiles 54 | /// 55 | public float[][] Map { get; private set; } 56 | 57 | /// 58 | /// Reads the Terrain Map Subfile from a byte array 59 | /// 60 | /// Data to read subfile from 61 | /// Number of tiles on the X axis in the city 62 | /// Number of tiles on the Y axis in the city 63 | /// 64 | /// Thrown when trying to parse an element that is out of bounds in the data array 65 | /// 66 | public void Parse(byte[] buffer, uint xSize, uint ySize) 67 | { 68 | SizeX = xSize + 1; 69 | SizeY = ySize + 1; 70 | 71 | Logger.Log(LogLevel.Info, "Parsing TerrainMap subfile..."); 72 | 73 | MajorVersion = BitConverter.ToUInt16(buffer, 0); 74 | 75 | // Loop through rest of file and save to out array 76 | int offset = 2; 77 | Map = new float[SizeX][]; 78 | for (int x = 0; x < SizeX; x++) 79 | { 80 | Map[x] = new float[SizeY]; 81 | for (int y = 0; y < SizeY; y++) 82 | { 83 | Map[x][y] = BitConverter.ToSingle(buffer, offset); 84 | offset += 4; 85 | Logger.Log(LogLevel.Debug, "Terrain segment [{0},{1}] read, offset {2} got {3}/{4} bytes left", 86 | x, 87 | y, 88 | offset, 89 | offset, 90 | buffer.Length); 91 | } 92 | } 93 | 94 | Logger.Log(LogLevel.Info, "TerrainMap subfile parsed"); 95 | 96 | } 97 | 98 | /// 99 | /// Prints out the contents of the Terrain Map Subfile 100 | /// 101 | public void Dump() 102 | { 103 | Console.WriteLine("Major Version: {0}", MajorVersion); 104 | Console.WriteLine("Terrain Map Data:"); 105 | for (int x = 0; x < SizeX; x++) 106 | { 107 | for (int y = 0; y < SizeY; y++) 108 | { 109 | Console.Write(Map[x][y] + " "); 110 | } 111 | Console.WriteLine(); 112 | } 113 | 114 | } 115 | 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /SC4Parser/SubFiles/PrebuiltNetworkSubfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using SC4Parser.Logging; 5 | 6 | namespace SC4Parser 7 | { 8 | /// 9 | /// Implementation of the Prebuilt Network Subfile. This file appears to contain all files with prefabricated models (e.g. elevated highways and monorail). 10 | /// 11 | /// 12 | /// Actual implementation of tiles found in this file can be found in DataStructure\PrebuiltNetworkTile.cs 13 | /// 14 | /// Implemented and references additional data from https://wiki.sc4devotion.com/index.php?title=Network_Subfiles. 15 | /// 16 | /// 17 | /// 18 | public class PrebuiltNetworkSubfile 19 | { 20 | /// 21 | /// Contains all network tiles in the network subfile 22 | /// 23 | /// 24 | public List NetworkTiles { get; private set; } = new List(); 25 | 26 | /// 27 | /// Read network subfile 1 from a byte array 28 | /// 29 | /// Data to read subfile from 30 | /// Size of the subfile 31 | /// 32 | /// Thrown when trying to parse an element that is out of bounds in the data array 33 | /// 34 | public void Parse(byte[] buffer, int size) 35 | { 36 | Logger.Log(LogLevel.Info, "Parsing Prebuilt Network subfile..."); 37 | 38 | uint bytesToRead = Convert.ToUInt32(size); 39 | uint offset = 0; 40 | 41 | // Loop through each byte in the subfile 42 | while (bytesToRead > 0) 43 | { 44 | // Work out the current tile size 45 | // (each tile is stored one after another in the file with the size of tile at the beginning) 46 | uint recordSize = BitConverter.ToUInt32(buffer, (int)offset); 47 | 48 | // Copy tile data out into it's own array 49 | byte[] tileBuffer = new byte[recordSize]; 50 | Array.Copy(buffer, offset, tileBuffer, 0, (int)recordSize); 51 | 52 | // Parse and add to list 53 | PrebuiltNetworkTile tile = new PrebuiltNetworkTile(); 54 | tile.Parse(tileBuffer, 0); 55 | NetworkTiles.Add(tile); 56 | 57 | // Record how much we have read and how far we have gone and move on 58 | // (deep) 59 | offset += recordSize; 60 | bytesToRead -= recordSize; 61 | 62 | Logger.Log(LogLevel.Debug, $"Network tile read ({recordSize}) got {bytesToRead}/{size} bytes left"); 63 | } 64 | 65 | if (bytesToRead != 0) 66 | { 67 | Logger.Log(LogLevel.Warning, $"Not all network tiles read from Prebuilt Network subfile ({bytesToRead} left)"); 68 | } 69 | 70 | Logger.Log(LogLevel.Info, "Prebuilt Network Subfile parsed"); 71 | } 72 | 73 | /// 74 | /// Checks to see if a tile with a given memory address is present in the file 75 | /// 76 | /// Memory address to look for 77 | /// Tile that has the given memory address, null if nothing is found 78 | public PrebuiltNetworkTile FindTile(uint memoryReference) 79 | { 80 | foreach (var tile in NetworkTiles) 81 | { 82 | if (tile.Memory == memoryReference) 83 | { 84 | Console.WriteLine("found reference"); 85 | return tile; 86 | } 87 | } 88 | 89 | return null; 90 | } 91 | 92 | /// 93 | /// Prints out the contents of the subfile 94 | /// 95 | public void Dump() 96 | { 97 | foreach (var tile in NetworkTiles) 98 | { 99 | Console.WriteLine("--------------------"); 100 | tile.Dump(); 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /SC4Parser/SubFiles/ItemIndexSubfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace SC4Parser 6 | { 7 | public class ItemIndexSubfile 8 | { 9 | public struct Item 10 | { 11 | public uint MemoryAddress { get; internal set; } 12 | public uint SubfileTypeId { get; internal set; } 13 | } 14 | 15 | public uint Size { get; private set; } 16 | public uint CRC { get; private set; } 17 | public uint MemoryAddress { get; private set; } 18 | public ushort MajorVersion { get; private set; } 19 | public float CityWidthMeters { get; private set; } 20 | public float CityDepthMeters { get; private set; } 21 | public uint CityWidthTracts { get; private set; } 22 | public uint CityDepthTracts { get; private set; } 23 | public uint CityWidthTiles { get; private set; } 24 | public uint CityDepthTiles { get; private set; } 25 | public uint CellColumnCount { get; private set; } 26 | public uint CellRowCount { get; private set; } 27 | public List[][] Items { get; private set; } 28 | 29 | public void Parse(byte[] buffer, int size) 30 | { 31 | using (MemoryStream stream = new MemoryStream(buffer)) 32 | using (BinaryReader reader = new BinaryReader(stream)) 33 | { 34 | Size = reader.ReadUInt32(); 35 | CRC = reader.ReadUInt32(); 36 | MemoryAddress = reader.ReadUInt32(); 37 | MajorVersion = reader.ReadUInt16(); 38 | CityWidthMeters = reader.ReadSingle(); 39 | CityDepthMeters = reader.ReadSingle(); 40 | CityWidthTracts = reader.ReadUInt32(); 41 | CityDepthTracts = reader.ReadUInt32(); 42 | CityWidthTiles = reader.ReadUInt32(); 43 | CityDepthTiles = reader.ReadUInt32(); 44 | CellColumnCount = reader.ReadUInt32(); 45 | 46 | Items = new List[CellColumnCount][]; 47 | for (int x = 0; x < CellColumnCount; x++) 48 | { 49 | CellRowCount = reader.ReadUInt32(); 50 | Items[x] = new List[CellRowCount]; 51 | for (int y = 0; y < CellRowCount; y++) 52 | { 53 | uint cellItemCount = reader.ReadUInt32(); 54 | 55 | Items[x][y] = new List(); 56 | if (cellItemCount > 0) 57 | { 58 | for (int i = 0; i < cellItemCount; i++) 59 | { 60 | Items[x][y].Add(new Item() 61 | { 62 | MemoryAddress = reader.ReadUInt32(), 63 | SubfileTypeId = reader.ReadUInt32() 64 | }); 65 | } 66 | } 67 | 68 | } 69 | } 70 | } 71 | } 72 | 73 | public void Dump() 74 | { 75 | Console.WriteLine("Size: {0} bytes", Size); 76 | Console.WriteLine("CRC: 0x{0}", CRC); 77 | Console.WriteLine("Memory Address: 0x{0}", MemoryAddress.ToString("X8")); 78 | Console.WriteLine("Major Version: {0}", MajorVersion); 79 | Console.WriteLine("City Width (Meters): {0}", CityWidthMeters); 80 | Console.WriteLine("City Depth (Meters): {0}", CityDepthMeters); 81 | Console.WriteLine("City Width (Tracts): {0}", CityWidthTracts); 82 | Console.WriteLine("City Depth (Tracts): {0}", CityDepthTracts); 83 | Console.WriteLine("City Width (Tiles): {0}", CityWidthTiles); 84 | Console.WriteLine("City Depth (Tiles): {0}", CityDepthTiles); 85 | Console.WriteLine("Cell Column Count: {0}", CellColumnCount); 86 | Console.WriteLine("Cell Row Count: {0}", CellRowCount); 87 | for (int x = 0; x < CellColumnCount; x++) 88 | { 89 | for (int y = 0; y < CellRowCount; y++) 90 | { 91 | foreach (Item i in Items[x][y]) 92 | { 93 | Console.WriteLine("[{2},{3}] MemoryAddress=0x{0} Subfile=0x{1}", 94 | i.MemoryAddress.ToString("X8"), 95 | i.SubfileTypeId.ToString("X8"), 96 | x, 97 | y); 98 | } 99 | } 100 | } 101 | } 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /SC4Parser/Logging/FileLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Collections.Generic; 4 | 5 | namespace SC4Parser.Logging 6 | { 7 | /// 8 | /// File Logger implementation, logs output to a file in a temp directory 9 | /// 10 | /// 11 | /// 12 | /// // Setup logger 13 | /// // This will automatically add it to list of log outputs 14 | /// FileLogger logger = new FileLogger(); 15 | /// 16 | /// // Check if the log file was created properly 17 | /// if (logger.Created == false) 18 | /// { 19 | /// Console.WriteLine("Log file could not be created"); 20 | /// return; 21 | /// } 22 | /// else 23 | /// { 24 | /// // Print out log location 25 | /// Console.WriteLine("Created log at {0}", logger.LogPath); 26 | /// } 27 | /// 28 | /// // Run some operations and generate some logs 29 | /// 30 | /// // Load save game 31 | /// SC4SaveFile savegame; 32 | /// try 33 | /// { 34 | /// savegame = new SC4SaveFile(@"C:\Path\To\Save\Game.sc4"); 35 | /// } 36 | /// catch (DBPFParsingException) 37 | /// { 38 | /// Console.Writeline("Issue occured while parsing DBPF"); 39 | /// return; 40 | /// } 41 | /// 42 | /// 43 | /// 44 | class FileLogger : ILogger 45 | { 46 | private static List EnabledChannels = new List 47 | { 48 | LogLevel.Info, 49 | LogLevel.Warning, 50 | LogLevel.Error, 51 | LogLevel.Fatal 52 | }; 53 | 54 | private static readonly Dictionary LogLevelText = new Dictionary 55 | { 56 | { LogLevel.Debug, "DEBUG" }, 57 | { LogLevel.Info, "INFO" }, 58 | { LogLevel.Warning, "WARNING" }, 59 | { LogLevel.Error, "ERROR" }, 60 | { LogLevel.Fatal, "FATAL" } 61 | }; 62 | 63 | public string LogPath { get; private set; } = ""; 64 | public bool Created = false; 65 | 66 | public FileLogger() 67 | { 68 | try 69 | { 70 | string logDirectory = Path.Combine(Path.GetTempPath(), "SC4Parser"); 71 | 72 | // Create log folder if it does not already exist 73 | if (Directory.Exists(logDirectory) == false) 74 | { 75 | Directory.CreateDirectory(logDirectory); 76 | } 77 | 78 | // Generate a log path 79 | LogPath = Path.Combine( 80 | logDirectory, 81 | string.Format("{0}-log--{1}.txt", "SC4Parser", DateTime.Now.ToString("HH-mm-ss--dd-MMM-yyy"))); 82 | 83 | // Attempt to create the file 84 | File.Create(LogPath); 85 | } 86 | catch (Exception e) 87 | { 88 | Logger.Log(LogLevel.Fatal, "Encountered fatal exception trying to setup FileLogger ({0}:{1})", 89 | e.GetType().ToString(), 90 | e.Message); 91 | return; 92 | } 93 | 94 | Created = true; 95 | Logger.AddLogOutput(this); 96 | } 97 | 98 | public void EnableChannel(LogLevel level) 99 | { 100 | EnabledChannels.Add(level); 101 | } 102 | 103 | public void DisableChannel(LogLevel level) 104 | { 105 | if (EnabledChannels.Contains(level)) 106 | { 107 | EnabledChannels.Remove(level); 108 | } 109 | } 110 | 111 | public void Log(LogLevel level, string format, params object[] args) 112 | { 113 | if (EnabledChannels.Contains(level) == false) 114 | return; 115 | 116 | string message = args.Length == 0 ? format : string.Format(format, args); 117 | message = string.Format("[{0}] [{1}] {2}", 118 | DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss.ff"), 119 | LogLevelText[level], 120 | message); 121 | 122 | // Attempt to write data to log file 123 | try 124 | { 125 | File.AppendAllText(LogPath, message + Environment.NewLine); 126 | } 127 | catch (Exception) 128 | { 129 | // Causes cyclical error as it keeps calling itself to log the message which fails 130 | //Logger.Log(LogLevel.Error, "Encountered exception while trying to write to log file ({0}:{1})", 131 | // e.GetType().ToString(), 132 | // e.Message); 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /SC4Parser/Region/Region.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Drawing; 6 | 7 | using SC4Parser.Logging; 8 | 9 | namespace SC4Parser.Region 10 | { 11 | public class Region 12 | { 13 | public List Buildings { get; private set; } = new List(); 14 | public List Lots { get; private set; } = new List(); 15 | public float[][] Terrain { get; private set; } 16 | 17 | public float GridCountX { get; private set; } 18 | public int GridCountY { get; private set; } 19 | 20 | public Dictionary CityNameDictionary { get; private set; } 21 | 22 | public string[][] RegionLayout; 23 | // TODO: Save an offset for building locations etc 24 | 25 | public void Load(string path) 26 | { 27 | string configImagePath = Path.Combine(path, "config.bmp"); 28 | if (!File.Exists(configImagePath)) 29 | { 30 | Logger.Log(LogLevel.Error, "Could not find config.bmp file for region @ {0}", path); 31 | return; 32 | } 33 | 34 | // Parse the region bitmap 35 | // TODO: Wrap in try 36 | Bitmap regionBitmap = new Bitmap(configImagePath); 37 | 38 | // Construct an empty region map 39 | RegionLayout = new string[regionBitmap.DiBHeader.Height][]; 40 | for (int y = 0; y < regionBitmap.DiBHeader.Height; y++) 41 | { 42 | RegionLayout[y] = new string[regionBitmap.DiBHeader.Width]; 43 | for (int x = 0; x < regionBitmap.DiBHeader.Width; x++) 44 | { 45 | // Leave this blank for now 46 | RegionLayout[y][x] = String.Empty; 47 | } 48 | } 49 | 50 | // Create blank terrain map for region 51 | Terrain = new float[regionBitmap.DiBHeader.Height * 64][]; 52 | for (int y = 0; y < regionBitmap.DiBHeader.Height * 64; y++) 53 | { 54 | Terrain[y] = new float[regionBitmap.DiBHeader.Width * 64]; 55 | } 56 | 57 | // Save region grid size 58 | // TODO: Verify region size and trim empty parts of terrin map 59 | // (only from the ends) 60 | GridCountY = regionBitmap.DiBHeader.Height * 64; 61 | GridCountX = regionBitmap.DiBHeader.Width * 64; 62 | 63 | // Get all save games at path 64 | int tilesProcessed = 0; 65 | SC4SaveFile save; 66 | foreach (string file in Directory.GetFiles(path, "*.sc4")) 67 | { 68 | using (save = new SC4SaveFile(file)) 69 | { 70 | // TODO: Access manually 71 | RegionViewSubfile regionView = save.GetRegionViewSubfile(); 72 | 73 | // Fill out the the tiles it occupies in the region map 74 | int tileCount = (int)regionView.CitySizeY / 64; 75 | for (int y = (int)regionView.TileYLocation; y < regionView.TileYLocation + tileCount; y++) 76 | { 77 | for (int x = (int)regionView.TileXLocation; x < regionView.TileXLocation + tileCount; x++) 78 | { 79 | RegionLayout[y][x] = Path.GetFileNameWithoutExtension(save.FilePath); 80 | tilesProcessed++; 81 | } 82 | } 83 | 84 | // Ok first add the terrain data to the correct part of the region terrain map 85 | float[][] cityTerrainMap = save.GetTerrainMapSubfile().Map; 86 | for (int x = 0; x < regionView.CitySizeX; x++) 87 | { 88 | for (int y = 0; y < regionView.CitySizeY; y++) 89 | { 90 | Terrain[(64 * (int)regionView.TileYLocation) + y][(64 * (int)regionView.TileXLocation) + x] = cityTerrainMap[y][x]; 91 | } 92 | } 93 | 94 | // Finally add the lots and buildings to region list (if they are present in city) 95 | if (save.ContainsLotSubfile()) 96 | { 97 | foreach (Lot lot in save.GetLotSubfile().Lots) 98 | { 99 | // Offset the lots by their position in the region 100 | lot.MinTileX += (64 * (int)regionView.TileXLocation); 101 | lot.MaxTileX += (64 * (int)regionView.TileXLocation); 102 | lot.MinTileZ += (64 * (int)regionView.TileYLocation); 103 | lot.MaxTileZ += (64 * (int)regionView.TileYLocation); 104 | 105 | Lots.Add(lot); 106 | } 107 | 108 | } 109 | 110 | // TODO: Do the same for buildings 111 | if (save.ContainsBuildingsSubfile()) 112 | { 113 | Buildings.AddRange(save.GetBuildingSubfile().Buildings); ; 114 | } 115 | 116 | } 117 | } 118 | 119 | // Check if we processed everything 120 | if (tilesProcessed != regionBitmap.DiBHeader.Width * regionBitmap.DiBHeader.Height) 121 | { 122 | Logger.Log(LogLevel.Warning, "Didn't process all tiles in config.bmp, region might not be properly constructed ({0}/{1} processed)", 123 | tilesProcessed, 124 | regionBitmap.DiBHeader.Width * regionBitmap.DiBHeader.Height); 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /SC4Parser/Structures/DatabasePackedFileHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | using SC4Parser.Logging; 5 | 6 | namespace SC4Parser 7 | { 8 | /// 9 | /// Header file for a Database Packed File (DBPF). 10 | /// Implements version 1.0 of the DBPF header used for SimCity 4 11 | /// 12 | /// 13 | /// Implemented from https://wiki.sc4devotion.com/index.php?title=DBPF#Header 14 | /// 15 | /// 16 | public class DatabasePackedFileHeader 17 | { 18 | /// 19 | /// File identifier (always 'DBPF') 20 | /// 21 | public string Identifier { get; private set; } 22 | /// 23 | /// DBPF major version 24 | /// 25 | /// 26 | /// Common DBPF versions: 27 | /// 1.0 seen in Sim City 4, The Sims 2 28 | /// 1.1 seen in The Sims 2 29 | /// 2.0 seen in Spore, The Sims 3 30 | /// 3.0 seen in SimCity 31 | /// 32 | public uint MajorVersion { get; private set; } 33 | /// 34 | /// DBPF minor version 35 | /// 36 | /// 37 | /// Common DBPF versions: 38 | /// 1.0 seen in Sim City 4, The Sims 2 39 | /// 1.1 seen in The Sims 2 40 | /// 2.0 seen in Spore, The Sims 3 41 | /// 3.0 seen in SimCity 42 | /// 43 | public uint MinorVersion { get; private set; } 44 | /// 45 | /// Date DBPF file was created 46 | /// 47 | /// 48 | /// In Unix time stamp format 49 | /// 50 | public DateTime DateCreated { get; private set; } 51 | /// 52 | /// Date DBPF file was modified 53 | /// 54 | /// 55 | /// In Unix time stamp format 56 | /// 57 | public DateTime DateModified { get; private set; } 58 | /// 59 | /// Index table major version 60 | /// 61 | /// 62 | /// Always 7 in The Sims 2, Sim City 4. If this is used in 2.0, then it is 0 for SPORE. 63 | /// 64 | public uint IndexMajorVersion { get; private set; } 65 | /// 66 | /// Number of Index Entries in Index table 67 | /// 68 | public uint IndexCount { get; private set; } 69 | /// 70 | /// Position of first Index Entry in the DBPF file 71 | /// 72 | public uint FirstIndexOffset { get; private set; } 73 | /// 74 | /// Size of index table in bytes 75 | /// 76 | public uint IndexSize { get; private set; } 77 | /// 78 | /// Number of hole entries in Hole Record 79 | /// 80 | public uint HoleCount { get; private set; } 81 | /// 82 | /// Location of Hole Record in the DBPF file 83 | /// 84 | public uint HoleOffset { get; private set; } 85 | /// 86 | /// size of the Hold Record 87 | /// 88 | public uint HoleSize { get; private set; } 89 | /// 90 | /// Index table minor version 91 | /// 92 | public uint IndexMinorVersion { get; private set; } 93 | 94 | /// 95 | /// Reads a DBPF header from a byte array 96 | /// 97 | /// Data to read header from 98 | /// 99 | /// Thrown when trying to parse an element that is out of bounds in the data array 100 | /// 101 | public void Parse(byte[] buffer) 102 | { 103 | if (buffer.Length < 96) 104 | { 105 | Logger.Log(LogLevel.Error, "DBPF header buffer is too small to parse ({0}/{1} bytes)", buffer.Length, 96); 106 | return; 107 | } 108 | 109 | // Read header contents 110 | Identifier = Encoding.ASCII.GetString(buffer, 0, 4); 111 | MajorVersion = BitConverter.ToUInt32(buffer, 4); 112 | MinorVersion = BitConverter.ToUInt32(buffer, 8); 113 | DateCreated = Utils.UnixTimestampToDateTime(BitConverter.ToUInt32(buffer, 24)); 114 | DateModified = Utils.UnixTimestampToDateTime(BitConverter.ToUInt32(buffer, 28)); 115 | IndexMajorVersion = BitConverter.ToUInt32(buffer, 32); 116 | IndexCount = BitConverter.ToUInt32(buffer, 36); 117 | FirstIndexOffset = BitConverter.ToUInt32(buffer, 40); 118 | IndexSize = BitConverter.ToUInt32(buffer, 44); 119 | HoleCount = BitConverter.ToUInt32(buffer, 48); 120 | HoleOffset = BitConverter.ToUInt32(buffer, 52); 121 | HoleSize = BitConverter.ToUInt32(buffer, 56); 122 | IndexMinorVersion = BitConverter.ToUInt32(buffer, 60); 123 | } 124 | 125 | /// 126 | /// Dumps contents of header 127 | /// 128 | public void Dump() 129 | { 130 | Console.WriteLine("Identifier: {0}", Identifier); 131 | Console.WriteLine("Major Version: {0}", MajorVersion); 132 | Console.WriteLine("Minor Version: {0}", MinorVersion); 133 | Console.WriteLine("Date Created: {0}", DateCreated); 134 | Console.WriteLine("Date Modified: {0}", DateModified); 135 | Console.WriteLine("Index Major Version: {0}", IndexMajorVersion); 136 | Console.WriteLine("Index Minor Version: {0}", IndexMinorVersion); 137 | Console.WriteLine("Index Entry Count: {0}", IndexCount); 138 | Console.WriteLine("First Index Offset: {0}", FirstIndexOffset); 139 | Console.WriteLine("Index Table Size: {0} bytes", IndexSize); 140 | Console.WriteLine("Hole Count: {0}", HoleCount); 141 | Console.WriteLine("Hole Offset: {0}", HoleOffset); 142 | Console.WriteLine("Hole Size: {0} bytes", HoleSize); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /SC4Parser/Region/Bitmap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | using SC4Parser.Logging; 7 | 8 | namespace SC4Parser.Region 9 | { 10 | /// 11 | /// Very simple Bitmap implementation, parses specifically the type of bitmap used by SimCity 4 12 | /// 13 | /// 14 | /// https://upload.wikimedia.org/wikipedia/commons/7/75/BMPfileFormat.svg 15 | /// (non compressed, BITMAPINFOHEADER) 16 | /// 17 | /// 18 | internal class Bitmap 19 | { 20 | internal static class EndinessHelper 21 | { 22 | public static bool IsLittleEndian => BitConverter.IsLittleEndian; 23 | 24 | public static UInt16 ReverseBytes(UInt16 value) 25 | { 26 | return (ushort)((value >> 8) + (value << 8)); 27 | } 28 | 29 | public static UInt32 ReverseBytes(UInt32 value) 30 | { 31 | return (value & 0x000000FFU) << 24 | (value & 0x0000FF00U) << 8 | 32 | (value & 0x00FF0000U) >> 8 | (value & 0xFF000000U) >> 24; 33 | } 34 | } 35 | 36 | public class FileHeader 37 | { 38 | public ushort Signature { get; private set; } 39 | public uint FileSize { get; private set; } 40 | public uint PixelArrayOffset { get; private set; } 41 | 42 | public FileHeader(BinaryReader reader) 43 | { 44 | Parse(reader); 45 | } 46 | 47 | public void Parse(BinaryReader reader) 48 | { 49 | Signature = reader.ReadUInt16(); 50 | FileSize = reader.ReadUInt32(); 51 | reader.BaseStream.Position += 4; // Skip reserved bytes 52 | PixelArrayOffset = reader.ReadUInt32(); 53 | 54 | // Header is little endian, reserve it we need to 55 | // TODO: Does it make sense to check the endianness of machine? 56 | if (EndinessHelper.IsLittleEndian) 57 | { 58 | Signature = EndinessHelper.ReverseBytes(Signature); 59 | FileSize = EndinessHelper.ReverseBytes(FileSize); 60 | PixelArrayOffset = EndinessHelper.ReverseBytes(PixelArrayOffset); 61 | } 62 | } 63 | 64 | public void Dump() 65 | { 66 | Console.WriteLine("Signature: 0x{0}", Signature.ToString("X2")); 67 | Console.WriteLine("File Size: 0x{0}", FileSize.ToString("X8")); 68 | Console.WriteLine("Pixel Array Offset: 0x{0}", PixelArrayOffset.ToString("X8")); 69 | } 70 | } 71 | 72 | public class InformationHeader 73 | { 74 | public uint HeaderSize { get; private set; } 75 | public int Width { get; private set; } 76 | public int Height { get; private set; } 77 | public ushort ColorPlanes { get; private set; } 78 | public ushort BitPerPixel { get; private set; } 79 | public uint Compression { get; private set; } 80 | 81 | public InformationHeader(BinaryReader reader) 82 | { 83 | Parse(reader); 84 | } 85 | 86 | public void Parse(BinaryReader reader) 87 | { 88 | HeaderSize = reader.ReadUInt32(); 89 | if (HeaderSize != 40) 90 | { 91 | Logger.Log(LogLevel.Error, "Can't parse non BITMAPINFOHEADER bitmap, this bitmap was probably not produced by SimCity"); 92 | return; 93 | } 94 | Width = reader.ReadInt32(); 95 | Height = reader.ReadInt32(); 96 | ColorPlanes = reader.ReadUInt16(); 97 | BitPerPixel = reader.ReadUInt16(); 98 | Compression = reader.ReadUInt32(); 99 | if (Compression != 0) 100 | { 101 | Logger.Log(LogLevel.Error, "Can't parse bitmap with compression, this bitmap was probably not produced by SimCity"); 102 | return; 103 | } 104 | reader.BaseStream.Position += 20; // Skip over the rest of the header, we don't care 105 | } 106 | 107 | public void Dump() 108 | { 109 | Console.WriteLine("HeaderSize: {0}", HeaderSize); 110 | Console.WriteLine("Width: {0}", Width); 111 | Console.WriteLine("Height: {0}", Height); 112 | Console.WriteLine("BitPerPixel: {0}", BitPerPixel); 113 | Console.WriteLine("Compression: {0}", Compression); 114 | } 115 | } 116 | 117 | public struct RGB 118 | { 119 | public byte R { get; private set; } 120 | public byte G { get; private set; } 121 | public byte B { get; private set; } 122 | 123 | public static RGB Parse(BinaryReader reader) 124 | { 125 | return new RGB() 126 | { 127 | B = reader.ReadByte(), 128 | G = reader.ReadByte(), 129 | R = reader.ReadByte(), 130 | }; 131 | } 132 | 133 | public string ToString() 134 | { 135 | return string.Format("{0},{1},{2}", R, G, B); 136 | } 137 | } 138 | 139 | public FileHeader Header { get; private set; } 140 | public InformationHeader DiBHeader { get; private set; } 141 | public RGB[][] PixelMap { get; private set; } 142 | 143 | public Bitmap(string path) 144 | { 145 | using (MemoryStream stream = new MemoryStream(File.ReadAllBytes(path))) 146 | using (BinaryReader reader = new BinaryReader(stream)) 147 | { 148 | // Parse headers 149 | Header = new FileHeader(reader); 150 | DiBHeader = new InformationHeader(reader); 151 | 152 | // Parse pixels 153 | PixelMap = new RGB[DiBHeader.Height][]; 154 | for (int y = DiBHeader.Height - 1; y >= 0; y--) 155 | { 156 | PixelMap[y] = new RGB[DiBHeader.Width]; 157 | for (int x = 0; x < DiBHeader.Width; x++) 158 | { 159 | PixelMap[y][x] = RGB.Parse(reader); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/csharp 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=csharp 4 | 5 | ### Csharp ### 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.rsuser 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Aa][Rr][Mm]/ 32 | [Aa][Rr][Mm]64/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | [Ll]ogs/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*[.json, .xml, .info] 147 | 148 | # Visual Studio code coverage results 149 | *.coverage 150 | *.coveragexml 151 | 152 | # NCrunch 153 | _NCrunch_* 154 | .*crunch*.local.xml 155 | nCrunchTemp_* 156 | 157 | # MightyMoose 158 | *.mm.* 159 | AutoTest.Net/ 160 | 161 | # Web workbench (sass) 162 | .sass-cache/ 163 | 164 | # Installshield output folder 165 | [Ee]xpress/ 166 | 167 | # DocProject is a documentation generator add-in 168 | DocProject/buildhelp/ 169 | DocProject/Help/*.HxT 170 | DocProject/Help/*.HxC 171 | DocProject/Help/*.hhc 172 | DocProject/Help/*.hhk 173 | DocProject/Help/*.hhp 174 | DocProject/Help/Html2 175 | DocProject/Help/html 176 | 177 | # Click-Once directory 178 | publish/ 179 | 180 | # Publish Web Output 181 | *.[Pp]ublish.xml 182 | *.azurePubxml 183 | # Note: Comment the next line if you want to checkin your web deploy settings, 184 | # but database connection strings (with potential passwords) will be unencrypted 185 | *.pubxml 186 | *.publishproj 187 | 188 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 189 | # checkin your Azure Web App publish settings, but sensitive information contained 190 | # in these scripts will be unencrypted 191 | PublishScripts/ 192 | 193 | # NuGet Packages 194 | *.nupkg 195 | # NuGet Symbol Packages 196 | *.snupkg 197 | # The packages folder can be ignored because of Package Restore 198 | **/[Pp]ackages/* 199 | # except build/, which is used as an MSBuild target. 200 | !**/[Pp]ackages/build/ 201 | # Uncomment if necessary however generally it will be regenerated when needed 202 | #!**/[Pp]ackages/repositories.config 203 | # NuGet v3's project.json files produces more ignorable files 204 | *.nuget.props 205 | *.nuget.targets 206 | 207 | # Microsoft Azure Build Output 208 | csx/ 209 | *.build.csdef 210 | 211 | # Microsoft Azure Emulator 212 | ecf/ 213 | rcf/ 214 | 215 | # Windows Store app package directories and files 216 | AppPackages/ 217 | BundleArtifacts/ 218 | Package.StoreAssociation.xml 219 | _pkginfo.txt 220 | *.appx 221 | *.appxbundle 222 | *.appxupload 223 | 224 | # Visual Studio cache files 225 | # files ending in .cache can be ignored 226 | *.[Cc]ache 227 | # but keep track of directories ending in .cache 228 | !?*.[Cc]ache/ 229 | 230 | # Others 231 | ClientBin/ 232 | ~$* 233 | *~ 234 | *.dbmdl 235 | *.dbproj.schemaview 236 | *.jfm 237 | *.pfx 238 | *.publishsettings 239 | orleans.codegen.cs 240 | 241 | # Including strong name files can present a security risk 242 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 243 | #*.snk 244 | 245 | # Since there are multiple workflows, uncomment next line to ignore bower_components 246 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 247 | #bower_components/ 248 | 249 | # RIA/Silverlight projects 250 | Generated_Code/ 251 | 252 | # Backup & report files from converting an old project file 253 | # to a newer Visual Studio version. Backup files are not needed, 254 | # because we have git ;-) 255 | _UpgradeReport_Files/ 256 | Backup*/ 257 | UpgradeLog*.XML 258 | UpgradeLog*.htm 259 | ServiceFabricBackup/ 260 | *.rptproj.bak 261 | 262 | # SQL Server files 263 | *.mdf 264 | *.ldf 265 | *.ndf 266 | 267 | # Business Intelligence projects 268 | *.rdl.data 269 | *.bim.layout 270 | *.bim_*.settings 271 | *.rptproj.rsuser 272 | *- [Bb]ackup.rdl 273 | *- [Bb]ackup ([0-9]).rdl 274 | *- [Bb]ackup ([0-9][0-9]).rdl 275 | 276 | # Microsoft Fakes 277 | FakesAssemblies/ 278 | 279 | # GhostDoc plugin setting file 280 | *.GhostDoc.xml 281 | 282 | # Node.js Tools for Visual Studio 283 | .ntvs_analysis.dat 284 | node_modules/ 285 | 286 | # Visual Studio 6 build log 287 | *.plg 288 | 289 | # Visual Studio 6 workspace options file 290 | *.opt 291 | 292 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 293 | *.vbw 294 | 295 | # Visual Studio LightSwitch build output 296 | **/*.HTMLClient/GeneratedArtifacts 297 | **/*.DesktopClient/GeneratedArtifacts 298 | **/*.DesktopClient/ModelManifest.xml 299 | **/*.Server/GeneratedArtifacts 300 | **/*.Server/ModelManifest.xml 301 | _Pvt_Extensions 302 | 303 | # Paket dependency manager 304 | .paket/paket.exe 305 | paket-files/ 306 | 307 | # FAKE - F# Make 308 | .fake/ 309 | 310 | # CodeRush personal settings 311 | .cr/personal 312 | 313 | # Python Tools for Visual Studio (PTVS) 314 | __pycache__/ 315 | *.pyc 316 | 317 | # Cake - Uncomment if you are using it 318 | # tools/** 319 | # !tools/packages.config 320 | 321 | # Tabs Studio 322 | *.tss 323 | 324 | # Telerik's JustMock configuration file 325 | *.jmconfig 326 | 327 | # BizTalk build output 328 | *.btp.cs 329 | *.btm.cs 330 | *.odx.cs 331 | *.xsd.cs 332 | 333 | # OpenCover UI analysis results 334 | OpenCover/ 335 | 336 | # Azure Stream Analytics local run output 337 | ASALocalRun/ 338 | 339 | # MSBuild Binary and Structured Log 340 | *.binlog 341 | 342 | # NVidia Nsight GPU debugger configuration file 343 | *.nvuser 344 | 345 | # MFractors (Xamarin productivity tool) working folder 346 | .mfractor/ 347 | 348 | # Local History for Visual Studio 349 | .localhistory/ 350 | 351 | # BeatPulse healthcheck temp database 352 | healthchecksdb 353 | 354 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 355 | MigrationBackup/ 356 | 357 | # Ionide (cross platform F# VS Code tools) working folder 358 | .ionide/ 359 | 360 | # End of https://www.toptal.com/developers/gitignore/api/csharp 361 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SC4Parser 2 | SC4Parser is a general purpose library for parsing, finding, and loading files from Simcity 4 save game files ([Maxis Database Packed Files (DBPF)](https://wiki.sc4devotion.com/index.php?title=Savegame)), implemented as a Net Standard 2.0 class library. 3 | 4 | The library was mainly intended for extracting items, from save games. It contains some partial implementation of specific Simcity 4 subfiles but mainly provides solid structures for loading data from save game files. 5 | 6 | Because DBPF files were used for save games for other maxis games (Sims 3, Spore etc) the library may be useful for adapting to work with other Maxis games. 7 | 8 | # What can it do 9 | 10 | - Parse SimCity 4 savegames/Database Packed Files (DBPFs) 11 | - Find and load IndexEntry/file data from saves based on Type ID or TypeGroupInstance (TGI) 12 | - Decompress QFS compressed IndexEntry data 13 | - Parse the following Subfiles: 14 | - [Buildings Subfile](https://wiki.sc4devotion.com/index.php?title=Building_Subfile) (*Fully Implemented*) 15 | - [TerrainMap Subfile](https://github.com/sebamarynissen/sc4/blob/227aecdd01fedd78059a4114e6b0a1e9b6bd50a0/lib/terrain-map.js#L19) (*Fully Implemented*) 16 | - [Network Subfile 1](https://wiki.sc4devotion.com/index.php?title=Network_Subfiles) (*Fully Implemented*) 17 | - [Network Subfile 2](https://wiki.sc4devotion.com/index.php?title=Network_Subfiles) (*Fully Implemented*) 18 | - Bridge Network Subfile (*Partially Implemented*) 19 | - [Lots Subfile](https://wiki.sc4devotion.com/index.php?title=Lot_Subfile) (*Partially Implemented*) 20 | - [RegionView Subfile](https://wiki.sc4devotion.com/index.php?title=Region_View_Subfiles) (*Partially Implemented*) 21 | 22 | # Setup/Download 23 | 24 | You can fetch the latest version of SC4Parser from [Nuget](https://www.nuget.org/packages/SC4Parser/), using the [NuGet Package Manager UI](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-visual-studio) or using the Package Manager Console in Visual Studio: 25 | 26 | ``` 27 | Install-Package SC4Parser 28 | ``` 29 | 30 | You can also download a prebuilt windows version of the library from the [latest releases from github](https://github.com/Killeroo/SC4Parser/releases/latest) and reference the library in your project (when using Visual Studio have a look [at this guide](https://docs.microsoft.com/en-us/visualstudio/ide/how-to-add-or-remove-references-by-using-the-reference-manager?view=vs-2019#add-a-reference)). Or build the library from source (it's not that hard I swear..). 31 | 32 | # Getting started 33 | Once you have library setup and referenced (check above) it should be fairly straightforward to use. The first thing you need to do is load the SimCity 4 save game: 34 | ``` 35 | SC4SaveFile savegame = new SC4SaveFile(@"C:\Path\To\Save\Game.sc4"); 36 | ``` 37 | Once the savegame is loaded you can search for specific files inside the save: 38 | ``` 39 | // Find the Terrain Map Subfile 40 | IndexEntry entry = savegame.FindIndexEntry(new TypeGroupInstance("a9dd6ff4", "e98f9525", "00000001")); 41 | ``` 42 | and/or load the file's data from the save (will be automatically decompressed if needed) 43 | ``` 44 | // Load the data for the Terrain Map Subfile 45 | var data = savegame.LoadIndexEntry(new TypeGroupInstance("a9dd6ff4", "e98f9525", "00000001")); 46 | 47 | // Pass the data to your own code or parser (the world is your oyster) 48 | YourOwnInterestingTerrainParser parser = new YourOwnInterestingTerrainParser(data); 49 | ``` 50 | or you can load one of the implemented subfiles 51 | ``` 52 | // Get a list of all building in the city 53 | List buildings = savegame.GetBuildingSubfile().Buildings; 54 | ``` 55 | 56 | # Documentation 57 | You can find full documentation with examples in [DOCUMENTATION.md](DOCUMENTATION.md) 58 | 59 | # Implemented Subfiles 60 | As mentioned, this library was not intended to implement all subfiles contained in SimCity 4 saves but along the way a few subfiles were implemented and have been included in the library (some more may be added in the future): 61 | 62 | - [Buildings Subfile](https://wiki.sc4devotion.com/index.php?title=Building_Subfile) (*Fully Implemented*) 63 | - [TerrainMap Subfile](https://github.com/sebamarynissen/sc4/blob/227aecdd01fedd78059a4114e6b0a1e9b6bd50a0/lib/terrain-map.js#L19) (*Fully Implemented*) 64 | - [Network Subfile 1](https://wiki.sc4devotion.com/index.php?title=Network_Subfiles) (*Fully Implemented*) 65 | - [Network Subfile 2](https://wiki.sc4devotion.com/index.php?title=Network_Subfiles) (*Fully Implemented*) 66 | - Bridge Network Subfile (*Partially Implemented*) 67 | - [Lots Subfile](https://wiki.sc4devotion.com/index.php?title=Lot_Subfile) (*Partially Implemented*) 68 | - [RegionView Subfile](https://wiki.sc4devotion.com/index.php?title=Region_View_Subfiles) (*Partially Implemented*) 69 | - [Network Index Subfile](https://wiki.sc4devotion.com/index.php?title=Network_Subfiles) (*Very Incomplete, do not use*) 70 | 71 | Most information included in the library or source came from write ups found at https://wiki.sc4devotion.com 72 | 73 | # Design 74 | The classes in this library (specifically the subfiles and their objects) have been designed around what is actually found in the save game data. I have opted to, for the most part, implement the save game objects and properties as they appear in the raw data rather than representing them differently soley for the sake of accessiblity. 75 | 76 | This is why you may find what seem like duplicate properties like ```MaxSizeX1``` and ```MaxSizeX2```, littered throughout the save game subfiles. The save games and their data contain weird variables and odd patterns, the purposes of which are not always known and can only truly be understood by those who worked on the game. So I have left it as an exercise to the user; The library give you access to what is in the files and then left it up to them to understand and use them as they see fit. I do not want to let my implementation of the files dictate how their program should access them. 77 | 78 | It is worth noting as well that some unknown fields have just been left out, I didn't want to litter classes with variables whos use was completely unknown (I'm not a complete monster). 79 | 80 | # Motivation 81 | The library was created for [another project](https://github.com/Killeroo/SC4Cartographer) that would create maps from savegame. I couldn't find any good or usable code for DBPF parsing written in C# so I used the incredibly useful documentation over at [SC4Devotion](https://wiki.sc4devotion.com/index.php?title=Savegame) to decode and write a general parser for these saves. 82 | 83 | Along the way it also became neccessary to implement the QFS/RefPak compression algorithm used to pack files in the savegames. This proved particularly troublesome as all source code for mods that I could find seemed to use the [same](https://github.com/wouanagaine/SC4Mapper-2013/blob/master/Modules/qfs.c) [algorithm](https://github.com/sebamarynissen/sc4/blob/master/src/decompress.cpp) for decompression. So instead of finding a way to compile and link the c implementation to the algorithm (I did try) I decided to [port it over](https://github.com/Killeroo/SC4Parser/blob/master/SC4Parser/Compression/QFS.cs) (thanks Denis Auroux!). 84 | 85 | Hopefully some more accessible source code for parsing and decoding these files will help someone like me who just wants to write a small tool to open up SimCity 4 save games 86 | 87 | # License 88 | ``` 89 | MIT License 90 | 91 | Copyright (c) 2022 Matthew Carney 92 | 93 | Permission is hereby granted, free of charge, to any person obtaining a copy 94 | of this software and associated documentation files (the "Software"), to deal 95 | in the Software without restriction, including without limitation the rights 96 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 97 | copies of the Software, and to permit persons to whom the Software is 98 | furnished to do so, subject to the following conditions: 99 | 100 | The above copyright notice and this permission notice shall be included in all 101 | copies or substantial portions of the Software. 102 | 103 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 104 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 105 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 106 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 107 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 108 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 109 | SOFTWARE. 110 | ``` 111 | -------------------------------------------------------------------------------- /SC4Parser/Structures/TypeGroupInstance.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SC4Parser 4 | { 5 | /// 6 | /// Implements TypeGroupInstance (TGI) identifier used to identify elements and files in a SimCity 4 savegame (DBPF) 7 | /// 8 | /// 9 | /// TypeGroupInstance (TGI) is used as an identifier for files within a SimCity 4 savegame (DBPF) 10 | /// It consists of the items TypeID, GroupID and InstanceID. The combination of these fields creates 11 | /// a unique reference for the file. 12 | /// It is used in comparisons a lot and the values for the fields are quite often referenced as hex 13 | /// so it is represented here with all the neccessary methods for usage, conversion and comparison 14 | /// mainly for convience. 15 | /// More info here: https://www.wiki.sc4devotion.com/index.php?title=Type_Group_Instance 16 | /// 17 | public struct TypeGroupInstance : IEquatable 18 | { 19 | /// 20 | /// Type ID 21 | /// 22 | public uint Type { get; set; } 23 | /// 24 | /// Group ID 25 | /// 26 | public uint Group { get; set; } 27 | /// 28 | /// Instance ID 29 | /// 30 | public uint Instance { get; set; } 31 | 32 | /// 33 | /// TypeGroupInstance (TGI) constructor using uint values of IDs 34 | /// 35 | /// Items Type ID 36 | /// Items Group ID 37 | /// Items Instance ID 38 | /// 39 | /// 40 | /// // Create Terrain Map Subfile's TGI 41 | /// TypeGroupInstance terrainMapTGI = new TypeGroupInstance(2849861620, 3918501157, 1); 42 | /// 43 | /// // Use tgi to load subfile from save 44 | /// sc4Save.LoadIndexEntry(terrainMapTGI); 45 | /// 46 | /// 47 | /// 48 | public TypeGroupInstance(uint type, uint group, uint instance) 49 | : this() 50 | { 51 | Type = type; 52 | Group = group; 53 | Instance = instance; 54 | } 55 | /// 56 | /// TypeGroupInstance (TGI) constructor using string hex values of IDs 57 | /// 58 | /// Items Type ID 59 | /// Items Group ID 60 | /// Items Instance ID 61 | /// 62 | /// 63 | /// // Create Terrain Map Subfile's TGI 64 | /// TypeGroupInstance terrainMapTGI = new TypeGroupInstance("A9DD6FF4", "E98f9525", "00000001"); 65 | /// 66 | /// // Use tgi to load subfile from save 67 | /// sc4Save.LoadIndexEntry(terrainMapTGI); 68 | /// 69 | /// 70 | /// 71 | /// Don't include the 0x at the start of any hex 72 | /// 73 | /// 74 | public TypeGroupInstance(string type_hex, string group_hex, string instance_hex) 75 | : this() 76 | { 77 | Type = uint.Parse(type_hex, System.Globalization.NumberStyles.HexNumber); 78 | Group = uint.Parse(group_hex, System.Globalization.NumberStyles.HexNumber); 79 | Instance = uint.Parse(instance_hex, System.Globalization.NumberStyles.HexNumber); 80 | } 81 | 82 | /// 83 | /// General equals comparitor for TypeGroupInstance (TGI) 84 | /// 85 | /// Object to compare against 86 | /// returns true if objects are equal, false if they are not 87 | public override bool Equals(object obj) 88 | { 89 | if (obj is TypeGroupInstance) 90 | { 91 | return this.Equals((TypeGroupInstance)obj); 92 | } 93 | return false; 94 | } 95 | /// 96 | /// Specific equals compaitor for TypeGroupInstance (TGI) 97 | /// 98 | /// TGI to compare against 99 | /// returns true if objects are equal, false if they are not 100 | public bool Equals(TypeGroupInstance tgi) 101 | { 102 | return (Type == tgi.Type) && (Group == tgi.Group) && (Instance == tgi.Instance); 103 | } 104 | 105 | /// 106 | /// Get the hash for the TypeGroupInstance (TGI) 107 | /// 108 | /// The hash value of the TGI 109 | public override int GetHashCode() 110 | { 111 | return (int)(Type + Group + Instance); 112 | } 113 | 114 | /// 115 | /// Equals comparitor, checks if the 2 provided objects are equal 116 | /// 117 | /// Left hand side of comparison 118 | /// Right habd sude if comparison 119 | /// returns true if objects are equal, false if they are not 120 | public static bool operator ==(TypeGroupInstance lhs, TypeGroupInstance rhs) 121 | { 122 | return lhs.Equals(rhs); 123 | } 124 | /// 125 | /// Not equals comparitor, checks if the 2 provided objects are not equal 126 | /// 127 | /// Left hand side of comparison 128 | /// Right habd sude if comparison 129 | /// returns true if objects are not equal, false if they are not 130 | public static bool operator !=(TypeGroupInstance lhs, TypeGroupInstance rhs) 131 | { 132 | return !(lhs.Equals(rhs)); 133 | } 134 | 135 | /// 136 | /// Creates a TypeGroupInstance (TGI) from a Type ID string 137 | /// 138 | /// The item'ss Type ID 139 | /// The created TGI 140 | public static TypeGroupInstance FromHex(string type_hex) 141 | { 142 | return new TypeGroupInstance( 143 | type_hex, 144 | "0", 145 | "0" 146 | ); 147 | } 148 | /// 149 | /// Creates a TypeGroupInstance (TGI) from a Type ID and Group ID strings 150 | /// 151 | /// The item's Type ID 152 | /// The item's Group ID 153 | /// The created TGI 154 | public static TypeGroupInstance FromHex(string type_hex, string group_hex) 155 | { 156 | return new TypeGroupInstance( 157 | type_hex, 158 | group_hex, 159 | "0" 160 | ); 161 | } 162 | /// 163 | /// Creates a TypeGroupInstance (TGI) from a Type ID, Group ID and Instance ID strings 164 | /// 165 | /// The item's Type ID 166 | /// The item's Group ID 167 | /// The item's Instance ID 168 | /// The created TGI 169 | public static TypeGroupInstance FromHex(string type_hex, string group_hex, string instance_hex) 170 | { 171 | return new TypeGroupInstance( 172 | type_hex, 173 | group_hex, 174 | instance_hex 175 | ); 176 | } 177 | 178 | /// 179 | /// Converts the TypeGroupInstance (TGI) to a string 180 | /// 181 | /// String representation of the TGI 182 | public new string ToString() 183 | { 184 | return string.Format("0x{0} 0x{1} 0x{2}", 185 | Type.ToString("X8"), 186 | Group.ToString("X8"), 187 | Instance.ToString("X8")); 188 | } 189 | 190 | /// 191 | /// Prints out the contents of the TypeGroupInstance (TGI) 192 | /// 193 | public void Dump() 194 | { 195 | Console.WriteLine("{0} {1} {2}", 196 | Type.ToString("X8"), 197 | Group.ToString("X8"), 198 | Instance.ToString("X8")); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /SC4Parser/Compression/QFS.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using SC4Parser.Logging; 4 | 5 | namespace SC4Parser.Compression 6 | { 7 | /// 8 | /// Implementation of QFS/RefPack/LZ77 decompression. This compression is used on larger entries inside saves 9 | /// 10 | /// 11 | /// Note that this implementaiton contains control characters and other changes specific to SimCity 4. 12 | /// You can read about other game specifics at thsi specification for QFS spec http://wiki.niotso.org/RefPack. 13 | /// 14 | /// Ported from https://github.com/wouanagaine/SC4Mapper-2013/blob/db29c9bf88678a144dd1f9438e63b7a4b5e7f635/Modules/qfs.c#L25 15 | /// 16 | /// More information on file specification: 17 | /// - https://www.wiki.sc4devotion.com/index.php?title=DBPF_Compression 18 | /// - http://wiki.niotso.org/RefPack#Naming_notes 19 | /// 20 | public class QFS 21 | { 22 | /// 23 | /// Uncompress data using QFS/RefPak and return uncompressed array of uncompressed data 24 | /// 25 | /// Compressed array of data 26 | /// Uncompressed data array 27 | /// 28 | /// 29 | /// // Load save game 30 | /// SC4SaveFile savegame = new SC4SaveFile(@"C:\Path\To\Save\Game.sc4"); 31 | /// 32 | /// // Read raw data for Region View Subfile from save 33 | /// byte[] data = sc4Save.LoadIndexEntryRaw(REGION_VIEW_SUBFILE_TGI); 34 | /// 35 | /// // Decompress data (This file will normally be compressed, should idealy check before decompressing) 36 | /// byte[] decompressedData = QFS.UncompressData(data); 37 | /// 38 | /// 39 | /// 40 | /// Thrown when the compression algorithm tries to access an element that is out of bounds in the array 41 | /// 42 | public static byte[] UncompressData(byte[] data) 43 | { 44 | byte[] sourceBytes = data; 45 | byte[] destinationBytes; 46 | int sourcePosition = 0; 47 | int destinationPosition = 0; 48 | 49 | // Check first 4 bytes (size of header + compressed data) 50 | uint compressedSize = BitConverter.ToUInt32(sourceBytes, 0); 51 | 52 | // Next read the 5 byte header 53 | byte[] header = new byte[5]; 54 | for (int i = 0; i < 5; i++) 55 | { 56 | header[i] = sourceBytes[i + 4]; 57 | } 58 | 59 | // First 2 bytes should be the QFS identifier 60 | // Next 3 bytes should be the uncompressed size of file 61 | // (we do this by byte shifting (from most significant byte to least)) 62 | // the last 3 bytes of the header to make a number) 63 | uint uncompressedSize = Convert.ToUInt32((long)(header[2] << 16) + (header[3] << 8) + header[4]); ; 64 | 65 | // Create our destination array 66 | destinationBytes = new byte[uncompressedSize]; 67 | 68 | // Next set our position in the file 69 | // (The if checks if the first 4 bytes are the size of the file 70 | // if so our start position is 4 bytes + 5 byte header if not then our 71 | // offset is just the header (5 bytes)) 72 | //if ((sourceBytes[0] & 0x01) != 0) 73 | //{ 74 | // sourcePosition = 9;//8; 75 | //} 76 | //else 77 | //{ 78 | // sourcePosition = 5; 79 | //} 80 | 81 | // Above code is redundant for SimCity 4 saves as the QFS compressed files all have the same header length 82 | // (Check was throwing off start position and caused decompression to get buggered) 83 | sourcePosition = 9; 84 | 85 | // In QFS the control character tells us what type of decompression operation we are going to perform (there are 4) 86 | // Most involve using the bytes proceeding the control byte to determine the amount of data that should be copied from what 87 | // offset. These bytes are labled a, b and c. Some operations only use 1 proceeding byte, others can use 3 88 | byte controlCharacter = 0; 89 | byte a = 0; 90 | byte b = 0; 91 | byte c = 0; 92 | int length = 0; 93 | int offset = 0; 94 | 95 | // Main decoding loop 96 | // Keep decoding while sourcePosition is in source array and position isn't 0xFC? 97 | while ((sourcePosition < sourceBytes.Length) && (sourceBytes[sourcePosition] < 0xFC)) 98 | { 99 | // Read our packcode/control character 100 | controlCharacter = sourceBytes[sourcePosition]; 101 | 102 | // Read bytes proceeding packcode 103 | a = sourceBytes[sourcePosition + 1]; 104 | b = sourceBytes[sourcePosition + 2]; 105 | 106 | // Check which packcode type we are dealing with 107 | if ((controlCharacter & 0x80) == 0) 108 | { 109 | // First we copy from the source array to the destination array 110 | length = controlCharacter & 3; 111 | LZCompliantCopy(ref sourceBytes, sourcePosition + 2, ref destinationBytes, destinationPosition, length); 112 | 113 | // Then we copy characters already in the destination array to our current position in the destination array 114 | sourcePosition += length + 2; 115 | destinationPosition += length; 116 | length = ((controlCharacter & 0x1C) >> 2) + 3; 117 | offset = ((controlCharacter >> 5) << 8) + a + 1; 118 | LZCompliantCopy(ref destinationBytes, destinationPosition - offset, ref destinationBytes, destinationPosition, length); 119 | 120 | destinationPosition += length; 121 | } 122 | else if ((controlCharacter & 0x40) == 0) 123 | { 124 | length = (a >> 6) & 3; 125 | LZCompliantCopy(ref sourceBytes, sourcePosition + 3, ref destinationBytes, destinationPosition, length); 126 | 127 | sourcePosition += length + 3; 128 | destinationPosition += length; 129 | length = (controlCharacter & 0x3F) + 4; 130 | offset = (a & 0x3F) * 256 + b + 1; 131 | LZCompliantCopy(ref destinationBytes, destinationPosition - offset, ref destinationBytes, destinationPosition, length); 132 | 133 | destinationPosition += length; 134 | } 135 | else if ((controlCharacter & 0x20) == 0) 136 | { 137 | c = sourceBytes[sourcePosition + 3]; 138 | 139 | length = controlCharacter & 3; 140 | LZCompliantCopy(ref sourceBytes, sourcePosition + 4, ref destinationBytes, destinationPosition, length); 141 | 142 | sourcePosition += length + 4; 143 | destinationPosition += length; 144 | length = ((controlCharacter >> 2) & 3) * 256 + c + 5; 145 | offset = ((controlCharacter & 0x10) << 12) + 256 * a + b + 1; 146 | LZCompliantCopy(ref destinationBytes, destinationPosition - offset, ref destinationBytes, destinationPosition, length); 147 | 148 | destinationPosition += length; 149 | } 150 | else 151 | { 152 | length = (controlCharacter & 0x1F) * 4 + 4; 153 | LZCompliantCopy(ref sourceBytes, sourcePosition + 1, ref destinationBytes, destinationPosition, length); 154 | 155 | sourcePosition += length + 1; 156 | destinationPosition += length; 157 | } 158 | } 159 | 160 | // Add trailing bytes 161 | if ((sourcePosition < sourceBytes.Length) && (destinationPosition < destinationBytes.Length)) 162 | { 163 | LZCompliantCopy(ref sourceBytes, sourcePosition + 1, ref destinationBytes, destinationPosition, sourceBytes[sourcePosition] & 3); 164 | destinationPosition += sourceBytes[sourcePosition] & 3; 165 | } 166 | 167 | if (destinationPosition != destinationBytes.Length) 168 | { 169 | Logger.Log(LogLevel.Warning, "QFS bad length, {0} instead of {1}", destinationPosition, destinationBytes.Length); 170 | } 171 | 172 | return destinationBytes; 173 | } 174 | 175 | 176 | /// 177 | /// Method that implements LZ compliant copying of data between arrays 178 | /// 179 | /// Array to copy from 180 | /// Position in array to copy from 181 | /// Array to copy to 182 | /// Position in array to copy to 183 | /// Amount of data to copy 184 | /// 185 | /// With QFS (LZ77) we require an LZ compatible copy method between arrays, what this means practically is that we need to copy 186 | /// stuff one byte at a time from arrays. This is, because with LZ compatible algorithms, it is complete legal to copy over data that overruns 187 | /// the currently filled position in the destination array. In other words it is more than likely the we will be asked to copy over data that hasn't 188 | /// been copied yet. It's confusing, so we copy things one byte at a time. 189 | /// 190 | /// 191 | /// Thrown when the copy method tries to access an element that is out of bounds in the array 192 | /// 193 | private static void LZCompliantCopy(ref byte[] source, int sourceOffset, ref byte[] destination, int destinationOffset, int length) 194 | { 195 | if (length != 0) 196 | { 197 | for (int i = 0; i < length; i++) 198 | { 199 | Buffer.BlockCopy(source, sourceOffset, destination, destinationOffset, 1); 200 | 201 | sourceOffset++; 202 | destinationOffset++; 203 | } 204 | } 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /SC4Parser/Structures/Lot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SC4Parser 4 | { 5 | /// 6 | /// Representation of a Simcity 4 lot as it is stored in a save game 7 | /// 8 | /// 9 | /// This implementation is not complete. 10 | /// 11 | /// Implemented from https://www.wiki.sc4devotion.com/index.php?title=Lot_Subfile 12 | /// 13 | /// 14 | /// 15 | /// How to read and use lot data using library 16 | /// // (this is effectively what is done in SC4Save.GetLotSubfile()) 17 | /// 18 | /// // Load save game 19 | /// SC4SaveFile savegame = new SC4SaveFile(@"C:\Path\To\Save\Game.sc4"); 20 | /// 21 | /// // load Lot Subfile from save 22 | /// LotSubfile lotSubfile = new LotSubfile(); 23 | /// IndexEntry lotEntry = savegame.FindIndexEntryWithType("C9BD5D4A") 24 | /// byte[] lotSubfileData = savegame.LoadIndexEntry(lotEntry.TGI); 25 | /// lotSubfile.Parse(lotSubfileData, lotSubfileData.Length); 26 | /// 27 | /// // loop through lots and print out their sizes 28 | /// foreach (Lot lot in lotSubfile.Lots) 29 | /// { 30 | /// Console.Writeline(lot.SizeX + "x" + lot.SizeZ); 31 | /// } 32 | /// 33 | /// 34 | /// 35 | /// 36 | public class Lot 37 | { 38 | /// 39 | /// Position of lot within DBPf file 40 | /// 41 | public uint Offset { get; private set; } 42 | /// 43 | /// Size of lot 44 | /// 45 | public uint Size { get; private set; } 46 | /// 47 | /// Lot data's crc 48 | /// 49 | public uint CRC { get; private set; } 50 | /// 51 | /// Lot's memory 52 | /// 53 | public uint Memory { get; private set; } 54 | /// 55 | /// Lot's spec major version 56 | /// 57 | public ushort MajorVersion { get; private set; } 58 | /// 59 | /// Instance ID of the lot 60 | /// 61 | public uint LotInstanceID { get; private set; } 62 | /// 63 | /// Lot Flag byte 1 , can have one of the following values: 64 | /// 0x01 - Might have to do with road access 65 | /// 0x02 - Might have to do with road(job?) access 66 | /// 0x04 - Might have to do with road access 67 | /// 0x08 - Means the lot is watered 68 | /// 0x10 - Means the lot is powered 69 | /// 0x20 - Means the lot is marked historical 70 | /// 0x40 - Might mean the lot is built 71 | /// 72 | /// 73 | /// Data from https://www.wiki.sc4devotion.com/index.php?title=Lot_Subfile#Appendix_1_-_Flag_Byte_1 74 | /// 75 | public byte FlagByte1 { get; private set; } 76 | /// 77 | /// Minimum tile X coordinate for lot 78 | /// 79 | public int MinTileX { get; internal set; } 80 | /// 81 | /// Minimum tile Z coordinate for lot 82 | /// 83 | public int MinTileZ { get; internal set; } 84 | /// 85 | /// Maximum tile X coordinate for lot 86 | /// 87 | public int MaxTileX { get; internal set; } 88 | /// 89 | /// Maximum tile Z coordinate for lot 90 | /// 91 | public int MaxTileZ { get; internal set; } 92 | /// 93 | /// Lot's commute tile X 94 | /// 95 | public byte CommuteTileX { get; private set; } 96 | /// 97 | /// Lot's commute tile Z 98 | /// 99 | public byte CommuteTileZ { get; private set; } 100 | /// 101 | /// Lot's Y position 102 | /// 103 | public float PositionY { get; private set; } 104 | /// 105 | /// Lot's Y coordinate if slope is conforming 106 | /// 107 | public float Slope1Y { get; private set; } 108 | /// 109 | /// Lot's Y coordinate if slope is conforming 110 | /// 111 | public float Slope2Y { get; private set; } 112 | /// 113 | /// Lot width 114 | /// 115 | public byte SizeX { get; private set; } 116 | /// 117 | /// Lot depth 118 | /// 119 | public byte SizeZ { get; private set; } 120 | /// 121 | /// Lot's orientation 122 | /// 123 | /// 124 | /// 125 | /// 126 | /// 127 | public byte Orientation { get; private set; } 128 | /// 129 | /// Lot flag byte 2, can be one of the following files: 130 | /// 0x01 - Flag Byte 1 = 0x10 - Powered (empty growable zones) 131 | /// 0x02 - Flag Byte 1 = 0x50 - Powered and built 132 | /// 0x03 - Flag Byte 1 = 0x58 - Powered, watered and built 133 | /// 0x04 - Seen it once on a tall office under construction 134 | /// 0x06 - Seen it once on a water tower without power 135 | /// 136 | /// 137 | /// Information from: https://www.wiki.sc4devotion.com/index.php?title=Lot_Subfile#Appendix_2_-_Flag_Byte_2 138 | /// 139 | public byte FlagByte2 { get; private set; } 140 | /// 141 | /// Lot flag byte 3, unknown use 142 | /// 143 | /// 144 | /// More information here: https://www.wiki.sc4devotion.com/index.php?title=Lot_Subfile#Appendix_3_-_Flag_Byte_3 145 | /// 146 | public byte FlagByte3 { get; private set; } 147 | /// 148 | /// Lot's zone type 149 | /// 150 | /// 151 | public byte ZoneType { get; private set; } 152 | /// 153 | /// Lot's zone wealth 154 | /// 155 | /// 156 | public byte ZoneWealth { get; private set; } 157 | /// 158 | /// Date (in game?) that lot grew or was plopped 159 | /// 160 | public uint DateLotAppeared { get; private set; } 161 | /// 162 | /// Lot's associated building Instance ID 163 | /// 164 | public uint BuildingInstanceID { get; private set; } 165 | /// 166 | /// Unknown lot value 167 | /// 168 | public byte Unknown { get; private set; } 169 | 170 | /// 171 | /// Read an individual lot object from a byte array 172 | /// 173 | /// Data to read lot from 174 | /// Position in data to read lot from 175 | /// 176 | /// This implementation is not complete 177 | /// 178 | /// 179 | /// Thrown when trying to parse an element that is out of bounds in the data array 180 | /// 181 | public void Parse(byte[] buffer, uint offset) 182 | { 183 | Offset = offset; 184 | Size = BitConverter.ToUInt32(buffer, 0); 185 | CRC = BitConverter.ToUInt32(buffer, 4); 186 | Memory = BitConverter.ToUInt32(buffer, 8); 187 | MajorVersion = BitConverter.ToUInt16(buffer, 12); 188 | LotInstanceID = BitConverter.ToUInt32(buffer, 14); 189 | FlagByte1 = buffer[18]; 190 | MinTileX = buffer[19]; 191 | MinTileZ = buffer[20]; 192 | MaxTileX = buffer[21]; 193 | MaxTileZ = buffer[22]; 194 | CommuteTileX = buffer[23]; 195 | CommuteTileZ = buffer[24]; 196 | PositionY = BitConverter.ToSingle(buffer, 25); 197 | Slope1Y = BitConverter.ToSingle(buffer, 29); 198 | Slope2Y = BitConverter.ToSingle(buffer, 33); 199 | SizeX = buffer[37]; 200 | SizeZ = buffer[38]; 201 | Orientation = buffer[39]; 202 | FlagByte2 = buffer[40]; 203 | FlagByte3 = buffer[41]; 204 | ZoneType = buffer[42]; 205 | ZoneWealth = buffer[43]; 206 | } 207 | 208 | /// 209 | /// Prints out the values of the lot 210 | /// 211 | public void Dump() 212 | { 213 | Console.WriteLine("Offset: {0} (0x{1})", Offset, Offset.ToString("x8")); 214 | Console.WriteLine("Size: {0} (0x{1})", Size, Size.ToString("x8")); 215 | Console.WriteLine("CRC: 0x{0}", CRC.ToString("x8")); 216 | Console.WriteLine("Memory address: 0x{0}", Memory.ToString("x8")); 217 | Console.WriteLine("Major Version: {0}", MajorVersion); 218 | Console.WriteLine("Lot IID: 0x{0}", LotInstanceID.ToString("x8")); 219 | Console.WriteLine("Flag Byte 1: 0x{0}", FlagByte1.ToString("x8")); 220 | Console.WriteLine("Min Tile X: 0x{0} ({1})", MinTileX.ToString("x8"), MinTileX); 221 | Console.WriteLine("Min Tile Z: 0x{0} ({1})", MinTileZ.ToString("x8"), MinTileZ); 222 | Console.WriteLine("Max Tile X: 0x{0} ({1})", MaxTileX.ToString("x8"), MaxTileX); 223 | Console.WriteLine("Max Tile Z: 0x{0} ({1})", MaxTileZ.ToString("x8"), MaxTileZ); 224 | Console.WriteLine("Commute Tile X: 0x{0} ({1})", MaxTileX.ToString("x8"), MaxTileX); 225 | Console.WriteLine("Commute Tile Z: 0x{0} ({1})", MaxTileZ.ToString("x8"), MaxTileZ); 226 | Console.WriteLine("Position Y: {0}", PositionY); 227 | Console.WriteLine("Slope 1 Y: {0}", Slope1Y); 228 | Console.WriteLine("Slope 2 Y: {0}", Slope2Y); 229 | Console.WriteLine("Lot Width: 0x{0} ({1})", SizeX.ToString("x8"), SizeX); 230 | Console.WriteLine("Lot Depth: 0x{0} ({1})", SizeZ.ToString("x8"), SizeZ); 231 | Console.WriteLine("Lot Orientation: 0x{0} ({1})", Orientation.ToString("x8"), Constants.ORIENTATION_STRINGS[Orientation]); 232 | Console.WriteLine("Flag Byte 2: 0x{0} ({1})", FlagByte2.ToString("x8"), FlagByte2); 233 | Console.WriteLine("Flag Byte 3: 0x{0} ({1})", FlagByte3.ToString("x8"), FlagByte3); 234 | Console.WriteLine("Zone Type: 0x{0} ({1})", ZoneType.ToString("x8"), Constants.LOT_ZONE_TYPE_STRINGS[ZoneType]); 235 | Console.WriteLine("Zone Wealth: 0x{0} ({1})", ZoneWealth.ToString("x8"), Constants.LOT_ZONE_WEALTH_STRINGS[ZoneWealth]); 236 | } 237 | 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /SC4Parser/Structures/SaveGameProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using SC4Parser.Logging; 5 | 6 | namespace SC4Parser 7 | { 8 | /// 9 | /// Represents a Savegame Property (SIGPROP). SIGPROPs are small structures used to store individual entries of information for a given object. 10 | /// 11 | /// 12 | /// SIGPROPs are highly situational and their value and use depends on where they are used: 13 | /// Take, for example, SIGPROPs being used for storing specific information about buildings (patient capacity for a hospital, dispatches available for a firestation, 14 | /// custom name given to a building). A SIGPROPs data can be several different types, they can also contain several values. 15 | /// (Writing this parsing was a pain) 16 | /// 17 | /// Implemented using the following: https://wiki.sc4devotion.com/index.php?title=Building_Subfile#Appendix_1:_Structure_of_SGPROP_.28SaveGame_Properties.29 18 | /// 19 | /// 20 | /// 21 | public class SaveGameProperty 22 | { 23 | /// 24 | /// SaveGame Property (SIGPROP) value, used to identify the use of the SIGPROP 25 | /// 26 | /// 27 | /// A SIGPROP with a Property Name Value of 0x899AFBAD is used to store a buildings custom name 28 | /// 29 | public uint PropertyNameValue { get; private set; } 30 | /// 31 | /// SaveGame Property (SIGPROP) value copy, duplicated for unknown reason. 32 | /// 33 | /// 34 | public uint PropertyNameValueCopy { get; private set; } 35 | /// 36 | /// Unknown SaveGame Property value 37 | /// 38 | public uint Unknown1 { get; private set; } 39 | /// 40 | /// Data type stored in the SaveGame Property (SIGPROP) 41 | /// 42 | /// 43 | /// 01=UInt8, 02=UInt16, 03=UInt32, 07=SInt32, 08=SInt64, 09=Float32, 0B=Boolean, 0C=String 44 | /// 45 | /// 46 | /// 47 | public byte DataType { get; private set; } 48 | /// 49 | /// Determines if there is repeated/multiple data in the SaveGame Property (SIGPROP) 50 | /// 51 | public byte KeyType { get; private set; } 52 | /// 53 | /// Unknown SaveGame Property value 54 | /// 55 | public ushort Unknown2 { get; private set; } 56 | /// 57 | /// Amount of data that is stored in the SaveGame Property (SIGPROP) 58 | /// 59 | /// 60 | public uint DataRepeatedCount { get; private set; } 61 | /// 62 | /// Data that is stored in the SaveGame Property (SIGPROP) 63 | /// 64 | /// 65 | /// 66 | public List Data { get; private set; } = new List(); 67 | 68 | /// 69 | /// Loads an individual SaveGame Property (SIGPROP) from a byte array 70 | /// 71 | /// Data to read the SIGPROP from 72 | /// Position in the data array to start reading the SIGPROP from 73 | /// The new offset/position after the SIGPROP has been read 74 | /// 75 | /// This parse functions works pretty similarly to other parse functions, but because they can return in the middle 76 | /// of data entries, the calling function might need the current index after the data has been read so we return that. 77 | /// 78 | /// The data buffer provided may contain multiple SIGPROPs but the method is only designed to read one 79 | /// 80 | /// 81 | /// Thrown when trying to parse an element that is out of bounds in the data array 82 | /// 83 | public int Parse(byte[] buffer, int offset = 0) 84 | { 85 | PropertyNameValue = BitConverter.ToUInt32(buffer, offset + 0); 86 | PropertyNameValueCopy = BitConverter.ToUInt32(buffer, offset + 4); 87 | Unknown1 = BitConverter.ToUInt32(buffer, offset + 8); 88 | DataType = buffer[offset + 12]; 89 | KeyType = buffer[offset + 13]; 90 | Unknown2 = BitConverter.ToUInt16(buffer, offset + 14); 91 | 92 | int currentOffset = offset + 16; 93 | 94 | if (KeyType == 0x80) // Reading multiple values 95 | { 96 | DataRepeatedCount = BitConverter.ToUInt32(buffer, currentOffset); 97 | currentOffset += 4; 98 | 99 | for (int i = 0; i < DataRepeatedCount; i++) 100 | { 101 | switch (DataType) 102 | { 103 | case 0x01: 104 | Data.Add(buffer[currentOffset]); 105 | currentOffset += 1; 106 | break; 107 | case 0x02: 108 | Data.Add(BitConverter.ToUInt16(buffer, currentOffset)); 109 | currentOffset += 2; 110 | break; 111 | case 0x03: 112 | Data.Add(BitConverter.ToUInt32(buffer, currentOffset)); 113 | currentOffset += 4; 114 | break; 115 | case 0x07: 116 | Data.Add(BitConverter.ToInt32(buffer, currentOffset)); 117 | currentOffset += 4; 118 | break; 119 | case 0x08: 120 | Data.Add(BitConverter.ToInt64(buffer, currentOffset)); 121 | currentOffset += 8; 122 | break; 123 | case 0x09: 124 | Data.Add(BitConverter.ToSingle(buffer, currentOffset)); 125 | currentOffset += 4; 126 | break; 127 | case 0x0B: 128 | Data.Add(BitConverter.ToBoolean(buffer, currentOffset)); 129 | currentOffset += 1; 130 | break; 131 | case 0x0C: 132 | Data.Add(Convert.ToChar(buffer[currentOffset])); 133 | currentOffset += 1; 134 | break; 135 | default: 136 | break; 137 | } 138 | } 139 | } 140 | else // Just reading one value 141 | { 142 | switch (DataType) 143 | { 144 | case 0x01: 145 | Data.Add(buffer[currentOffset]); 146 | currentOffset += 1; 147 | break; 148 | case 0x02: 149 | Data.Add(BitConverter.ToUInt16(buffer, currentOffset)); 150 | currentOffset += 2; 151 | break; 152 | case 0x03: 153 | Data.Add(BitConverter.ToUInt32(buffer, currentOffset)); 154 | currentOffset += 4; 155 | break; 156 | case 0x07: 157 | Data.Add(BitConverter.ToInt32(buffer, currentOffset)); 158 | currentOffset += 4; 159 | break; 160 | case 0x08: 161 | Data.Add(BitConverter.ToInt64(buffer, currentOffset)); 162 | currentOffset += 8; 163 | break; 164 | case 0x09: 165 | Data.Add(BitConverter.ToSingle(buffer, currentOffset)); 166 | currentOffset += 4; 167 | break; 168 | case 0x0B: 169 | Data.Add(BitConverter.ToBoolean(buffer, currentOffset)); 170 | currentOffset += 1; 171 | break; 172 | case 0x0C: 173 | Data.Add(Convert.ToChar(buffer[currentOffset])); 174 | currentOffset += 1; 175 | break; 176 | default: 177 | break; 178 | } 179 | } 180 | 181 | return currentOffset; 182 | } 183 | 184 | /// 185 | /// Prints the values of SaveGame Property (SIGPROP) 186 | /// 187 | public void Dump() 188 | { 189 | Console.WriteLine("Property Name Value: {0} [{1}]", PropertyNameValue, PropertyNameValue.ToString("x8")); 190 | Console.WriteLine("Property Name Value (copy): {0} [{1}]", PropertyNameValueCopy, PropertyNameValueCopy.ToString("x8")); 191 | Console.WriteLine("Unknown1: {0} [{1}]", Unknown1, Unknown1.ToString("x8")); 192 | Console.WriteLine("Data Type: {0} [{1}]", DataType, Constants.SIGPROP_DATATYPE_TYPE_STRINGS[DataType]); 193 | Console.WriteLine("Key Type: {0}", KeyType); 194 | Console.WriteLine("Unknown: {0}", Unknown2); 195 | Console.WriteLine("Data Repeats: {0}", DataRepeatedCount); 196 | 197 | Console.WriteLine("Values:"); 198 | for (int i = 0; i < Data.Count; i++) 199 | { 200 | Console.WriteLine(" - {0}", Data[i]); 201 | } 202 | } 203 | 204 | /// 205 | /// Extracts a bunch of SaveGame Properties (SIGPROP) and then returns the new offset after everything has been read 206 | /// 207 | /// Data to read SIGPROPs from 208 | /// Number of SIGPROPs to try and read 209 | /// Offset/position to start reading the SIGPROPs from in the data array 210 | /// A list of all parsed SIGPROPs 211 | /// 212 | /// Thrown when trying to parse an element that is out of bounds in the data array 213 | /// 214 | /// 215 | public static List ExtractFromBuffer(byte[] buffer, uint count, ref uint offset) 216 | 217 | { 218 | List results = new List(); 219 | int currentOffset = (int) offset; 220 | 221 | for (int i = 0; i < count; i++) 222 | { 223 | SaveGameProperty property = new SaveGameProperty(); 224 | 225 | // Parse returns the new offset so we keep track of that 226 | currentOffset = property.Parse(buffer, currentOffset); 227 | results.Add(property); 228 | 229 | Logger.Log(LogLevel.Debug, "Read SaveGame Property @ offset {0}, {1} bytes left to read", 230 | currentOffset, 231 | ((buffer.Length - offset) + count) - currentOffset 232 | ); 233 | } 234 | 235 | // Update the offset now that we have read all the SGPROPs 236 | // so the caller knows where to continue reading data from 237 | offset = (uint) currentOffset; 238 | 239 | return results; 240 | } 241 | 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /SC4Parser/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SC4Parser 5 | { 6 | /// 7 | /// Stores common values and identifiers used in SimCity 4 save gamesa 8 | /// 9 | public class Constants 10 | { 11 | /// 12 | /// TypeGroupInstance (TGI) ID for the Database Directory File (DBDF) 13 | /// 14 | /// 15 | /// 16 | public static readonly TypeGroupInstance DATABASE_DIRECTORY_FILE_TGI = new TypeGroupInstance("E86B1EEF", "E86B1EEF", "286B1F03"); 17 | /// 18 | /// TypeGroupInstance (TGI) ID for the Region View Subfile 19 | /// 20 | /// 21 | /// 22 | public static readonly TypeGroupInstance REGION_VIEW_SUBFILE_TGI = new TypeGroupInstance("CA027EDB", "CA027EE1", "00000000"); 23 | /// 24 | /// TypeGroupInstance (TGI) ID for the Terrain Map Subfile 25 | /// 26 | /// 27 | /// 28 | public static readonly TypeGroupInstance TERRAIN_MAP_SUBFILE_TGI = new TypeGroupInstance("A9DD6FF4", "E98f9525", "00000001"); 29 | 30 | /// 31 | /// Type ID of Lot Subfile 32 | /// 33 | /// 34 | public static readonly string LOT_SUBFILE_TYPE = "C9BD5D4A"; 35 | /// 36 | /// Type ID of Building Subfile 37 | /// 38 | /// 39 | public static readonly string BUILDING_SUBFILE_TYPE = "A9BD882D"; 40 | /// 41 | /// Type ID of Network Index Subfile 42 | /// 43 | /// 44 | public static readonly string NETWORK_INDEX_SUBFILE_TYPE = "6A0F82B2"; 45 | /// 46 | /// Type ID of Network Subfile 1 47 | /// 48 | /// 49 | public static readonly string NETWORK_SUBFILE_1_TYPE = "C9C05C6E"; 50 | /// 51 | /// Type ID of Network Subfile 2 52 | /// 53 | /// 54 | public static readonly string NETWORK_SUBFILE_2_TYPE = "CA16374F"; 55 | /// 56 | /// Type ID of the prebuilt network subfile 57 | /// 58 | /// 59 | public static readonly string PREBUILT_NETWORK_SUBFILE_TYPE = "49C1A034"; 60 | /// 61 | /// Type ID of the bridge network subfile 62 | /// 63 | /// 64 | public static readonly string BRIDGE_NETWORK_SUBFILE_TYPE = "49CC1BCD"; 65 | /// 66 | /// Type ID of the tunnel network subfile 67 | /// 68 | public static readonly string TUNNEL_NETWORK_SUBFILE_TYPE = "8A4BD52B"; 69 | public static readonly string ITEM_INDEX_SUBFILE_TYPE = "098F964D"; 70 | 71 | /// 72 | /// Orientations used by SimCity 4 save game items as strings 73 | /// 74 | /// 75 | /// Following is a full list of all different orientations: 76 | /// 0x00 = North 77 | /// 0x01 = East 78 | /// 0x02 = South 79 | /// 0x03 = West 80 | /// 0x80 = North, mirrored 81 | /// 0x81 = East, mirrored 82 | /// 0x82 = south, mirrored 83 | /// 0x83 = West, mirrored 84 | /// 85 | /// 86 | /// 87 | public static string[] ORIENTATION_STRINGS = new string[] 88 | { 89 | "North", 90 | "East", 91 | "South", 92 | "West" 93 | }; 94 | 95 | /// 96 | /// Different types used in Save Game Propertie's (SIGPROPs) data 97 | /// 98 | /// 99 | /// 100 | public static Dictionary SIGPROP_DATATYPE_TYPES = new Dictionary 101 | { 102 | {0x01, new byte()}, 103 | {0x02, new UInt16()}, 104 | {0x03, new UInt32()}, 105 | {0x07, new Int32()}, 106 | {0x08, new Int64()}, 107 | {0x09, new float()}, 108 | {0x0B, new Boolean()}, 109 | {0x0C, new char()} 110 | }; 111 | /// 112 | /// Different types used in Save Game Propertie's (SIGPROPs) as strings 113 | /// 114 | public static Dictionary SIGPROP_DATATYPE_TYPE_STRINGS = new Dictionary 115 | { 116 | {0x01, "UInt8"}, 117 | {0x02, "UInt16"}, 118 | {0x03, "UInt32"}, 119 | {0x07, "Int32"}, 120 | {0x08, "Int64"}, 121 | {0x09, "Float"}, 122 | {0x0B, "Boolean"}, 123 | {0x0C, "string//char"} 124 | }; 125 | 126 | /// 127 | /// Lot zone types as strings 128 | /// 129 | public static Dictionary LOT_ZONE_TYPE_STRINGS = new Dictionary 130 | { 131 | {0x00, "Unknown"}, 132 | {0x01, "Residential - Low"}, 133 | {0x02, "Residential - Medium"}, 134 | {0x03, "Residential - High"}, 135 | {0x04, "Commercial - Low"}, 136 | {0x05, "Commercial - Medium"}, 137 | {0x06, "Commercial - High"}, 138 | {0x07, "Industrial - Low"}, 139 | {0x08, "Industrial - Medium"}, 140 | {0x09, "Industrial - High"}, 141 | {0x0A, "Military"}, 142 | {0x0B, "Airport"}, 143 | {0x0C, "Seaport"}, 144 | {0x0D, "Spaceport"}, 145 | {0x0E, "Plopped building"}, 146 | {0x0F, "Plopped building"}, 147 | }; 148 | /// 149 | /// Lot wealth types as strings 150 | /// 151 | public static Dictionary LOT_ZONE_WEALTH_STRINGS = new Dictionary 152 | { 153 | {0x00, "None"}, 154 | {0x01, @"$"}, 155 | {0x02, @"$$"}, 156 | {0x03, @"$$$"} 157 | }; 158 | 159 | /// 160 | /// Different network types as strings 161 | /// 162 | public static Dictionary NETWORK_TYPE_STRINGS = new Dictionary 163 | { 164 | {0x00, "Road"}, 165 | {0x01, "Rail"}, 166 | {0x02, "Maxis Elevated Highway"}, 167 | {0x03, "Street"}, 168 | {0x04, "Pipe"}, 169 | {0x05, "Powerline"}, 170 | {0x06, "Avenue"}, 171 | {0x07, "Subway"}, 172 | {0x08, "Light Rail"}, 173 | {0x09, "Monorail"}, 174 | {0x0A, "One Way Road"}, 175 | {0x0B, "Dirt Road"} 176 | }; 177 | 178 | /// 179 | /// Different occupancy groups that appear in the RegionView Subfile. Ordered by the index that they appear at. 180 | /// 181 | public static Dictionary OCCUPANCY_GROUPS = new Dictionary() 182 | { 183 | {0, "Max Residential Population"}, 184 | {1, "R$ Max Population"}, 185 | {2, "R$ Current Population"}, 186 | {3, "R$$ Max Population"}, 187 | {4, "R$$ Current Population"}, 188 | {5, "R$$$ Max Population"}, 189 | {6, "R$$$ Current Population"}, 190 | {7, "R$ Jobs"}, 191 | {8, "R$$ Jobs"}, 192 | {9, "R$$$ Jobs"}, 193 | {10, "R$ Workforce EQ1"}, 194 | {11, "R$ Workforce EQ2"}, 195 | {12, "R$ Workforce EQ3"}, 196 | {13, "R$ Workforce EQ4"}, 197 | {14, "R$$ Workforce EQ1"}, 198 | {15, "R$$ Workforce EQ2"}, 199 | {16, "R$$ Workforce EQ3"}, 200 | {17, "R$$ Workforce EQ4"}, 201 | {18, "R$$$ Workforce EQ1"}, 202 | {19, "R$$$ Workforce EQ2"}, 203 | {20, "R$$$ Workforce EQ3"}, 204 | {21, "R$$$ Workforce EQ4"}, 205 | {22, "CS$ Max Jobs"}, 206 | {23, "CS$ Cur Jobs"}, 207 | {24, "CS$$ Max Jobs"}, 208 | {25, "CS$$ Cur Jobs"}, 209 | {26, "CS$$$ Max Jobs"}, 210 | {27, "CS$$$ Cur Jobs"}, 211 | {28, "CO$$ Max Jobs"}, 212 | {29, "CO$$ Cur Jobs"}, 213 | {30, "CO$$$ Max Jobs"}, 214 | {31, "CO$$$ Cur Jobs"}, 215 | {32, "IA Max Jobs"}, 216 | {33, "IA Cur Jobs"}, 217 | {34, "ID Max Jobs"}, 218 | {35, "ID Cur Jobs"}, 219 | {36, "IM Max Jobs"}, 220 | {37, "IM Cur Jobs"}, 221 | {38, "IH Max Jobs"}, 222 | {39, "IH Cur Jobs"} 223 | }; 224 | 225 | /// 226 | /// Low density residential zone type 227 | /// 228 | public const byte LOT_ZONE_TYPE_RESIDENTIAL_LOW = 0x01; 229 | /// 230 | /// Medium density residential zone type 231 | /// 232 | public const byte LOT_ZONE_TYPE_RESIDENTIAL_MEDIUM = 0x02; 233 | /// 234 | /// High density residential zone type 235 | /// 236 | public const byte LOT_ZONE_TYPE_RESIDENTIAL_HIGH = 0x03; 237 | /// 238 | /// Low density commercial zone type 239 | /// 240 | public const byte LOT_ZONE_TYPE_COMMERCIAL_LOW = 0x04; 241 | /// 242 | /// Medium density commercial zone type 243 | /// 244 | public const byte LOT_ZONE_TYPE_COMMERCIAL_MEDIUM = 0x05; 245 | /// 246 | /// High density commercial zone type 247 | /// 248 | public const byte LOT_ZONE_TYPE_COMMERCIAL_HIGH = 0x06; 249 | /// 250 | /// Low density industrial zone type 251 | /// 252 | public const byte LOT_ZONE_TYPE_INDUSTRIAL_LOW = 0x07; 253 | /// 254 | /// Medium density industrial zone type 255 | /// 256 | public const byte LOT_ZONE_TYPE_INDUSTRIAL_MEDIUM = 0x08; 257 | /// 258 | /// High density industrial zone type 259 | /// 260 | public const byte LOT_ZONE_TYPE_INDUSTRIAL_HIGH = 0x09; 261 | /// 262 | /// Military zone type 263 | /// 264 | public const byte LOT_ZONE_TYPE_MILITARY = 0x0A; 265 | /// 266 | /// Airport zone type 267 | /// 268 | public const byte LOT_ZONE_TYPE_AIRPORT = 0x0B; 269 | /// 270 | /// Seaport zone type 271 | /// 272 | public const byte LOT_ZONE_TYPE_SEAPORT = 0x0C; 273 | /// 274 | /// Spaceport zone type 275 | /// 276 | public const byte LOT_ZONE_TYPE_SPACEPORT = 0x0D; 277 | /// 278 | /// Plopped building zone type 279 | /// 280 | public const byte LOT_ZONE_TYPE_PLOPPED_BUILDING = 0x0F; 281 | /// 282 | /// Plopped building zone type 283 | /// 284 | public const byte LOT_ZONE_TYPE_PLOPPED_BUILDING_ALT = 0x0E; 285 | 286 | /// 287 | /// No zone wealth value 288 | /// 289 | public const byte LOT_WEALTH_NONE = 0x00; 290 | /// 291 | /// Low zone wealth value 292 | /// 293 | public const byte LOT_WEALTH_LOW = 0x01; 294 | /// 295 | /// Medium zone wealth value 296 | /// 297 | public const byte LOT_WEALTH_MEDIUM = 0x02; 298 | /// 299 | /// High zone wealth value 300 | /// 301 | public const byte LOT_WEALTH_HIGH = 0x03; 302 | 303 | /// 304 | /// North orientation 305 | /// 306 | public const byte ORIENTATION_NORTH = 0x00; 307 | /// 308 | /// East orientation 309 | /// 310 | public const byte ORIENTATION_EAST = 0x01; 311 | /// 312 | /// South orientation 313 | /// 314 | public const byte ORIENTATION_SOUTH = 0x02; 315 | /// 316 | /// West orientation 317 | /// 318 | public const byte ORIENTATION_WEST = 0x03; 319 | 320 | /// 321 | /// Number of grid tiles in a small sized city 322 | /// 323 | public const int SMALL_CITY_TILE_COUNT = 64; 324 | /// 325 | /// Number of grid tiles in a medium sized city 326 | /// 327 | public const int MEDIUM_CITY_TILE_COUNT = 128; 328 | /// 329 | /// Number of grid tiles in a large city 330 | /// 331 | public const int LARGE_CITY_TILE_COUNT = 128; 332 | 333 | /// 334 | /// City mode that represents if a city is in God Mode 335 | /// 336 | /// 337 | public const int GOD_MODE_FLAG = 0; 338 | /// 339 | /// City mode that represents if a city is in Mayor Mode 340 | /// 341 | /// 342 | public const int MAYOR_MODE_FLAG = 1; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /SC4Parser/SubFiles/RegionViewSubfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | using SC4Parser.Logging; 6 | 7 | namespace SC4Parser 8 | { 9 | /// 10 | /// Region View Subfile (partial implementation). Contains basic city information from a region point of view. 11 | /// 12 | /// 13 | /// Only a partial implementation and will not contain all values from the save game 14 | /// 15 | /// Based off spec from here: https://wiki.sc4devotion.com/index.php?title=Region_View_Subfiles 16 | /// 17 | /// 18 | /// 19 | /// // Simple usage 20 | /// // (Just assume the region view subfile has already been read, see SC4SaveGame.GetRegionViewSubfile()) 21 | /// 22 | /// // Access some data 23 | /// Console.WriteLine("city location x={0} y={1}", 24 | /// regionViewSubfile.TileXLocation, 25 | /// regionViewSubfile.TileYLocation); 26 | /// 27 | /// 28 | public class RegionViewSubfile 29 | { 30 | /// 31 | /// Major version of the subfile 32 | /// 33 | /// 34 | /// You can see the different versions here: https://www.wiki.sc4devotion.com/index.php?title=Region_View_Subfiles 35 | /// This implementation is based around the SimCity 4 Rush Hour/Deluxe version of the game (1.13) 36 | /// 37 | public ushort MajorVersion { get; protected set; } 38 | /// 39 | /// Minor version of the subfile 40 | /// 41 | /// W 42 | /// You can see the different versions here: https://www.wiki.sc4devotion.com/index.php?title=Region_View_Subfiles 43 | /// This implementation is based around the SimCity 4 Rush Hour/Deluxe version of the game (1.13) 44 | /// 45 | public ushort MinorVersion { get; private set; } 46 | /// 47 | /// X location of the city in the region view 48 | /// 49 | public uint TileXLocation { get; private set; } 50 | /// 51 | /// Z location of the city in the region view 52 | /// 53 | public uint TileYLocation { get; private set; } 54 | /// 55 | /// X size of the city 56 | /// 57 | /// 58 | /// Multiplied by 64 to get the number of the tiles in the city 59 | /// 60 | public uint CitySizeX { get; private set; } 61 | /// 62 | /// Y size of the city 63 | /// 64 | /// 65 | /// Multiplied by 64 to get the number of the tiles in the city 66 | /// 67 | public uint CitySizeY { get; private set; } 68 | /// 69 | /// Residential population of city 70 | /// 71 | public uint ResidentialPopulation { get; private set; } 72 | /// 73 | /// Commercial population of city 74 | /// 75 | public uint CommercialPopulation { get; private set; } 76 | /// 77 | /// Industrial population of city 78 | /// 79 | public uint IndustrialPopulation { get; private set; } 80 | /// 81 | /// Mayor rating of city, in bars as seen on region view (12 max) 82 | /// 83 | public byte MayorRating { get; private set; } 84 | /// 85 | /// City star count (as seen from region view), (0=1, 1=2, 2=3) 86 | /// 87 | public byte StarCount { get; private set; } 88 | /// 89 | /// Indicates if the city is a tutorial city. 1 for tutorial map. 90 | /// 91 | public byte TutorialFlag { get; private set; } 92 | /// 93 | /// City GUID 94 | /// 95 | public uint CityGuid { get; private set; } 96 | /// 97 | /// Mode city is in (1 = Mayor mode, 0 = God mode) 98 | /// 99 | public byte ModeFlag { get; private set; } 100 | /// 101 | /// Length of city name string 102 | /// 103 | public uint CityNameLength { get; private set; } 104 | /// 105 | /// City's name 106 | /// 107 | public string CityName { get; private set; } 108 | /// 109 | /// Length of former city name 110 | /// 111 | public uint FormerCityNameLength { get; private set; } 112 | /// 113 | /// Cities former name 114 | /// 115 | public string FormerCityName { get; private set; } 116 | /// 117 | /// Length of mayor name string 118 | /// 119 | public uint MayorNameLength { get; private set; } 120 | /// 121 | /// City's mayor 122 | /// 123 | public string MayorName { get; private set; } 124 | /// 125 | /// City description string length 126 | /// 127 | public uint InternalDescriptionLength { get; private set; } 128 | /// 129 | /// City description 130 | /// 131 | public string InternalDescription { get; private set; } 132 | /// 133 | /// Length of default mayor name 134 | /// 135 | public uint DefaultMayorNameLength { get; private set; } 136 | /// 137 | /// Default mayor name (usually "Jonas Sparks") 138 | /// 139 | public string DefaultMayorName { get; private set; } 140 | public uint CurrentOccupancyGroupCount{ get; private set; } 141 | public List OccupancyGroupsCurrent = new List(); 142 | public uint MaxOccupancyGroupCount { get; private set; } 143 | public List OccupancyGroupsMax = new List(); 144 | public uint LimitsOccupancyGroupCount { get; private set; } 145 | public List OccupancyGroupsLimits = new List(); 146 | 147 | /// 148 | /// Parses Region View Subfile from a byte array 149 | /// 150 | /// Data to read subfile from 151 | /// 152 | /// Thrown when trying to parse an element that is out of bounds in the data array 153 | /// 154 | public void Parse(byte[] buffer) 155 | { 156 | uint internalOffset = 0; 157 | 158 | Logger.Log(LogLevel.Info, "Parsing RegionView subfile..."); 159 | 160 | // TODO: Convert to BinaryReader (yeah I know..) 161 | MajorVersion = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 162 | MinorVersion = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 163 | 164 | if (MinorVersion < 13) 165 | Logger.Log(LogLevel.Warning, "Parsing a pre Rush Hour save game, some of the values in the RegionView Subfile might be garbled"); 166 | 167 | TileXLocation = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 168 | TileYLocation = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 169 | CitySizeX = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0) * 64; 170 | CitySizeY = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0) * 64; 171 | ResidentialPopulation = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 172 | CommercialPopulation = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); ; 173 | IndustrialPopulation = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); ; 174 | 175 | if (MinorVersion > 9) 176 | internalOffset += 4; 177 | 178 | if (MinorVersion > 10) 179 | MayorRating = Extensions.ReadByte(buffer, ref internalOffset); 180 | 181 | StarCount = Extensions.ReadByte(buffer, ref internalOffset); 182 | TutorialFlag = Extensions.ReadByte(buffer, ref internalOffset); 183 | CityGuid = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 184 | internalOffset += 4 * 5; // Skip over unknown fields 185 | ModeFlag = Extensions.ReadByte(buffer, ref internalOffset); 186 | CityNameLength = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 187 | CityName = Encoding.ASCII.GetString(Extensions.ReadBytes(buffer, CityNameLength, ref internalOffset)); 188 | FormerCityNameLength = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 189 | FormerCityName = Encoding.ASCII.GetString(Extensions.ReadBytes(buffer, FormerCityNameLength, ref internalOffset)); 190 | MayorNameLength = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 191 | MayorName = Encoding.ASCII.GetString(Extensions.ReadBytes(buffer, MayorNameLength, ref internalOffset)); 192 | InternalDescriptionLength = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 193 | InternalDescription = Encoding.ASCII.GetString(Extensions.ReadBytes(buffer, InternalDescriptionLength, ref internalOffset)); 194 | DefaultMayorNameLength = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 195 | DefaultMayorName = Encoding.ASCII.GetString(Extensions.ReadBytes(buffer, DefaultMayorNameLength, ref internalOffset)); 196 | internalOffset += (4 * 6); // Skip over 6 unused uints 197 | 198 | // Parse Occupant Group info 199 | CurrentOccupancyGroupCount = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 200 | for (int i = 0; i < CurrentOccupancyGroupCount; i++) 201 | { 202 | uint group = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 203 | uint pop = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 204 | OccupancyGroupsCurrent.Add(new OccupancyGroup(i, Constants.OCCUPANCY_GROUPS[i], group, pop)); 205 | } 206 | 207 | LimitsOccupancyGroupCount = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 208 | for (int i = 0; i < LimitsOccupancyGroupCount; i++) 209 | { 210 | uint group = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 211 | uint pop = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 212 | OccupancyGroupsMax.Add(new OccupancyGroup(i, Constants.OCCUPANCY_GROUPS[i], group, pop)); 213 | } 214 | 215 | MaxOccupancyGroupCount = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 216 | for (int i = 0; i < MaxOccupancyGroupCount; i++) 217 | { 218 | uint group = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 219 | uint pop = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 220 | OccupancyGroupsLimits.Add(new OccupancyGroup(i, Constants.OCCUPANCY_GROUPS[i], group, pop)); 221 | } 222 | 223 | Logger.Log(LogLevel.Info, "RegionView subfile parsed"); 224 | } 225 | 226 | /// 227 | /// Prints out the contents of the Region View Subfile 228 | /// 229 | public void Dump() 230 | { 231 | Console.WriteLine("Major Version: {0}", MajorVersion); 232 | Console.WriteLine("Minor Version: {0}", MinorVersion); 233 | Console.WriteLine("Tile X Location: {0}", TileXLocation); 234 | Console.WriteLine("Tile Y Location: {0}", TileYLocation); 235 | Console.WriteLine("City Size X: {0}", CitySizeX); 236 | Console.WriteLine("City Size Y: {0}", CitySizeY); 237 | Console.WriteLine("Residential Population: {0}", ResidentialPopulation); 238 | Console.WriteLine("Commercial Population: {0}", CommercialPopulation); 239 | Console.WriteLine("Industrial Population: {0}", IndustrialPopulation); 240 | Console.WriteLine("Mayor Rating: {0}", MayorRating); 241 | Console.WriteLine("Star Count: {0}", StarCount); 242 | Console.WriteLine("Tutorial Flag: {0}", TutorialFlag); 243 | Console.WriteLine("City GUID: {0}", CityGuid); 244 | Console.WriteLine("Mode flag: {0} [{1}]", ModeFlag == Constants.GOD_MODE_FLAG ? "God Mode" : "Mayor Mode", ModeFlag); 245 | Console.WriteLine("City Name Length: {0}", CityNameLength); 246 | Console.WriteLine("City Name: {0}", CityName); 247 | Console.WriteLine("Former City Name Length: {0}", FormerCityNameLength); 248 | Console.WriteLine("Former City Name: {0}", FormerCityName); 249 | Console.WriteLine("Mayor Name Length: {0}", MayorNameLength); 250 | Console.WriteLine("Mayor Name: {0}", MayorName); 251 | Console.WriteLine("Internal Description Length: {0}", InternalDescriptionLength); 252 | Console.WriteLine("Internal Description: {0}", InternalDescription); 253 | Console.WriteLine("Default Mayor Name Length: {0}", DefaultMayorNameLength); 254 | Console.WriteLine("Default Mayor Name: {0}", DefaultMayorName); 255 | Console.WriteLine("Current Occupancy Group Count: {0}", CurrentOccupancyGroupCount); 256 | foreach (OccupancyGroup group in OccupancyGroupsCurrent) 257 | { 258 | group.Dump(); 259 | } 260 | Console.WriteLine("Max Occupancy Group Count: {0}", MaxOccupancyGroupCount); 261 | foreach (OccupancyGroup group in OccupancyGroupsMax) 262 | { 263 | group.Dump(); 264 | } 265 | Console.WriteLine("Limits Occupancy Group Count: {0}", LimitsOccupancyGroupCount); 266 | foreach (OccupancyGroup group in OccupancyGroupsLimits) 267 | { 268 | group.Dump(); 269 | } 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /SC4Parser/Structures/Building.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using SC4Parser.Logging; 5 | 6 | namespace SC4Parser 7 | { 8 | /// 9 | /// Representation of a building in Simcity 4, as it is stored in a save game 10 | /// 11 | /// 12 | /// Implemented from https://wiki.sc4devotion.com/index.php?title=Building_Subfile 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// // How to read and use building data using library 19 | /// // (this is effectively what is done in SC4Save.GetBuildingSubfile()) 20 | /// 21 | /// // Load save game 22 | /// SC4SaveFile savegame = null; 23 | /// try 24 | /// { 25 | /// savegame = new SC4SaveFile(@"C:\Path\To\Save\Game.sc4"); 26 | /// } 27 | /// catch (DBPFParsingException) 28 | /// { 29 | /// Console.Writeline("Issue occured while parsing DBPF"); 30 | /// return; 31 | /// } 32 | /// 33 | /// // load Building Subfile from save 34 | /// BuildingSubfile buildingSubfile = new BuildingSubfile(); 35 | /// try 36 | /// { 37 | /// IndexEntry buildingEntry = savegame.FindIndexEntryWithType("A9BD882D") 38 | /// byte[] buildingSubfileData = savegame.LoadIndexEntry(buildingEntry.TGI); 39 | /// buildingSubfile.Parse(buildingSubfileData, buildingSubfileData.Length 40 | /// } 41 | /// catch (Exception) 42 | /// { 43 | /// Console.Writeline("Error loading building subfile); 44 | /// } 45 | /// 46 | /// // loop through buildings and print out their TGIs 47 | /// foreach (Building building in buildingsSubfile.Buildings) 48 | /// { 49 | /// Console.Writeline(building.TGI.ToString(); 50 | /// } 51 | /// 52 | /// 53 | public class Building 54 | { 55 | /// 56 | /// Offset of building data within Building subfile 57 | /// 58 | public uint Offset { get; private set; } 59 | /// 60 | /// Size of building data 61 | /// 62 | public uint Size { get; private set; } 63 | /// 64 | /// CRC of building data 65 | /// 66 | public uint CRC { get; private set; } 67 | /// 68 | /// Memory reference of building data 69 | /// 70 | public uint Memory { get; private set; } 71 | /// 72 | /// Major version of building spec 73 | /// 74 | public ushort MajorVersion { get; private set; } 75 | /// 76 | /// Minor version of building spec 77 | /// 78 | public ushort MinorVersion { get; private set; } 79 | /// 80 | /// Zot word for building 81 | /// 82 | public ushort ZotWord { get; private set; } 83 | /// 84 | /// Unknown field 85 | /// 86 | public byte Unknown1 { get; private set; } 87 | /// 88 | /// Appearance flag for building. Can be one of the following values: 89 | /// 0x01 (00000001b) - Building that appears in the game (if this is off, the building has been deleted). 90 | /// 0x02 (00000010b) - ? (unused). 91 | /// 0x04 (00000100b) - ? (always on). 92 | /// 0x08 (00001000b) - Flora 93 | /// 0x40 (01000000b) - Burnt 94 | /// 95 | public byte AppearanceFlag { get; private set; } 96 | /// 97 | /// Unknown value, is always the same for all buildings 98 | /// 99 | public uint x278128A0 { get; private set; } 100 | /// 101 | /// Minimum tract X coordinate for building 102 | /// 103 | public byte MinTractX { get; private set; } 104 | /// 105 | /// Minimum tract Z coordinate for building 106 | /// 107 | public byte MinTractZ { get; private set; } 108 | /// 109 | /// Maximum tract X coordinate for building 110 | /// 111 | public byte MaxTractX { get; private set; } 112 | /// 113 | /// Maximum tract Z coordinate for building 114 | /// 115 | public byte MaxTractZ { get; private set; } 116 | /// 117 | /// Tract size on the X axis for building 118 | /// 119 | public ushort TractSizeX { get; private set; } 120 | /// 121 | /// Tract size on the Z axis for building 122 | /// 123 | public ushort TractSizeZ { get; private set; } 124 | /// 125 | /// Number of save game properties (SIGProps) associated with building 126 | /// 127 | /// 128 | public uint SaveGamePropertyCount { get; private set; } 129 | /// 130 | /// Save game properties (SIGProps) of building 131 | /// 132 | /// 133 | /// 134 | /// For a list of all possible building SIGPROPs visit the following: 135 | /// https://wiki.sc4devotion.com/index.php?title=Building_Subfile#Savegame_Properties_.28SGProps.29 136 | /// 137 | public List SaveGamePropertyEntries { get; private set; } = new List(); 138 | /// 139 | /// Unknown field 140 | /// 141 | public byte Unknown2 { get; private set; } 142 | /// 143 | /// The Building's Group ID 144 | /// 145 | public uint GroupID { get; private set; } 146 | /// 147 | /// The Building's Type ID 148 | /// 149 | public uint TypeID { get; private set; } 150 | /// 151 | /// The Building's Instance ID 152 | /// 153 | public uint InstanceID { get; private set; } 154 | /// 155 | /// Building's Instance Id when the the building appears 156 | /// 157 | public uint InstanceIDOnAppearance { get; private set; } 158 | /// 159 | /// Minimum X coordinate of building 160 | /// 161 | public float MinCoordinateX { get; private set; } 162 | /// 163 | /// Minimum Y coordinate of building 164 | /// 165 | public float MinCoordinateY { get; private set; } 166 | /// 167 | /// Minimum Z coordinate of building 168 | /// 169 | public float MinCoordinateZ { get; private set; } 170 | /// 171 | /// Maximum Z coordinate of building 172 | /// 173 | public float MaxCoordinateX { get; private set; } 174 | /// 175 | /// Maximum Y coordinate of building 176 | /// 177 | public float MaxCoordinateY { get; private set; } 178 | /// 179 | /// Maximum Z coordinate of building 180 | /// 181 | public float MaxCoordinateZ { get; private set; } 182 | /// 183 | /// Building's orientation 184 | /// 185 | /// 186 | /// 187 | /// 188 | /// 189 | public byte Orientation { get; private set; } 190 | /// 191 | /// Building's scaffolding height 192 | /// 193 | public float ScaffoldingHeight { get; private set; } 194 | 195 | /// 196 | /// TypeGroupInstance (TGI) of building, reference to prop exemplar 197 | /// 198 | /// 199 | /// Same as typeid, groupid and instanceid from this file. Just included it for accessibility 200 | /// 201 | /// 202 | public TypeGroupInstance TGI { get; private set; } = new TypeGroupInstance(); 203 | 204 | /// 205 | /// Load a building from a byte array 206 | /// 207 | /// Data to load building from 208 | /// Position in data to read building from 209 | /// 210 | /// Thrown when trying to parse an element that is out of bounds in the data array 211 | /// 212 | public void Parse(byte[] buffer, uint offset) 213 | { 214 | Offset = offset; 215 | Size = BitConverter.ToUInt32(buffer, 0); 216 | CRC = BitConverter.ToUInt32(buffer, 4); 217 | Memory = BitConverter.ToUInt32(buffer, 8); 218 | MajorVersion = BitConverter.ToUInt16(buffer, 12); 219 | MinorVersion = BitConverter.ToUInt16(buffer, 14); 220 | ZotWord = BitConverter.ToUInt16(buffer, 16); 221 | Unknown1 = buffer[18]; 222 | AppearanceFlag = buffer[19]; // TODO: this is always 5 (at the byte level) it is supposed to be 4.. 223 | x278128A0 = BitConverter.ToUInt32(buffer, 20); 224 | MinTractX = buffer[24]; 225 | MinTractZ = buffer[25]; 226 | MaxTractX = buffer[26]; 227 | MaxTractZ = buffer[27]; 228 | TractSizeX = BitConverter.ToUInt16(buffer, 28); 229 | TractSizeZ = BitConverter.ToUInt16(buffer, 30); 230 | SaveGamePropertyCount = BitConverter.ToUInt32(buffer, 32); 231 | 232 | // This represents the offset where data resumes after the SaveGame Properties entries (SGPROPs) 233 | // ExtractFromBuffer (if called) will update it to the offset after the SGPROPs 234 | uint saveGamePropertiesOffset = 36; 235 | 236 | if (SaveGamePropertyCount > 0) 237 | { 238 | SaveGamePropertyEntries = SaveGameProperty.ExtractFromBuffer(buffer, SaveGamePropertyCount, ref saveGamePropertiesOffset); 239 | } 240 | 241 | Unknown2 = buffer[saveGamePropertiesOffset + 0]; 242 | GroupID = BitConverter.ToUInt32(buffer, (int) saveGamePropertiesOffset + 1); 243 | TypeID = BitConverter.ToUInt32(buffer, (int) saveGamePropertiesOffset + 5); 244 | InstanceID = BitConverter.ToUInt32(buffer, (int) saveGamePropertiesOffset + 9); 245 | TGI = new TypeGroupInstance(TypeID, GroupID, InstanceID); 246 | InstanceIDOnAppearance = BitConverter.ToUInt32(buffer, (int) saveGamePropertiesOffset + 13); 247 | MinCoordinateX = BitConverter.ToSingle(buffer, (int) saveGamePropertiesOffset + 17); 248 | MinCoordinateY = BitConverter.ToSingle(buffer, (int) saveGamePropertiesOffset + 21); 249 | MinCoordinateZ = BitConverter.ToSingle(buffer, (int) saveGamePropertiesOffset + 25); 250 | MaxCoordinateX = BitConverter.ToSingle(buffer, (int) saveGamePropertiesOffset + 29); 251 | MaxCoordinateY = BitConverter.ToSingle(buffer, (int) saveGamePropertiesOffset + 33); 252 | MaxCoordinateZ = BitConverter.ToSingle(buffer, (int) saveGamePropertiesOffset + 37); 253 | Orientation = buffer[saveGamePropertiesOffset + 41]; 254 | ScaffoldingHeight = BitConverter.ToSingle(buffer, (int)saveGamePropertiesOffset + 42); 255 | 256 | // Sanity check out current offset to make sure we haven't missed anything 257 | if (saveGamePropertiesOffset + 46 != Size) 258 | { 259 | Logger.Log(LogLevel.Warning, "Building was not properly parsed ({0}/{1} read)", 260 | saveGamePropertiesOffset + 46, 261 | Size); 262 | } 263 | } 264 | 265 | /// 266 | /// Prints out the contents of the building 267 | /// 268 | public void Dump() 269 | { 270 | Console.WriteLine("Offset: {0} (0x{1})", Offset, Offset.ToString("x8")); 271 | Console.WriteLine("Size: {0} (0x{1})", Size, Size.ToString("x8")); 272 | Console.WriteLine("CRC: 0x{0}", CRC.ToString("x8")); 273 | Console.WriteLine("Memory address: 0x{0}", Memory.ToString("x8")); 274 | Console.WriteLine("Major Version: {0}", MajorVersion); 275 | Console.WriteLine("Minor Version: {0}", MinorVersion); 276 | Console.WriteLine("Zot Word: {0}", ZotWord); 277 | Console.WriteLine("Unknown1: {0}", Unknown1); 278 | Console.WriteLine("Appearance Flag: 0x{0}", AppearanceFlag.ToString("x8")); 279 | Console.WriteLine("0x278128A0: 0x{0}", x278128A0.ToString("x8")); 280 | Console.WriteLine("MinTractX: {0} (0x{1}) MaxTractX: {2} (0x{3})", 281 | MinTractX, 282 | MinTractX.ToString("x8"), 283 | MaxTractX, 284 | MaxTractX.ToString("x8")); 285 | Console.WriteLine("MinTractZ: {0} (0x{1}) MaxTractZ: {2} (0x{3})", 286 | MinTractZ, 287 | MinTractZ.ToString("x8"), 288 | MaxTractZ, 289 | MaxTractZ.ToString("x8")); 290 | Console.WriteLine("TractSizeX: {0}", TractSizeX); 291 | Console.WriteLine("TractSizeZ: {0}", TractSizeZ); 292 | Console.WriteLine("SaveGame Properties: {0}", SaveGamePropertyCount); 293 | 294 | // Dump any savegame properties if they are present 295 | if (SaveGamePropertyCount > 0) 296 | { 297 | for (int i = 0; i < SaveGamePropertyCount; i++) 298 | { 299 | Console.WriteLine("=================="); 300 | SaveGamePropertyEntries[i].Dump(); 301 | } 302 | } 303 | 304 | Console.WriteLine("Unknown2: {0}", Unknown2); 305 | Console.WriteLine("Group ID: {0} (0x{1})", GroupID, GroupID.ToString("x8")); 306 | Console.WriteLine("Type ID: {0} (0x{1})", TypeID, TypeID.ToString("x8")); 307 | Console.WriteLine("Instance ID: {0} (0x{1})", InstanceID, InstanceID.ToString("x8")); 308 | Console.WriteLine("Instance ID (on appearance): {0} (0x{1})", InstanceIDOnAppearance, InstanceIDOnAppearance.ToString("x8")); 309 | Console.WriteLine("Min Coordinate X: {0}", MinCoordinateX); 310 | Console.WriteLine("Min Coordinate Y: {0}", MinCoordinateY); 311 | Console.WriteLine("Min Coordinate Z: {0}", MinCoordinateZ); 312 | Console.WriteLine("Max Coordinate X: {0}", MaxCoordinateX); 313 | Console.WriteLine("Max Coordinate Y: {0}", MaxCoordinateY); 314 | Console.WriteLine("Max Coordinate Z: {0}", MaxCoordinateZ); 315 | Console.WriteLine("Orientation: {0} ({1})", Orientation, Constants.ORIENTATION_STRINGS[Orientation]); 316 | Console.WriteLine("Scaffolding Height: {0}", ScaffoldingHeight); 317 | Console.WriteLine("Prop Exemplar Reference: {0}", TGI.ToString()); 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /SC4Parser/SubFiles/NetworkIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SC4Parser 5 | { 6 | 7 | /// 8 | /// Network tile reference, this is the representation of a network tile that is 9 | /// stored in the Network Index Subfile 10 | /// 11 | /// 12 | public class NetworkTileReference 13 | { 14 | /// 15 | /// The tile's count in the city 16 | /// 17 | /// 18 | /// Tile numbering starts in the NW corner, which is tile number 0x00000000. 19 | /// - Tile number 0x00000001 is to the east of that tile. 20 | /// - In a small city, the first tile in the second row is 0x00000040. 21 | /// - In a medium city, the first tile in the second row is 0x00000080. 22 | /// - In a large city, the first tile in the second row is 0x00000100. 23 | /// - In a small city, the last tile in the last row is 0x00000FFF. 24 | /// - In a medium city, the last tile in the last row is 0x00003FFF. 25 | /// - In a large city, the last tile in the last row is 0x0000FFFF. 26 | /// (info from https://wiki.sc4devotion.com/index.php?title=Network_Subfiles#Network_Index_Subfile_Body) 27 | /// 28 | public uint TileNumber { private set; get; } 29 | /// 30 | /// Memory address of the network tile 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | public uint MemoryAddressRef { private set; get; } 37 | /// 38 | /// ID of the subfile that stores the network tile 39 | /// 40 | /// 41 | /// 42 | /// 43 | /// 44 | /// 45 | public uint SubfileTypeIDRef { private set; get; } 46 | 47 | /// 48 | /// Parses a single Network Tile reference. Returns offset after block has been parsed 49 | /// 50 | /// Data to parse block from 51 | /// Where to start parsing block 52 | /// 53 | /// Thrown when trying to parse an element that is out of bounds in the data array 54 | /// 55 | public void Parse(byte[] buffer, ref uint offset) 56 | { 57 | TileNumber = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref offset), 0); 58 | MemoryAddressRef = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref offset), 0); 59 | SubfileTypeIDRef = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref offset), 0); 60 | } 61 | 62 | /// 63 | /// Prints out the contents of the network block 64 | /// 65 | public void Dump() 66 | { 67 | Console.WriteLine("Tile Number: 0x{0}", TileNumber.ToString("x8")); 68 | Console.WriteLine("Tile Memory Reference: 0x{0}", MemoryAddressRef.ToString("x8")); 69 | Console.WriteLine("Tile Subfile: 0x{0}", SubfileTypeIDRef.ToString("x8")); 70 | } 71 | } 72 | 73 | /// 74 | /// Incomplete Network Index implementation. 75 | /// 76 | /// Implemented from https://wiki.sc4devotion.com/index.php?title=Network_Subfiles#Network_Index_Subfile_Body 77 | /// 78 | /// 79 | /// Partially completed. DO NOT USE, will probably crash. 80 | /// 81 | public class NetworkIndex 82 | { 83 | /// 84 | /// Size of subfile 85 | /// 86 | public uint SubfileSize { get; private set; } 87 | /// 88 | /// Subfile's CRC 89 | /// 90 | public uint CRC { get; private set; } 91 | /// 92 | /// Subfile's memory address 93 | /// 94 | public uint MemoryAddress { get; private set; } 95 | /// 96 | /// Major version of subfile 97 | /// 98 | public ushort MajorVersion { get; private set; } 99 | /// 100 | /// Number of tiles in city 101 | /// 102 | public uint CityTileCount { get; private set; } 103 | /// 104 | /// Number of network tiles in city 105 | /// 106 | public uint NetworkTileCount { get; private set; } 107 | 108 | /// 109 | /// List of all network tiles stored in the index file 110 | /// 111 | /// 112 | List NetworkTileReferences = new List(); 113 | 114 | /// 115 | /// Parses Network Index Subfile 116 | /// 117 | /// Buffer to read file from 118 | /// 119 | /// Incompleted. DO NOT USE, will probably crash. 120 | /// 121 | /// 122 | /// Thrown when trying to parse an element that is out of bounds in the data array 123 | /// 124 | public void Parse(byte[] buffer) 125 | { 126 | uint internalOffset = 0; 127 | 128 | SubfileSize = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 129 | CRC = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 130 | MemoryAddress = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 131 | MajorVersion = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 132 | CityTileCount = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 133 | NetworkTileCount = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 134 | 135 | for (int index = 0; index < NetworkTileCount; index++) 136 | { 137 | NetworkTileReference reference = new NetworkTileReference(); 138 | 139 | reference.Parse(buffer, ref internalOffset); 140 | NetworkTileReferences.Add(reference); // Add to list 141 | 142 | // Trying to parse some unknown reference stuff 143 | uint BlockCount = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 144 | 145 | // Traverse over this unknown block structure, we would skip it but has a very variable size so we just 146 | // go through it so we correctly have the offset incremented 147 | for (int blocknum = 0; blocknum < BlockCount; blocknum++) 148 | { 149 | uint BlockNumber = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 150 | uint Count = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 151 | 152 | //Console.WriteLine("========="); 153 | //Console.WriteLine(blocknum); 154 | //Console.WriteLine(Count); 155 | for (int blockchunks = 0; blockchunks < Count; blockchunks++) 156 | { 157 | //Console.WriteLine(blockchunks); 158 | internalOffset += 8; 159 | } 160 | } 161 | 162 | // Have to traverse over these because we don't know how big the index references are 163 | byte UnknownByte = Extensions.ReadByte(buffer, ref internalOffset); 164 | uint UnknownUint1 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 165 | uint UnknownUint2 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 166 | uint UnknownUint3 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 167 | uint UnknownUint4 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 168 | 169 | float UnknownFloat1 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 170 | ushort UnknownShort1 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 171 | ushort UnknownShort2 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 172 | ushort UnknownShort3 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 173 | ushort UnknownShort4 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 174 | float UnknownFloat2 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 175 | ushort UnknownShort5 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 176 | ushort UnknownShort6 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 177 | ushort UnknownShort7 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 178 | ushort UnknownShort8 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 179 | float UnknownFloat3 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 180 | ushort UnknownShort9 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 181 | ushort UnknownShort10 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 182 | ushort UnknownShort11 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 183 | ushort UnknownShort12 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 184 | float UnknownFloat4 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 185 | ushort UnknownShort13 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 186 | ushort UnknownShort14 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 187 | ushort UnknownShort15 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 188 | ushort UnknownShort16 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 189 | 190 | // there are a variable number of counts at the end, one of these might be a count 191 | ushort UnknownShort17 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 192 | uint UnknownUint5 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 193 | if (UnknownUint5 == 0) // Skip over this :/ 194 | { 195 | continue; 196 | } 197 | 198 | uint UnknownUint6 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 199 | float UnknownFloat5 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 200 | ushort UnknownShort18 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 201 | ushort UnknownShort19 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 202 | ushort UnknownShort20 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 203 | float UnknownFloat6 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 204 | ushort UnknownShort21 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 205 | 206 | //Console.WriteLine("#######################################"); 207 | //Console.WriteLine(TileNumber.ToString("x8")); 208 | //Console.WriteLine(MemoryAddressRef.ToString("x8")); 209 | //Console.WriteLine(SubfileTypeIDRef.ToString("x8")); 210 | //Console.WriteLine(BlockCount); 211 | 212 | //Console.WriteLine(UnknownByte); 213 | //Console.WriteLine(UnknownUint1); 214 | //Console.WriteLine(UnknownUint2); 215 | //Console.WriteLine(UnknownUint3); 216 | //Console.WriteLine(UnknownUint4); 217 | 218 | //Console.WriteLine(UnknownFloat1); 219 | //Console.WriteLine(UnknownShort1); 220 | //Console.WriteLine(UnknownShort2); 221 | //Console.WriteLine(UnknownShort3); 222 | //Console.WriteLine(UnknownShort4); 223 | //Console.WriteLine(UnknownFloat2); 224 | //Console.WriteLine(UnknownShort5); 225 | //Console.WriteLine(UnknownShort6); 226 | //Console.WriteLine(UnknownShort7); 227 | //Console.WriteLine(UnknownShort8); 228 | //Console.WriteLine(UnknownFloat3); 229 | //Console.WriteLine(UnknownShort9); 230 | //Console.WriteLine(UnknownShort10); 231 | //Console.WriteLine(UnknownShort11); 232 | //Console.WriteLine(UnknownShort12); 233 | //Console.WriteLine(UnknownFloat4); 234 | //Console.WriteLine(UnknownShort13); 235 | //Console.WriteLine(UnknownShort14); 236 | //Console.WriteLine(UnknownShort15); 237 | //Console.WriteLine(UnknownShort16); 238 | 239 | //Console.WriteLine("->"); 240 | //Console.WriteLine(UnknownShort17); 241 | //Console.WriteLine(UnknownUint5); 242 | //Console.WriteLine(UnknownUint6); 243 | //Console.WriteLine(UnknownFloat5); 244 | //Console.WriteLine(UnknownShort18); 245 | //Console.WriteLine(UnknownShort19); 246 | //Console.WriteLine(UnknownShort20); 247 | //Console.WriteLine(UnknownFloat6); 248 | //Console.WriteLine(UnknownShort21); 249 | 250 | 251 | 252 | } 253 | 254 | } 255 | 256 | /// 257 | /// Prints out the contents of the file 258 | /// 259 | public void Dump() 260 | { 261 | Console.WriteLine(SubfileSize); 262 | Console.WriteLine(CRC.ToString("X")); 263 | Console.WriteLine(MemoryAddress.ToString("X")); 264 | Console.WriteLine(MajorVersion); 265 | Console.WriteLine(CityTileCount); 266 | Console.WriteLine(NetworkTileCount); 267 | Console.WriteLine(); 268 | 269 | foreach (var reference in NetworkTileReferences) 270 | { 271 | Console.WriteLine("======================"); 272 | reference.Dump(); 273 | } 274 | } 275 | 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /SC4Parser/Structures/BridgeNetworkTile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SC4Parser 5 | { 6 | /// 7 | /// Representation of a city's bridge tiles which are found in the bridge network subfile (partial implementation) 8 | /// 9 | /// 10 | /// As the name suggests the bridge network subfile contains every bridge tile in a city. 11 | /// This was reverse engineered by me, it follows a similar structure to the other network tiles. 12 | /// 13 | /// This implementation is not complete (these tiles are big and they vary A LOT in size and I am not sure 14 | /// why) 15 | /// 16 | /// 17 | /// 18 | /// 19 | public class BridgeNetworkTile 20 | { 21 | /// 22 | /// Size of network tile entry 23 | /// 24 | public uint Size { get; private set; } 25 | /// 26 | /// Network tile's CRC 27 | /// 28 | public uint CRC { get; private set; } 29 | /// 30 | /// Network tile's memory address 31 | /// 32 | public uint Memory { get; private set; } 33 | /// 34 | /// Unknown version? 35 | /// 36 | public ushort UnknownVersion1 { get; private set; } 37 | /// 38 | /// Unknown version? 39 | /// 40 | public ushort UnknownVersion2 { get; private set; } 41 | /// 42 | /// Tile's ZOT bytes 43 | /// 44 | public ushort ZotBytes { get; private set; } 45 | /// 46 | /// Network tile's appearance flag 47 | /// 48 | public byte AppearanceFlag { get; private set; } 49 | /// 50 | /// Unknown uint, always 0xC772BF98 51 | /// 52 | public uint C772BF98 { get; private set; } 53 | /// 54 | /// Network tile's min x tract coordinate 55 | /// 56 | public byte MinTractX { get; private set; } 57 | /// 58 | /// Network tile's min z tract coordinate 59 | /// 60 | public byte MinTractZ { get; private set; } 61 | /// 62 | /// Network tile's max x tract coordinate 63 | /// 64 | public byte MaxTractX { get; private set; } 65 | /// 66 | /// Network tile's max z tract coordinate 67 | /// 68 | public byte MaxTractZ { get; private set; } 69 | /// 70 | /// Network tile's x tract size 71 | /// 72 | public ushort TractSizeX { get; private set; } 73 | /// 74 | /// Network tile's z tract size 75 | /// 76 | public ushort TractSizeZ { get; private set; } 77 | /// 78 | /// Network tile's Texture ID 79 | /// 80 | public uint TextureID { get; private set; } 81 | /// 82 | /// Network tile's orientation 83 | /// 84 | /// 85 | public byte Orientation { get; private set; } 86 | 87 | /// 88 | /// The network tile's type 89 | /// 90 | /// 91 | public byte NetworkType { get; private set; } 92 | 93 | /// 94 | /// Specifies if the network tile is connected on it's west side 95 | /// 96 | /// 97 | /// 0x0 for false, 0x2 for true. 98 | /// 99 | public byte WestConnection { get; private set; } 100 | /// 101 | /// Specifies if the network tile is connected on it's north side 102 | /// 103 | /// 104 | /// 0x0 for false, 0x2 for true. 105 | /// 106 | public byte NorthConnection { get; private set; } 107 | /// 108 | /// Specifies if the network tile is connected on it's east side 109 | /// 110 | /// 111 | /// 0x0 for false, 0x2 for true. 112 | /// 113 | public byte EastConnection { get; private set; } 114 | /// 115 | /// Specifies if the network tile is connected on it's south side 116 | /// 117 | /// 118 | /// 0x0 for false, 0x2 for true. 119 | /// 120 | public byte SouthConnection { get; private set; } 121 | 122 | /// 123 | /// Number of save game properties (sigprops) attached to the network tile 124 | /// 125 | /// 126 | public uint SaveGamePropertyCount { get; private set; } 127 | /// 128 | /// Network tile save game properties 129 | /// 130 | /// 131 | public List SaveGamePropertyEntries { get; private set; } = new List(); 132 | 133 | /// 134 | /// Maximum X size of the Network tile 135 | /// 136 | /// 137 | /// This seems to be a quarter of the network tile's actual size 138 | /// 139 | public float MaxSizeX { get; private set; } 140 | /// 141 | /// Maximum Y size of the Network tile 142 | /// 143 | /// 144 | /// This seems to be a quarter of the network tile's actual size 145 | /// 146 | public float MaxSizeY { get; private set; } 147 | /// 148 | /// Maximum Z size of the Network tile 149 | /// 150 | /// 151 | /// This seems to be a quarter of the network tile's actual size 152 | /// 153 | public float MaxSizeZ { get; private set; } 154 | /// 155 | /// Minimum X size of the Network tile 156 | /// 157 | /// 158 | /// This seems to be a quarter of the network tile's actual size 159 | /// 160 | public float MinSizeX { get; private set; } 161 | /// 162 | /// Minimum Y size of the Network tile 163 | /// 164 | /// 165 | /// This seems to be a quarter of the network tile's actual size 166 | /// 167 | public float MinSizeY { get; private set; } 168 | /// 169 | /// Minimum Z size of the Network tile 170 | /// 171 | /// 172 | /// This seems to be a quarter of the network tile's actual size 173 | /// 174 | public float MinSizeZ { get; private set; } 175 | 176 | /// 177 | /// Network tile X coordinate (1st set) 178 | /// 179 | public float PosX1 { get; private set; } 180 | /// 181 | /// Network tile Y coordinate (1st set) 182 | /// 183 | public float PosY1 { get; private set; } 184 | /// 185 | /// Network tile Z coordinate (1st set) 186 | /// 187 | public float PosZ1 { get; private set; } 188 | 189 | /// 190 | /// Network tile X coordinate (2nd set) 191 | /// 192 | public float PosX2 { get; private set; } 193 | /// 194 | /// Network tile Y coordinate (2nd set) 195 | /// 196 | public float PosY2 { get; private set; } 197 | /// 198 | /// Network tile Z coordinate (2nd set) 199 | /// 200 | public float PosZ2 { get; private set; } 201 | 202 | /// 203 | /// Network tile X coordinate (3rd set) 204 | /// 205 | public float PosX3 { get; private set; } 206 | /// 207 | /// Network tile Y coordinate (3rd set) 208 | /// 209 | public float PosY3 { get; private set; } 210 | /// 211 | /// Network tile Z coordinate (3rd set) 212 | /// 213 | public float PosZ3 { get; private set; } 214 | 215 | /// 216 | /// Network tile X coordinate (4th set) 217 | /// 218 | public float PosX4 { get; private set; } 219 | /// 220 | /// Network tile Y coordinate (4th set) 221 | /// 222 | public float PosY4 { get; private set; } 223 | /// 224 | /// Network tile Z coordinate (4th set) 225 | /// 226 | public float PosZ4 { get; private set; } 227 | 228 | /// 229 | /// Parses a bridge network tile (from Bridge network subfile) from a byte array 230 | /// 231 | /// Buffer to read tile from 232 | /// Position in the buffer to start reading data from 233 | /// 234 | /// Thrown when trying to parse an element that is out of bounds in the data array 235 | /// 236 | public void Parse(byte[] buffer, uint offset) 237 | { 238 | uint internalOffset = 0; 239 | 240 | Size = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 241 | CRC = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 242 | Memory = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 243 | //ushort UnknownUShort1 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 244 | internalOffset += 2; 245 | UnknownVersion1 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 246 | UnknownVersion2 = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 247 | ZotBytes = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 248 | //byte UnknownByte1 = Extensions.ReadByte(buffer, ref internalOffset); 249 | internalOffset++; 250 | AppearanceFlag = Extensions.ReadByte(buffer, ref internalOffset); 251 | C772BF98 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 252 | MinTractX = Extensions.ReadByte(buffer, ref internalOffset); 253 | MinTractZ = Extensions.ReadByte(buffer, ref internalOffset); 254 | MaxTractX = Extensions.ReadByte(buffer, ref internalOffset); 255 | MaxTractZ = Extensions.ReadByte(buffer, ref internalOffset); 256 | TractSizeX = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 257 | TractSizeZ = BitConverter.ToUInt16(Extensions.ReadBytes(buffer, 2, ref internalOffset), 0); 258 | SaveGamePropertyCount = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 259 | if (SaveGamePropertyCount > 0) 260 | SaveGamePropertyEntries = SaveGameProperty.ExtractFromBuffer(buffer, SaveGamePropertyCount, ref internalOffset); 261 | internalOffset += 13; 262 | MaxSizeX = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 263 | MaxSizeY = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 264 | MaxSizeZ = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 265 | MinSizeX = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 266 | MinSizeY = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 267 | MinSizeZ = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 268 | //float UnknownFloat1 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 269 | //float UnknownFloat2 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 270 | //uint UnknownUint1 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 271 | internalOffset += 12; 272 | PosX1 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 273 | PosY1 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 274 | PosZ1 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 275 | //float UnknownFloat3 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 276 | //float UnknownFloat4 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 277 | //uint UnknownUint2 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 278 | internalOffset += 12; 279 | PosX2 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 280 | PosY2 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 281 | PosZ2 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 282 | //float UnknownFloat5 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 283 | //float UnknownFloat6 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 284 | //uint UnknownUint3 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 285 | internalOffset += 12; 286 | PosX3 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 287 | PosY3 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 288 | PosZ3 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 289 | //float UnknownFloat7 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 290 | //float UnknownFloat8 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 291 | //uint UnknownUint4 = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 292 | internalOffset += 12; 293 | TextureID = BitConverter.ToUInt32(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 294 | internalOffset += 5; 295 | Orientation = Extensions.ReadByte(buffer, ref internalOffset); 296 | internalOffset += 3; 297 | NetworkType = Extensions.ReadByte(buffer, ref internalOffset); 298 | WestConnection = Extensions.ReadByte(buffer, ref internalOffset); 299 | NorthConnection = Extensions.ReadByte(buffer, ref internalOffset); 300 | EastConnection = Extensions.ReadByte(buffer, ref internalOffset); 301 | SouthConnection = Extensions.ReadByte(buffer, ref internalOffset); 302 | internalOffset += 8; 303 | PosX4 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 304 | PosY4 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 305 | PosZ4 = BitConverter.ToSingle(Extensions.ReadBytes(buffer, 4, ref internalOffset), 0); 306 | } 307 | 308 | /// 309 | /// Prints out the contents of the networktile 310 | /// 311 | public void Dump() 312 | { 313 | 314 | Console.WriteLine("Record Size: {0}", Size.ToString("X")); 315 | Console.WriteLine("CRC: 0x{0}", CRC.ToString("x")); 316 | Console.WriteLine("Memory: 0x{0}", Memory); 317 | Console.WriteLine("Major Version: {0}", UnknownVersion1); // Always 8 318 | Console.WriteLine("Minor Version: {0}", UnknownVersion2); // Always 4 319 | Console.WriteLine("Zot Bytes: {0}", ZotBytes); // ALways 0 320 | Console.WriteLine("Appearance Flag: {0}", AppearanceFlag); // Always 5; 321 | Console.WriteLine("0xC772BF98: 0x{0}", C772BF98.ToString("x")); // Always same 322 | Console.WriteLine("Min Tract X: 0x{0}", MinTractX.ToString("x")); 323 | Console.WriteLine("Min Tract Z: 0x{0}", MinTractZ.ToString("x")); 324 | Console.WriteLine("Max Tract X: 0x{0}", MaxTractX.ToString("x")); 325 | Console.WriteLine("Max Tract Z: 0x{0}", MaxTractZ.ToString("x")); 326 | Console.WriteLine("Tract Size X: {0}", TractSizeX); // Always 2 327 | Console.WriteLine("Tract Size Z: {0}", TractSizeZ); // Always 2 328 | Console.WriteLine("Properties Count: {0}", SaveGamePropertyCount); // Between 1 and 2 (1 seems to be a height) 329 | 330 | // Dump any savegame properties if they are present 331 | if (SaveGamePropertyCount > 0) 332 | { 333 | for (int i = 0; i < SaveGamePropertyCount; i++) 334 | { 335 | Console.WriteLine("=================="); 336 | SaveGamePropertyEntries[i].Dump(); 337 | } 338 | } 339 | 340 | Console.WriteLine("Max Size X: {0}", MaxSizeX); 341 | Console.WriteLine("Max Size Y: {0}", MaxSizeY); 342 | Console.WriteLine("Max Size Z: {0}", MaxSizeZ); 343 | Console.WriteLine("Min Size X: {0}", MinSizeX); 344 | Console.WriteLine("Min Size Y: {0}", MinSizeY); 345 | Console.WriteLine("Min Size Z: {0}", MinSizeZ); 346 | Console.WriteLine("Pos X (1): {0}", PosX1); 347 | Console.WriteLine("Pos Y (1): {0}", PosY1); 348 | Console.WriteLine("Pos Z (1): {0}", PosZ1); 349 | Console.WriteLine("Pos X (2): {0}", PosX2); 350 | Console.WriteLine("Pos Y (2): {0}", PosY2); 351 | Console.WriteLine("Pos Z (2): {0}", PosZ2); 352 | Console.WriteLine("Pos X (3): {0}", PosX3); 353 | Console.WriteLine("Pos Y (3): {0}", PosY3); 354 | Console.WriteLine("Pos Z (3): {0}", PosZ3); 355 | Console.WriteLine("TextureID: {0}", TextureID.ToString("X")); 356 | Console.WriteLine("Orientation: {0}", Orientation.ToString("X")); 357 | Console.WriteLine("Network Type: {0}", NetworkType.ToString("X")); 358 | Console.WriteLine("West Connection: {0} ", WestConnection.ToString("X")); 359 | Console.WriteLine("North Connection: {0} ", NorthConnection.ToString("X")); 360 | Console.WriteLine("East Connection: {0} ", EastConnection.ToString("X")); 361 | Console.WriteLine("South Connection: {0} ", SouthConnection.ToString("X")); 362 | Console.WriteLine("Pos X (4): {0}", PosX4); 363 | Console.WriteLine("Pos Y (4): {0}", PosY4); 364 | Console.WriteLine("Pos Z (4): {0}", PosZ4); 365 | } 366 | } 367 | } 368 | --------------------------------------------------------------------------------