diff --git a/.gitignore b/.gitignore
index 21f755d..82c2d45 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ profile.arm.json
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
+launchSettings.json
# Build results
[Dd]ebug/
diff --git a/GPatch/CopyBinaries.bat b/GPatch/CopyBinaries.bat
index 86d3d3f..9afbd4e 100644
--- a/GPatch/CopyBinaries.bat
+++ b/GPatch/CopyBinaries.bat
@@ -1,12 +1,18 @@
xcopy "%GIANTS_PATH%\gg_dx7r.dll" "Files\" /Y
xcopy "%GIANTS_PATH%\gg_dx9r.dll" "Files\" /Y
+xcopy "%GIANTS_PATH%\dedicated.exe" "Files\" /Y
xcopy "%GIANTS_PATH%\Giants.exe" "Files\" /Y
xcopy "%GIANTS_PATH%\GiantsMain.exe" "Files\" /Y
xcopy "%GIANTS_PATH%\GiantsDedicated.exe" "Files\" /Y
xcopy "%GIANTS_PATH%\gs_ds.dll" "Files\" /Y
xcopy "%GIANTS_PATH%\Giants.WebApi.Clients.dll" "Files\" /Y
xcopy "%GIANTS_PATH%\fmt.dll" "Files\" /Y
-xcopy "%GIANTS_PATH%\BugTrap.dll" "Files\" /Y
+xcopy "%GIANTS_PATH%\crashrpt_lang.ini" "Files\" /Y
+xcopy "%GIANTS_PATH%\CrashRpt1403.dll" "Files\" /Y
+xcopy "%GIANTS_PATH%\CrashSender1403.exe" "Files\" /Y
+xcopy "%GIANTS_PATH%\dbghelp.dll" "Files\" /Y
xcopy "%GIANTS_PATH%\cpprest_2_10.dll" "Files\" /Y
xcopy "%GIANTS_PATH%\Newtonsoft.Json.dll" "Files\" /Y
-xcopy "%GIANTS_PATH%\zlib1.dll" "Files\" /Y
\ No newline at end of file
+xcopy "%GIANTS_PATH%\zlib1.dll" "Files\" /Y
+
+pause
\ No newline at end of file
diff --git a/GPatch/GPatch.nsi b/GPatch/GPatch.nsi
index 3f12d3c..d466c76 100644
--- a/GPatch/GPatch.nsi
+++ b/GPatch/GPatch.nsi
@@ -42,7 +42,7 @@ SetCompressor /SOLID lzma
; MUI end ------
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
-OutFile "Output\GPatch1_498_204_0.exe"
+OutFile "Output\GPatch1_498_206_0.exe"
InstallDir "$PROGRAMFILES\Giants\"
InstallDirRegKey HKCU "SOFTWARE\PlanetMoon\Giants" "DestDir"
ShowInstDetails hide
@@ -77,6 +77,7 @@ Section
Delete $INSTDIR\gg_dx8r.dll
Delete $INSTDIR\gg_dx9r.dll
Delete $INSTDIR\Giants.exe
+ Delete $INSTDIR\BugTrap.dll
Delete $INSTDIR\GiantsMain.exe
Delete $INSTDIR\*.vso
Delete $INSTDIR\*.pso
diff --git a/Giants.BinTools/Extensions.cs b/Giants.BinTools/Extensions.cs
new file mode 100644
index 0000000..5e1267d
--- /dev/null
+++ b/Giants.BinTools/Extensions.cs
@@ -0,0 +1,38 @@
+namespace Giants.BinTools
+{
+ using System.IO;
+ using System.Text;
+
+ public static class Extensions
+ {
+ ///
+ /// Reads a null-terminated C-string from the binary reader.
+ ///
+ public static string ReadCString(this BinaryReader reader)
+ {
+ var stringBuilder = new StringBuilder();
+
+ while (true)
+ {
+ char c = reader.ReadChar();
+ if (c == '\0')
+ {
+ break;
+ }
+
+ stringBuilder.Append(c);
+ }
+
+ return stringBuilder.ToString();
+ }
+
+ ///
+ /// Writes a null-terminated C-string to the binary writer.
+ ///
+ public static void WriteCString(this BinaryWriter writer, string value)
+ {
+ writer.Write(Encoding.UTF8.GetBytes(value));
+ writer.Write('\0');
+ }
+ }
+}
diff --git a/Giants.BinTools/Giants.BinTools.csproj b/Giants.BinTools/Giants.BinTools.csproj
new file mode 100644
index 0000000..bcda015
--- /dev/null
+++ b/Giants.BinTools/Giants.BinTools.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Library
+ netcoreapp3.1
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/Giants.BinTools/Macro/DataDefinitionMacroLine.cs b/Giants.BinTools/Macro/DataDefinitionMacroLine.cs
new file mode 100644
index 0000000..0ee1943
--- /dev/null
+++ b/Giants.BinTools/Macro/DataDefinitionMacroLine.cs
@@ -0,0 +1,112 @@
+namespace Giants.BinTools.Macro
+{
+ using System;
+ using System.IO;
+ using Giants.BinTools;
+ using Giants.BinTools.Symbol;
+ using Newtonsoft.Json;
+
+ public class DataDefinitionMacroLine : MacroLine
+ {
+ public MacroLineType Type => MacroLineType.DataDefinition;
+
+ [JsonProperty]
+ public string Instruction { get; private set; }
+
+ [JsonProperty]
+ public string Constant { get; private set; }
+
+ [JsonProperty]
+ public string ConstantName { get; private set; }
+
+ [JsonProperty]
+ public string ArgumentPrefix { get; private set; }
+
+ [JsonProperty]
+ public int ArgumentIndex { get; private set; }
+
+ public DataDefinitionMacroLine(string instruction, string[] lineData, SymbolTable symbolTable)
+ {
+ this.Instruction = instruction;
+
+ string data = lineData[1];
+
+ int argIndexRef = data.IndexOf("\\");
+ if (argIndexRef > 0)
+ {
+ this.ArgumentPrefix = data[0..argIndexRef];
+ data = data[argIndexRef..];
+ }
+
+ if (data.StartsWith("\\"))
+ {
+ string index = data[1..];
+ if (index != "#")
+ {
+ this.ArgumentIndex = Convert.ToInt32(data[1..]);
+ }
+ }
+ else
+ {
+ string groupName = KnownSymbolGroupNames.GetGroupName(data);
+ if (groupName != null)
+ {
+ this.ConstantName = data;
+ if (symbolTable.TryGetSymbolFromName(groupName, data, out int symbolValue))
+ {
+ this.Constant = symbolValue.ToString();
+ }
+ }
+
+ if (this.Constant == null)
+ {
+ this.Constant = data;
+ }
+ }
+ }
+
+ internal DataDefinitionMacroLine() { }
+
+ public string Deserialize(BinaryReader reader, SymbolTable symbolTable)
+ {
+ string value = this.Instruction switch
+ {
+ MacroInstruction.DefByte => reader.ReadByte().ToString(),
+ MacroInstruction.DefFloat => reader.ReadSingle().ToString(),
+ MacroInstruction.DefLong => reader.ReadInt32().ToString(),
+ MacroInstruction.Name0cs => reader.ReadCString(),
+ _ => throw new NotSupportedException()
+ };
+
+ if (!string.IsNullOrEmpty(this.ArgumentPrefix))
+ {
+ if (int.TryParse(value, out int intValue) && symbolTable.TryGetSymbolFromId(this.ArgumentPrefix, intValue, out string symbolName))
+ {
+ return symbolName[this.ArgumentPrefix.Length..];
+ }
+ }
+
+ return value;
+ }
+
+ public void Serialize(string token, SymbolTable symbolTable, BinaryWriter binaryWriter)
+ {
+ if (!string.IsNullOrEmpty(this.ArgumentPrefix))
+ {
+ if (symbolTable.TryGetSymbolFromName(this.ArgumentPrefix, this.ArgumentPrefix + token, out int symbolValue))
+ {
+ token = symbolValue.ToString();
+ }
+ }
+
+ switch (this.Instruction)
+ {
+ case MacroInstruction.DefByte: binaryWriter.Write((byte)Convert.ChangeType(token, typeof(byte))); break;
+ case MacroInstruction.DefFloat: binaryWriter.Write((float)Convert.ChangeType(token, typeof(float))); break;
+ case MacroInstruction.DefLong: binaryWriter.Write((int)Convert.ChangeType(token, typeof(int))); break;
+ case MacroInstruction.Name0cs: binaryWriter.WriteCString(token); break;
+ default: throw new NotSupportedException();
+ }
+ }
+ }
+}
diff --git a/Giants.BinTools/Macro/ElseMacroLine.cs b/Giants.BinTools/Macro/ElseMacroLine.cs
new file mode 100644
index 0000000..a0e6e6f
--- /dev/null
+++ b/Giants.BinTools/Macro/ElseMacroLine.cs
@@ -0,0 +1,7 @@
+namespace Giants.BinTools.Macro
+{
+ public class ElseMacroLine : MacroLine
+ {
+ public MacroLineType Type => MacroLineType.Else;
+ }
+}
diff --git a/Giants.BinTools/Macro/EndIfMacroLine.cs b/Giants.BinTools/Macro/EndIfMacroLine.cs
new file mode 100644
index 0000000..75f4547
--- /dev/null
+++ b/Giants.BinTools/Macro/EndIfMacroLine.cs
@@ -0,0 +1,7 @@
+namespace Giants.BinTools.Macro
+{
+ public class EndIfMacroLine : MacroLine
+ {
+ public MacroLineType Type => MacroLineType.EndIf;
+ }
+}
diff --git a/Giants.BinTools/Macro/GroupUseMacroLine.cs b/Giants.BinTools/Macro/GroupUseMacroLine.cs
new file mode 100644
index 0000000..28ca712
--- /dev/null
+++ b/Giants.BinTools/Macro/GroupUseMacroLine.cs
@@ -0,0 +1,16 @@
+namespace Giants.BinTools.Macro
+{
+ public class GroupUseMacroLine : MacroLine
+ {
+ public MacroLineType Type => MacroLineType.GroupUse;
+
+ public string GroupName { get; set; }
+
+ public GroupUseMacroLine(string[] tokens)
+ {
+ this.GroupName = tokens[1];
+ }
+
+ internal GroupUseMacroLine() { }
+ }
+}
diff --git a/Giants.BinTools/Macro/IfMacroLine.cs b/Giants.BinTools/Macro/IfMacroLine.cs
new file mode 100644
index 0000000..05fc977
--- /dev/null
+++ b/Giants.BinTools/Macro/IfMacroLine.cs
@@ -0,0 +1,16 @@
+namespace Giants.BinTools.Macro
+{
+ public class IfMacroLine : MacroLine
+ {
+ public MacroLineType Type => MacroLineType.If;
+
+ public string Condition { get; }
+
+ public IfMacroLine(string[] tokens)
+ {
+ this.Condition = string.Join(" ", tokens[1..]);
+ }
+
+ internal IfMacroLine() { }
+ }
+}
diff --git a/Giants.BinTools/Macro/KnownMacroGroupNames.cs b/Giants.BinTools/Macro/KnownMacroGroupNames.cs
new file mode 100644
index 0000000..57f01ff
--- /dev/null
+++ b/Giants.BinTools/Macro/KnownMacroGroupNames.cs
@@ -0,0 +1,7 @@
+namespace Giants.BinTools.Macro
+{
+ public static class KnownMacroGroupNames
+ {
+ public const string FxDefGroup = "fxdefgroup";
+ }
+}
diff --git a/Giants.BinTools/Macro/KnownSymbolGroupNames.cs b/Giants.BinTools/Macro/KnownSymbolGroupNames.cs
new file mode 100644
index 0000000..d0f3e24
--- /dev/null
+++ b/Giants.BinTools/Macro/KnownSymbolGroupNames.cs
@@ -0,0 +1,29 @@
+namespace Giants.BinTools.Macro
+{
+ using System;
+ using System.Collections.Generic;
+
+ public static class KnownSymbolGroupNames
+ {
+ public const string Sfx = "sfx";
+ public const string Fx = "Fx";
+ public const string Object = "ObjObj";
+ public const string ObjectData = "ObjData";
+ public const string ObjectGroup = "ObjGroup";
+
+ public static readonly IList Names = new[] { Sfx, Fx, Object, ObjectData, ObjectGroup };
+
+ public static string GetGroupName(string str)
+ {
+ foreach (string groupName in Names)
+ {
+ if (str.StartsWith(groupName, StringComparison.OrdinalIgnoreCase))
+ {
+ return groupName;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Giants.BinTools/Macro/MacroDefinition.cs b/Giants.BinTools/Macro/MacroDefinition.cs
new file mode 100644
index 0000000..bcb8ad5
--- /dev/null
+++ b/Giants.BinTools/Macro/MacroDefinition.cs
@@ -0,0 +1,78 @@
+namespace Giants.BinTools.Macro
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using Giants.BinTools.Symbol;
+ using NLog;
+
+ public class MacroDefinition
+ {
+ private static readonly Logger logger = LogManager.GetLogger(nameof(MacroDefinition));
+
+ private static readonly string[] SplitCharacters = new string[] { " ", "\t" };
+
+ public IDictionary> Groups { get; }
+
+ public string Name { get; set; }
+
+ public MacroDefinition(string name, IDictionary> groups = null)
+ {
+ this.Name = name;
+ this.Groups = groups ?? new Dictionary>();
+ }
+
+ public void Read(StreamReader reader, SymbolTable symbolTable)
+ {
+ string activeGroup = string.Empty;
+
+ while (true)
+ {
+ string line = reader.ReadLine();
+ if (line == null)
+ {
+ throw new InvalidOperationException($"Unexpected end of macro definition in '{this.Name}'");
+ }
+
+ line = line.Trim();
+ if (line.StartsWith(";") || string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ if (line.StartsWith(MacroInstruction.MacroDefinitionEnd))
+ {
+ break;
+ }
+
+ string[] opcodeData = line.Split(SplitCharacters, StringSplitOptions.RemoveEmptyEntries);
+
+ var macroLine = MacroLineFactory.Read(opcodeData, symbolTable);
+ if (macroLine == null)
+ {
+ continue;
+ }
+
+ if (!this.Groups.Any() && !(macroLine is GroupUseMacroLine))
+ {
+ logger.Warn($"Warning: expected 'groupuse' for macro {this.Name}; this may be a bug");
+
+ // Try to recover
+ this.Groups.TryAdd("-MISSING-", new List());
+ activeGroup = "-MISSING-";
+ }
+
+ if (macroLine is GroupUseMacroLine groupUseMacroLine)
+ {
+ this.Groups[groupUseMacroLine.GroupName] = new List();
+ activeGroup = groupUseMacroLine.GroupName;
+ }
+ else
+ {
+ this.Groups[activeGroup].Add(macroLine);
+ }
+ }
+ }
+ }
+}
diff --git a/Giants.BinTools/Macro/MacroDefinitionTable.cs b/Giants.BinTools/Macro/MacroDefinitionTable.cs
new file mode 100644
index 0000000..d3b2bb5
--- /dev/null
+++ b/Giants.BinTools/Macro/MacroDefinitionTable.cs
@@ -0,0 +1,108 @@
+namespace Giants.BinTools.Macro
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+ using Giants.BinTools.Symbol;
+ using NLog;
+
+ public class MacroDefinitionTable
+ {
+ private static readonly Logger logger = LogManager.GetLogger(nameof(MacroDefinition));
+
+ private static readonly string[] SplitCharacters = new string[] { " ", "\t" };
+
+ private readonly HashSet includedFiles = new HashSet();
+
+ public SymbolTable SymbolTable { get; } = new SymbolTable();
+
+ public IList MacroDefinitions { get; } = new List();
+
+ public static MacroDefinitionTable GenerateFromLegacyBuildSystem(string bldFilePath)
+ {
+ var table = new MacroDefinitionTable();
+ table.ProcessFile(bldFilePath);
+
+ return table;
+ }
+
+ private void ProcessFile(string bldFilePath)
+ {
+ using FileStream stream = File.OpenRead(bldFilePath);
+ using StreamReader streamReader = new StreamReader(stream);
+
+ string currentLine;
+ do
+ {
+ currentLine = streamReader.ReadLine();
+ if (currentLine == null)
+ {
+ break;
+ }
+
+ currentLine = currentLine.Trim();
+ string[] tokens = currentLine.Split(SplitCharacters, StringSplitOptions.RemoveEmptyEntries);
+
+ if (currentLine.StartsWith(";") || !tokens.Any())
+ {
+ continue;
+ }
+ else if (tokens[0].Equals(MacroInstruction.MacroDefinitionStart, StringComparison.OrdinalIgnoreCase))
+ {
+ MacroDefinition macroDefinition = this.ReadDefinition(currentLine, streamReader);
+ this.MacroDefinitions.Add(macroDefinition);
+ }
+ else if (tokens[0].Equals(MacroInstruction.IncludeFile, StringComparison.OrdinalIgnoreCase))
+ {
+ string includeFile = tokens[1];
+ if (this.includedFiles.Contains(includeFile))
+ {
+ continue;
+ }
+
+ string directory = Directory.GetParent(bldFilePath).FullName;
+ string includeFilePath = Path.Combine(directory, includeFile);
+ if (string.IsNullOrEmpty(Path.GetExtension(includeFilePath)))
+ {
+ includeFilePath = includeFilePath + ".bld";
+ }
+
+ this.ProcessFile(includeFilePath);
+ }
+ else if (tokens[0].Equals(MacroInstruction.Define, StringComparison.OrdinalIgnoreCase))
+ {
+ if (int.TryParse(tokens[2], NumberStyles.Number, CultureInfo.InvariantCulture, out int symbolValue))
+ {
+ string groupName = KnownSymbolGroupNames.GetGroupName(tokens[1]);
+ if (groupName != null)
+ {
+ this.SymbolTable.AddSymbol(groupName, tokens[1], symbolValue);
+ }
+ }
+ else
+ {
+ logger.Warn($"Failed to parse symbol '{tokens[2]}'; only integer constants are supported");
+ }
+ }
+
+ } while (currentLine != null);
+ }
+
+ MacroDefinition ReadDefinition(string line, StreamReader reader)
+ {
+ string[] opcodeData = line.Split(SplitCharacters, StringSplitOptions.RemoveEmptyEntries);
+
+ // Save definition name
+ string macroName = opcodeData[1];
+
+ var macroDefinition = new MacroDefinition(macroName);
+ macroDefinition.Read(reader, this.SymbolTable);
+
+ return macroDefinition;
+ }
+
+ private MacroDefinitionTable() { }
+ }
+}
diff --git a/Giants.BinTools/Macro/MacroInstruction.cs b/Giants.BinTools/Macro/MacroInstruction.cs
new file mode 100644
index 0000000..39c764e
--- /dev/null
+++ b/Giants.BinTools/Macro/MacroInstruction.cs
@@ -0,0 +1,18 @@
+namespace Giants.BinTools.Macro
+{
+ public static class MacroInstruction
+ {
+ public const string IncludeFile = "include";
+ public const string MacroDefinitionStart = "macro";
+ public const string MacroDefinitionEnd = "macend";
+ public const string Define = "#define";
+ public const string DefByte = "db";
+ public const string DefLong = "dl";
+ public const string DefFloat = "df";
+ public const string Name0cs = "name0cs";
+ public const string GroupUse = "groupuse";
+ public const string If = "if";
+ public const string Else = "else";
+ public const string EndIf = "endif";
+ }
+}
diff --git a/Giants.BinTools/Macro/MacroLine.cs b/Giants.BinTools/Macro/MacroLine.cs
new file mode 100644
index 0000000..cb5fd8d
--- /dev/null
+++ b/Giants.BinTools/Macro/MacroLine.cs
@@ -0,0 +1,10 @@
+using Newtonsoft.Json;
+
+namespace Giants.BinTools.Macro
+{
+ [JsonConverter(typeof(MacroLineJsonConverter))]
+ public interface MacroLine
+ {
+ public MacroLineType Type { get; }
+ }
+}
diff --git a/Giants.BinTools/Macro/MacroLineFactory.cs b/Giants.BinTools/Macro/MacroLineFactory.cs
new file mode 100644
index 0000000..1b4d754
--- /dev/null
+++ b/Giants.BinTools/Macro/MacroLineFactory.cs
@@ -0,0 +1,24 @@
+namespace Giants.BinTools.Macro
+{
+ using Giants.BinTools.Symbol;
+
+ public static class MacroLineFactory
+ {
+ public static MacroLine Read(string[] tokens, SymbolTable symbolTable)
+ {
+ string instruction = tokens[0].ToLowerInvariant().Trim();
+ return instruction switch
+ {
+ MacroInstruction.GroupUse => new GroupUseMacroLine(tokens),
+ MacroInstruction.DefByte => new DataDefinitionMacroLine(instruction, tokens, symbolTable),
+ MacroInstruction.DefFloat => new DataDefinitionMacroLine(instruction, tokens, symbolTable),
+ MacroInstruction.DefLong => new DataDefinitionMacroLine(instruction, tokens, symbolTable),
+ MacroInstruction.Name0cs => new DataDefinitionMacroLine(instruction, tokens, symbolTable),
+ MacroInstruction.If => new IfMacroLine(tokens),
+ MacroInstruction.Else => new ElseMacroLine(),
+ MacroInstruction.EndIf => new EndIfMacroLine(),
+ _ => null,
+ };
+ }
+ }
+}
diff --git a/Giants.BinTools/Macro/MacroLineJsonConverter.cs b/Giants.BinTools/Macro/MacroLineJsonConverter.cs
new file mode 100644
index 0000000..22cc8ee
--- /dev/null
+++ b/Giants.BinTools/Macro/MacroLineJsonConverter.cs
@@ -0,0 +1,47 @@
+namespace Giants.BinTools.Macro
+{
+ using System;
+ using Newtonsoft.Json;
+ using Newtonsoft.Json.Converters;
+ using Newtonsoft.Json.Linq;
+
+ public class MacroLineJsonConverter : CustomCreationConverter
+ {
+ private MacroLineType type;
+ public override bool CanConvert(Type objectType)
+ {
+ return (objectType == typeof(MacroLine));
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ var jObject = JObject.ReadFrom(reader);
+ this.type = jObject["Type"].ToObject();
+ return base.ReadJson(jObject.CreateReader(), objectType, existingValue, serializer);
+ }
+
+ public override bool CanWrite
+ {
+ get { return false; }
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override MacroLine Create(Type objectType)
+ {
+ switch (this.type)
+ {
+ case MacroLineType.DataDefinition: return new DataDefinitionMacroLine();
+ case MacroLineType.Else: return new ElseMacroLine();
+ case MacroLineType.EndIf: return new EndIfMacroLine();
+ case MacroLineType.GroupUse: return new GroupUseMacroLine();
+ case MacroLineType.If: return new IfMacroLine();
+ }
+
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/Giants.BinTools/Macro/MacroLineType.cs b/Giants.BinTools/Macro/MacroLineType.cs
new file mode 100644
index 0000000..12e44a1
--- /dev/null
+++ b/Giants.BinTools/Macro/MacroLineType.cs
@@ -0,0 +1,12 @@
+namespace Giants.BinTools.Macro
+{
+ public enum MacroLineType
+ {
+ None = 0,
+ DataDefinition,
+ If,
+ Else,
+ EndIf,
+ GroupUse
+ }
+}
diff --git a/Giants.BinTools/Symbol/SymbolTable.cs b/Giants.BinTools/Symbol/SymbolTable.cs
new file mode 100644
index 0000000..ac33d36
--- /dev/null
+++ b/Giants.BinTools/Symbol/SymbolTable.cs
@@ -0,0 +1,83 @@
+namespace Giants.BinTools.Symbol
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Runtime.Serialization;
+ using Newtonsoft.Json;
+
+ public class SymbolTable
+ {
+ private static readonly HashSet ExcludedSymbols = new HashSet() { "FxBinVersion", "SfxVersionOld", "SfxVersion1", "SfxVersion2" };
+
+ [JsonProperty(nameof(symbols))]
+ private IDictionary> symbols = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ private IDictionary> reverseSymbolLookup = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ [OnDeserialized]
+ internal void OnDeserialized(StreamingContext context)
+ {
+ foreach (var symbolGroup in this.symbols)
+ {
+ this.reverseSymbolLookup.Add(symbolGroup.Key, new Dictionary());
+ foreach (var symbol in this.symbols[symbolGroup.Key])
+ {
+ this.reverseSymbolLookup[symbolGroup.Key].Add(symbol.Value, symbol.Key);
+ }
+ }
+ }
+
+ public void AddSymbol(string symbolGroup, string symbolName, int symbolValue)
+ {
+ if (ExcludedSymbols.Contains(symbolName))
+ {
+ return;
+ }
+
+ if (!this.symbols.ContainsKey(symbolGroup))
+ {
+ this.symbols.Add(symbolGroup, new CaseInsensitiveDictionary());
+ }
+
+ if (!this.reverseSymbolLookup.ContainsKey(symbolGroup))
+ {
+ this.reverseSymbolLookup.Add(symbolGroup, new Dictionary());
+ }
+
+ this.symbols[symbolGroup].Add(symbolName, symbolValue);
+ this.reverseSymbolLookup[symbolGroup].Add(symbolValue, symbolName);
+ }
+
+ public bool ContainsKey(string symbolGroup, string key)
+ {
+ if (!string.IsNullOrEmpty(symbolGroup) && !string.IsNullOrEmpty(key))
+ {
+ return this.symbols.ContainsKey(symbolGroup)
+ && this.symbols[symbolGroup].ContainsKey(key);
+ }
+
+ return false;
+ }
+
+ public bool TryGetSymbolFromName(string symbolGroup, string symbolName, out int symbolValue)
+ {
+ return this.symbols[symbolGroup].TryGetValue(symbolName, out symbolValue);
+ }
+
+ public bool TryGetSymbolFromId(string symbolGroup, int symbolValue, out string symbolName)
+ {
+ return this.reverseSymbolLookup[symbolGroup].TryGetValue(symbolValue, out symbolName);
+ }
+
+ public IDictionary GetSymbolGroup(string symbolGroup)
+ {
+ return this.symbols[symbolGroup];
+ }
+
+ private class CaseInsensitiveDictionary : Dictionary
+ {
+ public CaseInsensitiveDictionary()
+ : base(StringComparer.OrdinalIgnoreCase) { }
+ }
+ }
+}
diff --git a/Giants.DataContract/Contracts/V1/AppVersion.cs b/Giants.DataContract/Contracts/V1/AppVersion.cs
index 5bcf9c8..f4ab976 100644
--- a/Giants.DataContract/Contracts/V1/AppVersion.cs
+++ b/Giants.DataContract/Contracts/V1/AppVersion.cs
@@ -20,15 +20,15 @@
public override bool Equals(object obj)
{
return obj is AppVersion info &&
- Build == info.Build &&
- Major == info.Major &&
- Minor == info.Minor &&
- Revision == info.Revision;
+ this.Build == info.Build &&
+ this.Major == info.Major &&
+ this.Minor == info.Minor &&
+ this.Revision == info.Revision;
}
public override int GetHashCode()
{
- return HashCode.Combine(Build, Major, Minor, Revision);
+ return HashCode.Combine(this.Build, this.Major, this.Minor, this.Revision);
}
public Version ToVersion()
diff --git a/Giants.DataContract/Contracts/V1/PlayerInfo.cs b/Giants.DataContract/Contracts/V1/PlayerInfo.cs
index f804f64..1672140 100644
--- a/Giants.DataContract/Contracts/V1/PlayerInfo.cs
+++ b/Giants.DataContract/Contracts/V1/PlayerInfo.cs
@@ -23,16 +23,16 @@ namespace Giants.DataContract.V1
public override bool Equals(object obj)
{
return obj is PlayerInfo info &&
- Index == info.Index &&
- Name == info.Name &&
- Frags == info.Frags &&
- Deaths == info.Deaths &&
- TeamName == info.TeamName;
+ this.Index == info.Index &&
+ this.Name == info.Name &&
+ this.Frags == info.Frags &&
+ this.Deaths == info.Deaths &&
+ this.TeamName == info.TeamName;
}
public override int GetHashCode()
{
- return HashCode.Combine(Index, Name, Frags, Deaths, TeamName);
+ return HashCode.Combine(this.Index, this.Name, this.Frags, this.Deaths, this.TeamName);
}
}
}
diff --git a/Giants.DataContract/Contracts/V1/ServerInfo.cs b/Giants.DataContract/Contracts/V1/ServerInfo.cs
index 31d45a9..d2a9528 100644
--- a/Giants.DataContract/Contracts/V1/ServerInfo.cs
+++ b/Giants.DataContract/Contracts/V1/ServerInfo.cs
@@ -54,37 +54,37 @@
public override bool Equals(object obj)
{
return obj is ServerInfo info &&
- GameName == info.GameName &&
- EqualityComparer.Default.Equals(Version, info.Version) &&
- SessionName == info.SessionName &&
- Port == info.Port &&
- MapName == info.MapName &&
- GameType == info.GameType &&
- NumPlayers == info.NumPlayers &&
- GameState == info.GameState &&
- TimeLimit == info.TimeLimit &&
- FragLimit == info.FragLimit &&
- TeamFragLimit == info.TeamFragLimit &&
- FirstBaseComplete == info.FirstBaseComplete &&
- PlayerInfo.SequenceEqual(info.PlayerInfo);
+ this.GameName == info.GameName &&
+ EqualityComparer.Default.Equals(this.Version, info.Version) &&
+ this.SessionName == info.SessionName &&
+ this.Port == info.Port &&
+ this.MapName == info.MapName &&
+ this.GameType == info.GameType &&
+ this.NumPlayers == info.NumPlayers &&
+ this.GameState == info.GameState &&
+ this.TimeLimit == info.TimeLimit &&
+ this.FragLimit == info.FragLimit &&
+ this.TeamFragLimit == info.TeamFragLimit &&
+ this.FirstBaseComplete == info.FirstBaseComplete &&
+ this.PlayerInfo.SequenceEqual(info.PlayerInfo);
}
public override int GetHashCode()
{
HashCode hash = new HashCode();
- hash.Add(GameName);
- hash.Add(Version);
- hash.Add(SessionName);
- hash.Add(Port);
- hash.Add(MapName);
- hash.Add(GameType);
- hash.Add(NumPlayers);
- hash.Add(GameState);
- hash.Add(TimeLimit);
- hash.Add(FragLimit);
- hash.Add(TeamFragLimit);
- hash.Add(FirstBaseComplete);
- hash.Add(PlayerInfo);
+ hash.Add(this.GameName);
+ hash.Add(this.Version);
+ hash.Add(this.SessionName);
+ hash.Add(this.Port);
+ hash.Add(this.MapName);
+ hash.Add(this.GameType);
+ hash.Add(this.NumPlayers);
+ hash.Add(this.GameState);
+ hash.Add(this.TimeLimit);
+ hash.Add(this.FragLimit);
+ hash.Add(this.TeamFragLimit);
+ hash.Add(this.FirstBaseComplete);
+ hash.Add(this.PlayerInfo);
return hash.ToHashCode();
}
}
diff --git a/Giants.DataContract/Contracts/V1/ServerInfoWithHostAddress.cs b/Giants.DataContract/Contracts/V1/ServerInfoWithHostAddress.cs
index 4e4707a..dfb89cc 100644
--- a/Giants.DataContract/Contracts/V1/ServerInfoWithHostAddress.cs
+++ b/Giants.DataContract/Contracts/V1/ServerInfoWithHostAddress.cs
@@ -12,14 +12,14 @@
{
return obj is ServerInfoWithHostAddress address &&
base.Equals(obj) &&
- HostIpAddress == address.HostIpAddress;
+ this.HostIpAddress == address.HostIpAddress;
}
public override int GetHashCode()
{
HashCode hash = new HashCode();
hash.Add(base.GetHashCode());
- hash.Add(HostIpAddress);
+ hash.Add(this.HostIpAddress);
return hash.ToHashCode();
}
}
diff --git a/Giants.EffectCompiler.Tests/Giants.EffectCompiler.Tests.csproj b/Giants.EffectCompiler.Tests/Giants.EffectCompiler.Tests.csproj
new file mode 100644
index 0000000..86d94a6
--- /dev/null
+++ b/Giants.EffectCompiler.Tests/Giants.EffectCompiler.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp3.1
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Giants.EffectCompiler.Tests/Integration/DecompileCompileTests.cs b/Giants.EffectCompiler.Tests/Integration/DecompileCompileTests.cs
new file mode 100644
index 0000000..790630e
--- /dev/null
+++ b/Giants.EffectCompiler.Tests/Integration/DecompileCompileTests.cs
@@ -0,0 +1,57 @@
+namespace Giants.EffectCompiler.Tests.Integration
+{
+ using System;
+ using System.IO;
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ [TestClass]
+ public class DecompileCompileTests
+ {
+ private const string ProjectDirectoryPath = @"..\..\..\";
+
+ [TestMethod]
+ public void TestDecompileCompile()
+ {
+ // Verify round-trip of compiled file
+
+ Guid testIdentifier = Guid.NewGuid();
+ string textOutputPath = @$"Temp\fx_{testIdentifier}.txt";
+ string binaryOutputPath = @$"Temp\fx_{testIdentifier}.bin";
+
+ try
+ {
+ var definitionTable = Utilities.LoadDefinitions();
+ var fxDecompiler = new FxDecompiler(definitionTable);
+ fxDecompiler.Decompile(
+ Path.Combine(ProjectDirectoryPath, @"TestResources\fx.bin"),
+ Path.Combine(ProjectDirectoryPath, textOutputPath));
+
+ var fxCompiler = new FxCompiler(definitionTable);
+ fxCompiler.Compile(
+ Path.Combine(ProjectDirectoryPath, textOutputPath),
+ Path.Combine(ProjectDirectoryPath, binaryOutputPath));
+
+ using var originalFile = new BinaryReader(File.Open(Path.Combine(ProjectDirectoryPath, @"TestResources\fx.bin"), FileMode.Open));
+ using var recompiledFile = new BinaryReader(File.Open(Path.Combine(ProjectDirectoryPath, binaryOutputPath), FileMode.Open));
+
+ if (originalFile.BaseStream.Length != recompiledFile.BaseStream.Length)
+ {
+ throw new InvalidOperationException("File sizes do not match.");
+ }
+
+ while (originalFile.BaseStream.Position != originalFile.BaseStream.Length)
+ {
+ byte b1 = originalFile.ReadByte();
+ byte b2 = recompiledFile.ReadByte();
+
+ Assert.AreEqual(b1, b2);
+ }
+ }
+ finally
+ {
+ File.Delete(Path.Combine(ProjectDirectoryPath, textOutputPath));
+ File.Delete(Path.Combine(ProjectDirectoryPath, binaryOutputPath));
+ }
+ }
+ }
+}
diff --git a/Giants.EffectCompiler.Tests/TestResources/fx.bin b/Giants.EffectCompiler.Tests/TestResources/fx.bin
new file mode 100644
index 0000000..dbf15e9
Binary files /dev/null and b/Giants.EffectCompiler.Tests/TestResources/fx.bin differ
diff --git a/Giants.EffectCompiler/Compiler/ContentEntry.cs b/Giants.EffectCompiler/Compiler/ContentEntry.cs
new file mode 100644
index 0000000..139d1c3
--- /dev/null
+++ b/Giants.EffectCompiler/Compiler/ContentEntry.cs
@@ -0,0 +1,8 @@
+namespace Giants.EffectCompiler
+{
+ public class ContentEntry
+ {
+ public string Name { get; set; }
+ public int Offset { get; set; }
+ }
+}
diff --git a/Giants.EffectCompiler/Compiler/FxCompiler.cs b/Giants.EffectCompiler/Compiler/FxCompiler.cs
new file mode 100644
index 0000000..c0db85d
--- /dev/null
+++ b/Giants.EffectCompiler/Compiler/FxCompiler.cs
@@ -0,0 +1,155 @@
+namespace Giants.EffectCompiler
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Text;
+ using Giants.BinTools.Macro;
+ using NLog;
+
+ ///
+ /// Effect binary compiler.
+ ///
+ public class FxCompiler
+ {
+ private static readonly Logger logger = LogManager.GetLogger(nameof(FxCompiler));
+
+ private FxMacroDefinitionTable macroDefinitionTable;
+ private int doneOpcode;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The table of macro and symbol definitions.
+ public FxCompiler(FxMacroDefinitionTable macroDefinitionTable)
+ {
+ this.macroDefinitionTable = macroDefinitionTable;
+ }
+
+ ///
+ /// Compiles a textual effect file to the specified output path.
+ ///
+ /// The path to the effect file.
+ /// The path to write to.
+ public void Compile(
+ string inputPath,
+ string outputPath)
+ {
+ if (!File.Exists(inputPath))
+ {
+ throw new InvalidOperationException($"The input file {inputPath} does not exist.");
+ }
+
+ // Get constants for known symbols
+ this.doneOpcode = Utilities.GetFxSymbolValue(this.macroDefinitionTable.SymbolTable, "FxDone");
+
+ using var streamReader = new StreamReader(inputPath);
+ SerializedEffectData serializedEffectData = this.SerializeEffectData(streamReader);
+
+ using var fileStream = new FileStream(outputPath, FileMode.Create);
+ using var binaryWriter = new BinaryWriter(fileStream);
+
+ this.WriteHeader(binaryWriter, serializedEffectData);
+ this.WriteTableOfContents(binaryWriter, serializedEffectData);
+ this.WriteEffectData(binaryWriter, serializedEffectData);
+ }
+
+ private void WriteEffectData(BinaryWriter binaryWriter, SerializedEffectData serializedEffectData)
+ {
+ binaryWriter.Write(serializedEffectData.Data);
+ }
+
+ private void WriteTableOfContents(BinaryWriter binaryWriter, SerializedEffectData serializedEffectData)
+ {
+ foreach (var entry in serializedEffectData.TableOfContents)
+ {
+ binaryWriter.Write(Encoding.UTF8.GetBytes(entry.Name));
+ binaryWriter.Write('\0');
+ binaryWriter.Write(checked(entry.Offset + serializedEffectData.TableOfContentsSize));
+ }
+ }
+
+ private void WriteHeader(BinaryWriter binaryWriter, SerializedEffectData serializedEffectData)
+ {
+ binaryWriter.Write(FxBinaryData.CurrentVersion);
+ binaryWriter.Write(checked(serializedEffectData.Data.Length + serializedEffectData.TableOfContentsSize));
+ binaryWriter.Write(serializedEffectData.TableOfContents.Count);
+ }
+
+ private void SerializeEffect(string[] tokens, StreamReader reader, BinaryWriter binaryWriter)
+ {
+ while (!reader.EndOfStream)
+ {
+ tokens = reader.ReadLine().Split(Utilities.SplitCharacters, StringSplitOptions.RemoveEmptyEntries);
+
+ string macroName = tokens[0];
+ if (macroName == "fxdone")
+ {
+ binaryWriter.Write((byte)this.doneOpcode);
+ break;
+ }
+
+ FxMacroDefinition macroDefinition = this.macroDefinitionTable.MacroDefinitions
+ .Values
+ .FirstOrDefault(x => x.Name.Equals(macroName, StringComparison.OrdinalIgnoreCase) && x.FxDefGroup.Count() == tokens[1..].Length);
+
+ binaryWriter.Write((byte)macroDefinition.Opcode);
+
+ if (macroDefinition == null)
+ {
+ throw new InvalidOperationException("Unknown macro '{macroName}'");
+ }
+
+ var parameters = new List