├── 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