diff --git a/.gitignore b/.gitignore index e60f4c4..097ceb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ [Dd]ebug [Rr]elease *.userprefs -packages diff --git a/README.md b/README.md index 5117cad..5b30cc2 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,30 @@ # Age of Empires Scenario Editor -Command line application and library to edit Scenario and Campaign files -of games powered by the Genie Engine. -This includes the *Age of Empires* series and *Star Wars: Galactic Battlegrounds.* +Command line application and library to edit scenario and campaign files. -**Beta version** +**Alpha version** -# Command Line Application +## Usage -## Features + Available commands are: + copymap Copy parts of scenarios + compress Recompress a decompressed scenario file + decompress Decompress a scenario file for hex editing + extract Extract scenarios from campaign file + dump Dump a scenario file to JSON + dumpunits Dump units and map tiles below -* Convert AOE1 Scenario to AOE2 Scenario -* Convert a Scenario file to JSON and vice-versa -* Decompress and recompress compressed Scenario part for hex editing -* Dump unit information to easily gather Unit IDs -* Extract Scenario files from Campaign file - -## Planned Features - -* Create maps from bitmaps -* Trigger implementation -* Full conversion between all Scenario versions - (mapping of Unit IDs are needed for this, see Section Contributing) - -## Examples +The command `copymap` is able convert an AOE1 to an AOE2 scenario. +There seem to be some issues with cliffs. [OpeningMoves-AOE2.scx](files/OpeningMoves-AOE2.scx) -is a fully automated conversion of a Scenario file from the AOE1 demo version: +is a fully automated conversion of a scenario from the AOE1 demo version: - $ GenieEdit.exe convert files/examples/aoe2x-Empty.scx "files/examples/aoe1demo-Reigno_1-Opening Moves.scn" files/OpeningMoves-AOE2.scx + $ GenieEdit.exe copymap files/examples/aoe2x-Empty.scx "files/examples/aoe1demo-Reigno_1-Opening Moves.scn" files/OpeningMoves-AOE2.scx (Stone are set to 100 to be able to build a town center from the beginning.) -A unit dump looks like the following: +It can also be used to dump units from any version of a scenario: $ GenieEdit.exe dumpunits Debug2.scx Scenario Editor @@ -44,37 +36,13 @@ A unit dump looks like the following: 1 69 23 ( 2,5, 11,5) ... -# Library +### Code -The namespace `AdrianKousz.GenieEngine` contains classes -to read, write, and manipulate Scenario files. - -`Scenario` contains all data structures related to a Scenario file. -They are easily understandable. - -The map is stored in linear order. -This way, it is easier to process seperate tiles. -The code below gets the map tile at (10,5). -The map origin (0,0) is in the left corner of the map: - -![Map coordinate system](doc/MapCoord.png) - - Scenario scn; - int x = 10; - int y = 5; - Scenario.ScnMap.Tile tile = scn.Map.LinearTiles[scn.Map.SizeX * y + x]; - -`GenieFile` is the starting point for reading and writing: - - Scenario scn = GenieFile.Deserialize(filestream); - -`ScnDefaultFactory` is a class that is able to produce an empty -Scenario file. Eventually, it should be able to create a `Scenario` -that is usable without any modifications. +Have a look at the command implementations. They are very short. ## Build -Clone the projects MainUtil.cs, ExUtil.cs, and this one, +Clone my projects MainUtil.cs ExUtil.cs and this repository and build `src/GenieEdit.sln`. ## Contributing @@ -85,10 +53,12 @@ Keep in mind that the API is still very unstable. Suggestions: 1. Gather UnitIDs of AOE1 -2. Much help is appreciated to figure out Unit ID mappings +2. Much help is appreciated to figure out ID mappings for the conversion process. 3. Ideas how to configure the converter (mapping tables). There are some notes in German: [AOE1](files/units-aoe1.txt), [AOE2](files/units-aoe2.txt) -3. Feedback about the API. +3. The application should be able to generate maps from images + in the future. How should units be placed using the image? +4. Feedback about the API. diff --git a/doc/MapCoord.png b/doc/MapCoord.png deleted file mode 100644 index 2ff2e3b..0000000 Binary files a/doc/MapCoord.png and /dev/null differ diff --git a/files/OpeningMoves-AOE2.scx b/files/OpeningMoves-AOE2.scx index d8b4185..ae890f6 100644 Binary files a/files/OpeningMoves-AOE2.scx and b/files/OpeningMoves-AOE2.scx differ diff --git a/src/cmdapp/GenieEngineApp.csproj b/src/cmdapp/GenieEngineApp.csproj index b07e203..b3128ef 100644 --- a/src/cmdapp/GenieEngineApp.csproj +++ b/src/cmdapp/GenieEngineApp.csproj @@ -31,15 +31,14 @@ - - - - - + + + + + - @@ -62,11 +61,5 @@ - - ..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll - - - - \ No newline at end of file diff --git a/src/cmdapp/packages.config b/src/cmdapp/packages.config deleted file mode 100644 index 2abc396..0000000 --- a/src/cmdapp/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/cmdapp/src/CompressCommand.cs b/src/cmdapp/src/CompressCommand.cs index 86ea72f..13a012e 100644 --- a/src/cmdapp/src/CompressCommand.cs +++ b/src/cmdapp/src/CompressCommand.cs @@ -7,10 +7,20 @@ namespace AdrianKousz.GenieEngine { override public void Run() { - using (var input = File.OpenRead(args[0])) - using (var output = File.OpenWrite(args[1])) - { - GenieFile.ScenarioCompression(input, output, false); + using (var file = File.OpenRead(args[0])) { + using (var outfile = File.OpenWrite(args[1])) { + var reader = new ScnReader(file); + var writer = new ScnWriter(outfile); + + var header = reader.ReadScenarioHeader(); + reader.ReadSeparator("Custom"); + writer.Write(header); + writer.Flush(); + + using (var comp = new DeflateStream(outfile, CompressionMode.Compress)) { + file.CopyTo(comp); + } + } } } diff --git a/src/cmdapp/src/ConvertCommand.cs b/src/cmdapp/src/ConvertCommand.cs deleted file mode 100644 index 69d3b64..0000000 --- a/src/cmdapp/src/ConvertCommand.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.IO; -using AdrianKousz.Util; - -namespace AdrianKousz.GenieEngine -{ - public class ConvertCommand : BaseCommand - { - override public void Run() - { - using (var refstream = File.OpenRead(args[0])) - using (var srcstream = File.OpenRead(args[1])) - using (var outstream = File.OpenWrite(args[2])) - { - var refscn = GenieFile.Deserialize(refstream); - var srcscn = GenieFile.Deserialize(srcstream); - - srcscn.VersionString = refscn.VersionString; - srcscn.VersionNumber = refscn.VersionNumber; - srcscn.RawIndividualVictory = refscn.RawIndividualVictory; - srcscn.RawDisables = refscn.RawDisables; - srcscn.RawRemaining = refscn.RawRemaining; - - srcscn.PlayerSettings.ForEach(x => { - x.FilenameAI = "RandomGame"; - x.FilenameCTY = x.FilenamePER = ""; - x.ScriptAI = x.ScriptCTY = x.ScriptPER = ""; - }); - - var converter = new CopymapConverter(); - converter.Convert(srcscn); - - GenieFile.Serialize(srcscn, outstream); - } - } - - override public int GetArgumentCount() - { - return 3; - } - - override public string GetDescription() - { - return "Convert AOE1 scenario"; - } - - override public string GetHelp() - { - return "Convert AOE1 scenario to AOE2" - + "\n" - + "\nThe converter needs an existing AOE2 scenario to start with. Use the following arguments:" - + "\n " - ; - } - } -} diff --git a/src/cmdapp/src/CopymapCommand.cs b/src/cmdapp/src/CopymapCommand.cs new file mode 100644 index 0000000..3129569 --- /dev/null +++ b/src/cmdapp/src/CopymapCommand.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; + +namespace AdrianKousz.GenieEngine +{ + public class CopymapCommand : BaseCommand + { + override public void Run() + { + Data.Scenario result; + using (var dststream = File.OpenRead(args[0])) + using (var srcstream = File.OpenRead(args[1])) + { + var srcreader = new ScnReader(srcstream); + var src = srcreader.ReadScenario(); + var dstreader = new ScnReader(dststream); + var dst = dstreader.ReadScenario(); + var converter = new CopymapConverter(); + dst.PlayerNames = src.PlayerNames; + dst.StringTablePlayerNames = src.StringTablePlayerNames; + dst.Players = src.Players; + dst.Resources = src.Resources; + dst.ResourcesCopy = src.ResourcesCopy; + dst.Messages = src.Messages; + converter.Convert(dst, src); + result = dst; + } + using (var outstream = File.OpenWrite(args[2])) { + var outwriter = new ScnWriter(outstream); + outwriter.Write(result); + } + } + + override public int GetArgumentCount() + { + return 3; + } + + override public string GetDescription() + { + return "Copy parts of scenarios"; + } + + override public string GetHelp() + { + return "Copy the parts below from scenario file 2 into scenario file 1." + + "\nThe result is written to scenario file 3." + + "\n- Messages" + + "\n- Player information" + + "\n- Resources" + + "\n- Map" + + "\n- Units" + ; + } + } +} + diff --git a/src/cmdapp/src/CopymapConverter.cs b/src/cmdapp/src/CopymapConverter.cs index 2bd5d17..e2b82a7 100644 --- a/src/cmdapp/src/CopymapConverter.cs +++ b/src/cmdapp/src/CopymapConverter.cs @@ -1,24 +1,26 @@ using System; using System.Collections.Generic; using AdrianKousz.Util; +using AdrianKousz.GenieEngine.Data; namespace AdrianKousz.GenieEngine { public class CopymapConverter { - public void Convert(Scenario scn) + public void Convert(Scenario dst, Scenario src) { - scn.Units = ChangeUnits_AOE1_AOE2(scn.Units); - MoveUnits_AOE1_AOE2(scn.Units); - ChangeTiles_AOE1_AOE2(scn.Units, scn.Map); - RandomizeTrees_AOE2(scn.Units); + dst.Map = src.Map; + dst.Units = ChangeUnits_AOE1_AOE2(src.Units); + MoveUnits_AOE1_AOE2(dst.Units); + ChangeTiles_AOE1_AOE2(dst.Units, dst.Map); + RandomizeTrees_AOE2(dst.Units); } /** * Some buildings are not the same size. * They need to be moved by half a unit. */ - private void MoveUnits_AOE1_AOE2(IList[] units) + private void MoveUnits_AOE1_AOE2(IList[] units) { var mapping = new int[] { // Towers @@ -41,7 +43,7 @@ namespace AdrianKousz.GenieEngine /** * Simple mapping of unit IDs */ - private List[] ChangeUnits_AOE1_AOE2(IList[] units) + private List[] ChangeUnits_AOE1_AOE2(IList[] units) { var array = new int[] { // Resources @@ -140,17 +142,18 @@ namespace AdrianKousz.GenieEngine 121, 121, 129, 129, }; - var mapping = new Dictionary(); + var mapping = new Dictionary(); var ai = 0; while (ai < array.Length) - mapping.Add((short)array[ai++], (short)array[ai++]); + mapping.Add(array[ai++], array[ai++]); - var result = new List[Scenario.NumPlayerSections].Fill(); + var result = new List[Scenario.NumPlayerSections]; + result.Fill(); units.ForEach((i, list) => { list.ForEach((j, unit) => { if (mapping.ContainsKey(unit.UnitId)) { - unit.UnitId = mapping[unit.UnitId]; + unit.UnitId = (short)mapping[unit.UnitId]; result[i].Add(unit); } }); @@ -162,7 +165,7 @@ namespace AdrianKousz.GenieEngine /** * Farms need to be converted including the underlying terrain. */ - private void ChangeTiles_AOE1_AOE2(IList[] units, Scenario.ScnMap map) + private void ChangeTiles_AOE1_AOE2(IList[] units, Map map) { units.ForEach((i, list) => { list.ForEach((j, unit) => { @@ -172,7 +175,6 @@ namespace AdrianKousz.GenieEngine if (false) { // switch } else if (unit.UnitId == 50) { - // Farm map.LinearTiles[linearpos].Id = 7; map.LinearTiles[linearpos - 1].Id = 7; map.LinearTiles[linearpos + 1].Id = 7; @@ -198,7 +200,7 @@ namespace AdrianKousz.GenieEngine * Forests would contain the same tree graphic * without this function */ - private void RandomizeTrees_AOE2(IList[] units) + private void RandomizeTrees_AOE2(IList[] units) { var treeids = new int[] { 411, 351, 414, 350, 348, 349, diff --git a/src/cmdapp/src/DecompressCommand.cs b/src/cmdapp/src/DecompressCommand.cs index 7ac8591..bc4f905 100644 --- a/src/cmdapp/src/DecompressCommand.cs +++ b/src/cmdapp/src/DecompressCommand.cs @@ -1,4 +1,5 @@ using System.IO; +using System.IO.Compression; namespace AdrianKousz.GenieEngine { @@ -6,10 +7,20 @@ namespace AdrianKousz.GenieEngine { override public void Run() { - using (var input = File.OpenRead(args[0])) - using (var output = File.OpenWrite(args[1])) - { - GenieFile.ScenarioCompression(input, output, true); + using (var file = File.OpenRead(args[0])) { + using (var outfile = File.OpenWrite(args[1])) { + var reader = new ScnReader(file); + var writer = new ScnWriter(outfile); + + var header = reader.ReadScenarioHeader(); + writer.Write(header); + writer.WriteSeperator("Custom"); + writer.Flush(); + + using (var comp = new DeflateStream(file, CompressionMode.Decompress)) { + comp.CopyTo(outfile); + } + } } } diff --git a/src/cmdapp/src/DumpCommand.cs b/src/cmdapp/src/DumpCommand.cs new file mode 100644 index 0000000..19ef4cd --- /dev/null +++ b/src/cmdapp/src/DumpCommand.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; + +namespace AdrianKousz.GenieEngine +{ + public class DumpCommand : BaseCommand + { + override public void Run() + { + using (var stream = File.OpenRead(args[0])) { + var aoereader = new ScnReader(stream); + var scn = aoereader.ReadScenario(); + var ser = new System.Web.Script.Serialization.JavaScriptSerializer(); + var result = ser.Serialize(scn); + Console.WriteLine(result); + } + } + + override public int GetArgumentCount() + { + return 1; + } + + override public string GetDescription() + { + return "Dump a scenario file to JSON"; + } + } +} diff --git a/src/cmdapp/src/DumpunitsCommand.cs b/src/cmdapp/src/DumpunitsCommand.cs index a85101a..d995615 100644 --- a/src/cmdapp/src/DumpunitsCommand.cs +++ b/src/cmdapp/src/DumpunitsCommand.cs @@ -2,6 +2,7 @@ using System.IO; using System.Collections.Generic; using AdrianKousz.Util; +using AdrianKousz.GenieEngine.Data; namespace AdrianKousz.GenieEngine { @@ -10,9 +11,9 @@ namespace AdrianKousz.GenieEngine override public void Run() { Scenario scn; - using (var input = File.OpenRead(args[0])) - { - scn = GenieFile.Deserialize(input); + using (var stream = File.OpenRead(args[0])) { + var aoereader = new ScnReader(stream); + scn = aoereader.ReadScenario(); } Console.WriteLine("{0,-8} {1,-8} {2,-8} {3}", "ID", "UnitID", "TileID", "Position"); diff --git a/src/cmdapp/src/FromJsonCommand.cs b/src/cmdapp/src/FromJsonCommand.cs deleted file mode 100644 index ef25d9f..0000000 --- a/src/cmdapp/src/FromJsonCommand.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.IO; -using Newtonsoft.Json; -using AdrianKousz.Util; - -namespace AdrianKousz.GenieEngine -{ - public class FromJsonCommand : BaseCommand - { - override public void Run() - { - using (var input = File.OpenRead(args[0])) - using (var reader = input.GetReader()) - using (var output = File.OpenWrite(args[1])) - { - var str = reader.ReadToEnd(); - var scn = JsonConvert.DeserializeObject(str); - GenieFile.Serialize(scn, output); - } - } - - override public int GetArgumentCount() - { - return 2; - } - - override public string GetDescription() - { - return "Read a JSON file and save as scenario"; - } - } -} - diff --git a/src/cmdapp/src/Program.cs b/src/cmdapp/src/Program.cs index 84af326..773b35a 100644 --- a/src/cmdapp/src/Program.cs +++ b/src/cmdapp/src/Program.cs @@ -25,12 +25,11 @@ namespace AdrianKousz.GenieEngine public IDictionary> GetCommands() { var result = new Dictionary>(); - result.Add("convert", new ConvertCommand()); + result.Add("copymap", new CopymapCommand()); result.Add("compress", new CompressCommand()); result.Add("decompress", new DecompressCommand()); result.Add("extract", new ExtractCommand()); - result.Add("tojson", new ToJsonCommand()); - result.Add("fromjson", new FromJsonCommand()); + result.Add("dump", new DumpCommand()); result.Add("dumpunits", new DumpunitsCommand()); return result; } @@ -39,7 +38,8 @@ namespace AdrianKousz.GenieEngine { var fn = ""; using (var istream = System.IO.File.OpenRead(fn)) { - var scn = GenieFile.Deserialize(istream); + var ireader = new ScnReader(istream); + var scn = ireader.ReadScenario(); System.Diagnostics.Debugger.Break(); // Great to inspect scn } } diff --git a/src/cmdapp/src/ToJsonCommand.cs b/src/cmdapp/src/ToJsonCommand.cs deleted file mode 100644 index 54faf2d..0000000 --- a/src/cmdapp/src/ToJsonCommand.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.IO; -using Newtonsoft.Json; -using AdrianKousz.Util; - -namespace AdrianKousz.GenieEngine -{ - public class ToJsonCommand : BaseCommand - { - override public void Run() - { - using (var input = File.OpenRead(args[0])) - using (var output = File.OpenWrite(args[1])) - using (var writer = output.GetWriter()) - { - var scn = GenieFile.Deserialize(input); - var result = JsonConvert.SerializeObject(scn, Formatting.Indented); - writer.WriteLine(result); - } - } - - override public int GetArgumentCount() - { - return 2; - } - - override public string GetDescription() - { - return "Dump a scenario file to JSON"; - } - } -} diff --git a/src/lib/AdrianKousz.GenieEngine/CpnReader.cs b/src/lib/AdrianKousz.GenieEngine/CpnReader.cs index 4b96cee..197ebc0 100644 --- a/src/lib/AdrianKousz.GenieEngine/CpnReader.cs +++ b/src/lib/AdrianKousz.GenieEngine/CpnReader.cs @@ -1,5 +1,6 @@ using System.IO; using AdrianKousz.Util; +using AdrianKousz.GenieEngine.Data; namespace AdrianKousz.GenieEngine { diff --git a/src/lib/AdrianKousz.GenieEngine/GenieFile.cs b/src/lib/AdrianKousz.GenieEngine/GenieFile.cs deleted file mode 100644 index 8d68a17..0000000 --- a/src/lib/AdrianKousz.GenieEngine/GenieFile.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.IO; -using System.IO.Compression; -using AdrianKousz.Util; - -namespace AdrianKousz.GenieEngine -{ - public static class GenieFile - { - #region Serialization - - public static byte[] Serialize(T value) - { - byte[] result; - using (var stream = new MemoryStream()) - { - Serialize(value, stream); - result = stream.ToArray(); - } - return result; - } - - public static void Serialize(T value, Stream output) - { - var scenarioValue = value as Scenario; - if (scenarioValue != null) { - var scnwriter = ScnSerializerWriter.CreateDefault(); - using (var stream = new NonDisposingStreamWrapper(output)) - using (var writer = GetWriter(stream)) - { - scnwriter.Write(writer, scenarioValue); - return; - } - } - - throw new System.ArgumentException("Unsupported Type"); - } - - #endregion - - #region Deserialization - - public static T Deserialize(byte[] array) - { - return (T)Deserialize(array, typeof(T)); - } - - public static T Deserialize(Stream input) - { - return (T)Deserialize(input, typeof(T)); - } - - private static object Deserialize(byte[] array, System.Type type) - { - using (var stream = new MemoryStream(array)) - { - return Deserialize(stream, type); - } - } - - private static object Deserialize(Stream input, System.Type type) - { - if (type == typeof(Scenario)) { - var scnreader = ScnSerializerReader.CreateDefault(); - using (var stream = new NonDisposingStreamWrapper(input)) - using (var reader = GetReader(stream)) - { - return scnreader.ReadScenario(reader); - } - } - - throw new System.ArgumentException("Unsupported Type"); - } - - #endregion - - #region Low Level Methods - - public static ExtendedBinaryReader GetReader(Stream stream) - { - return new ExtendedBinaryReader(stream, Scenario.Encoding); - } - - public static ExtendedBinaryWriter GetWriter(Stream stream) - { - return new ExtendedBinaryWriter(stream, Scenario.Encoding); - } - - public static void ScenarioCompression(Stream input, Stream output, bool decompress) - { - var mode = decompress ? CompressionMode.Decompress : CompressionMode.Compress; - - byte[] buffer; - - buffer = input.ReadBytes(8); - output.Write(buffer); - - var headerLength = buffer[4] - | buffer[5] << 8 - | buffer[6] << 16 - | buffer[7] << 24 - ; - - buffer = input.ReadBytes(headerLength); - output.Write(buffer); - - using (var comp = new DeflateStream(input, mode)) - { - comp.CopyTo(output); - } - } - - #endregion - } -} - diff --git a/src/lib/AdrianKousz.GenieEngine/IScnFactory.cs b/src/lib/AdrianKousz.GenieEngine/IScnFactory.cs index eee1a25..2a36fe3 100644 --- a/src/lib/AdrianKousz.GenieEngine/IScnFactory.cs +++ b/src/lib/AdrianKousz.GenieEngine/IScnFactory.cs @@ -1,4 +1,7 @@ -namespace AdrianKousz.GenieEngine +using System; +using AdrianKousz.GenieEngine.Data; + +namespace AdrianKousz.GenieEngine { public interface IScnFactory { diff --git a/src/lib/AdrianKousz.GenieEngine/Scenario.cs b/src/lib/AdrianKousz.GenieEngine/Scenario.cs index a801b51..92e9b11 100644 --- a/src/lib/AdrianKousz.GenieEngine/Scenario.cs +++ b/src/lib/AdrianKousz.GenieEngine/Scenario.cs @@ -9,7 +9,7 @@ namespace AdrianKousz.GenieEngine public const Int32 Separator = -99; public const Int32 NumPlayers = 16; public const Int32 NumPlayerSections = 9; - public static readonly Encoding Encoding = Encoding.GetEncoding(1252); + public const Encoding Encoding = Encoding.GetEncoding(1252); public const Int32 ExpectedUnknown1 = 2; public const Int32 ExpectedUnknown2 = 0; @@ -117,11 +117,11 @@ namespace AdrianKousz.GenieEngine public Int32 Artifacts; public Int32 Discovery; public Int32 PercentExplored; + public Int32 Unknown; public Boolean RequireAllCustom; public Int32 Mode; public Int32 Score; public Int32 Time; - public Int32 Unknown; } public class ScnResourceCopy @@ -166,17 +166,17 @@ namespace AdrianKousz.GenieEngine public class ScnUnit { public const Single ExpectedUnknown1 = 1; - public const SByte ExpectedUnknown2 = 2; + public const Byte ExpectedUnknown2 = 2; public Single PosX; public Single PosY; + public Single Unknown1; public Int32 Id; public Int16 UnitId; + public Byte Unknown2; public Single Rotation; public Int16 InitialFrame; public Int32 GarrisonnedInId; - public Single Unknown1; - public SByte Unknown2; } } } diff --git a/src/lib/AdrianKousz.GenieEngine/ScnConvert.cs b/src/lib/AdrianKousz.GenieEngine/ScnConvert.cs new file mode 100644 index 0000000..a0375bb --- /dev/null +++ b/src/lib/AdrianKousz.GenieEngine/ScnConvert.cs @@ -0,0 +1,22 @@ +using System; +using AdrianKousz.Util; +using AdrianKousz.GenieEngine.Data; + +namespace AdrianKousz.GenieEngine +{ + public class ScnConvert + { + public void Convert(Scenario value, float version) + { + + } + public void Convert(UnitInfo value, float from, float to) + { + if (from < 1.18f && to >= 1.18f) { + value.InitialFrame = 0; + value.GarrisonnedInId = -1; + } + } + } +} + diff --git a/src/lib/AdrianKousz.GenieEngine/ScnDefaultFactory.cs b/src/lib/AdrianKousz.GenieEngine/ScnDefaultFactory.cs index 8bea630..b346e09 100644 --- a/src/lib/AdrianKousz.GenieEngine/ScnDefaultFactory.cs +++ b/src/lib/AdrianKousz.GenieEngine/ScnDefaultFactory.cs @@ -125,7 +125,7 @@ namespace AdrianKousz.GenieEngine result.GeneratorId = 2; result.SizeX = size; result.SizeY = size; - result.LinearTiles = new Scenario.ScnMap.Tile[size*size].Fill(() => new Scenario.ScnMap.Tile(0, 1, 0)); + result.LinearTiles = new Map.Tile[size*size].Fill(() => new Map.Tile(0, 1, 0)); return result; } diff --git a/src/lib/AdrianKousz.GenieEngine/ScnReader.cs b/src/lib/AdrianKousz.GenieEngine/ScnReader.cs new file mode 100644 index 0000000..bf7811b --- /dev/null +++ b/src/lib/AdrianKousz.GenieEngine/ScnReader.cs @@ -0,0 +1,327 @@ +using System.IO; +using System.IO.Compression; +using System.Collections.Generic; +using AdrianKousz.Util; +using AdrianKousz.GenieEngine.Data; + +namespace AdrianKousz.GenieEngine +{ + public class ScnReader + { + public float Version = 0; + + private ExtendedBinaryReader reader; + private IScnFactory factory; + + public ScnReader(Stream input) : this (input, new ScnDefaultFactory()) + { + } + + public ScnReader(Stream input, IScnFactory factory) + { + reader = new ExtendedBinaryReader(input, Scenario.Encoding); + this.factory = factory; + } + + #region Reader Methods + + public Scenario ReadScenario() + { + var result = factory.MakeScenario(false); + int number = 0; + + result.Header = ReadScenarioHeader(); + + var comp = new DeflateStream(reader.BaseStream, CompressionMode.Decompress); + reader = new ExtendedBinaryReader(comp, Scenario.Encoding); + + result.NextId = reader.ReadInt32(); + + Version = reader.ReadSingle(); + result.OriginalVersion = Version; + + result.PlayerNames = FillArray(() => reader.ReadZString(256), result.PlayerNames, Scenario.NumPlayers); + if (Version >= 1.18f) { + result.StringTablePlayerNames = FillArray(reader.ReadInt32, result.StringTablePlayerNames, Scenario.NumPlayers); + } + result.Players = FillArray(ReadPlayerInfo, result.Players, Scenario.NumPlayers); + result.Messages = ReadMessages(); + result.Cinematics = ReadCinematics(); + result.AIData = ReadAIData(); + + ReadSeparator("Resources"); + + result.Resources = FillArray(ReadResourceInfo, result.Resources, Scenario.NumPlayers); + + ReadSeparator("Victory"); + + result.GlobalVictory = ReadGlobalVictoryInfo(); + result.LinearDiplomacy = FillArray(reader.ReadInt32, result.LinearDiplomacy, Scenario.NumPlayers * Scenario.NumPlayers); + result.RawIndividualVictory = reader.ReadBytes(11520); + + ReadSeparator("Player Settings"); + + result.AlliedVictory = FillArray(reader.ReadInt32, result.AlliedVictory, Scenario.NumPlayers); + + if (Version >= 1.15f) number = (Scenario.NumPlayers * ( 20 /*Disables*/) + 3 /*Unknowns*/) * 4 /*Intsize*/; + if (Version >= 1.18f) number = (Scenario.NumPlayers * (3 /*Lengths*/ + 80 /*Disables*/) + 3 /*Unknowns*/) * 4 /*Intsize*/; + if (Version >= 1.30f) number = (Scenario.NumPlayers * (3 /*Lengths*/ + 180 /*Disables*/) + 3 /*Unknowns*/) * 4 /*Intsize*/; + result.RawDisables = reader.ReadBytes(number); + + result.StartingAges = FillArray(reader.ReadInt32, result.StartingAges, Scenario.NumPlayers); + + ReadSeparator("Map"); + + result.Map = ReadMap(); + + number = reader.ReadInt32(); + result.ResourcesCopy = FillArray(ReadResourceInfoCopy, result.ResourcesCopy, number - 1); + + result.Units = FillArray(ReadUnitInfoList, result.Units, number); + + var bytestream = new MemoryStream(); + reader.BaseStream.CopyTo(bytestream); + result.RawRemaining = bytestream.ToArray(); + + return result; + } + + public ScenarioHeader ReadScenarioHeader() + { + var result = factory.MakeScenarioHeader(); + + result.OriginalVersion = reader.ReadString(4); + reader.ReadInt32(); // Header Length + result.Unknown1 = reader.ReadInt32(); + result.Timestamp = DateTimes.FromUnixTime(reader.ReadInt32()); + result.Instructions = reader.ReadZStringInt32(); + result.Unknown2 = reader.ReadInt32(); + result.PlayerCount = reader.ReadInt32(); + + return result; + } + + public PlayerInfo ReadPlayerInfo() + { + var result = factory.MakePlayerInfo(); + + result.Active = reader.ReadBoolean(); + result.Human = reader.ReadBoolean(); + result.Civ = reader.ReadInt32(); + result.Unknown = reader.ReadInt32(); + + return result; + } + + public Messages ReadMessages() + { + var result = factory.MakeMessages(); + + result.Unknown1 = reader.ReadInt32(); + result.Unknown2 = reader.ReadByte(); + result.Unknown3 = reader.ReadSingle(); + result.OriginalFilename = reader.ReadStringInt16(); + + if (Version >= 1.18f) { + result.StringTableInstructions = reader.ReadInt32(); + result.StringTableHints = reader.ReadInt32(); + result.StringTableVictory = reader.ReadInt32(); + result.StringTableLoss = reader.ReadInt32(); + result.StringTableHistory = reader.ReadInt32(); + } + if (Version >= 1.22f) { + result.StringTableScouts = reader.ReadInt32(); + } + + result.TextInstructions = reader.ReadZStringInt16(); + result.TextHints = reader.ReadZStringInt16(); + result.TextVictory = reader.ReadZStringInt16(); + result.TextLoss = reader.ReadZStringInt16(); + result.TextHistory = reader.ReadZStringInt16(); + if (Version >= 1.22f) { + result.TextScouts = reader.ReadZStringInt16(); + } + + return result; + } + + public Cinematics ReadCinematics() + { + var result = factory.MakeCinematics(); + + result.CinemaPregameFn = reader.ReadStringInt16(); + result.CinemaVictoryFn = reader.ReadStringInt16(); + result.CinemaLossFn = reader.ReadStringInt16(); + result.BackgroundFn = reader.ReadStringInt16(); + + var hasBitmap = reader.ReadBoolean(); + + result.BitmapWidth = reader.ReadInt32(); + result.BitmapHeight = reader.ReadInt32(); + result.BitmapUnknown = reader.ReadInt16(); + + if (hasBitmap) { + result.RawBitmap = BitmapUtil.ReadRawBitmap(reader.BaseStream); + } + + return result; + } + + public AIData ReadAIData() + { + var result = factory.MakeAIData(); + + result.PlayerAIs = FillArray(reader.ReadStringInt16, result.PlayerAIs, Scenario.NumPlayers); + result.PlayerCTYs = FillArray(reader.ReadStringInt16, result.PlayerCTYs, Scenario.NumPlayers); + result.PlayerPERs = FillArray(reader.ReadStringInt16, result.PlayerPERs, Scenario.NumPlayers); + + result.PlayerCustomAIs = new string[Scenario.NumPlayers]; + result.PlayerCustomCTYs = new string[Scenario.NumPlayers]; + result.PlayerCustomPERs = new string[Scenario.NumPlayers]; + + int l1, l2, l3; + for (var i = 0; i < Scenario.NumPlayers; ++i) { + l1 = reader.ReadInt32(); + l2 = reader.ReadInt32(); + l3 = reader.ReadInt32(); + result.PlayerCustomAIs[i] = reader.ReadString(l1); + result.PlayerCustomCTYs[i] = reader.ReadString(l2); + result.PlayerCustomPERs[i] = reader.ReadString(l3); + } + + if (Version >= 1.18f) { + result.AITypes = FillArray(reader.ReadByte, result.AITypes, Scenario.NumPlayers); + } + + return result; + } + + public ResourceInfo ReadResourceInfo() + { + var result = factory.MakeResourceInfo(); + + result.Gold = reader.ReadInt32(); + result.Wood = reader.ReadInt32(); + result.Food = reader.ReadInt32(); + result.Stone = reader.ReadInt32(); + if (Version >= 1.18f) { + result.Ore = reader.ReadInt32(); + if (Version < 1.3f) { + result.Unknown = reader.ReadInt32(); + } + } + + return result; + } + + public ResourceInfoCopy ReadResourceInfoCopy() + { + var result = factory.MakeResourceInfoCopy(); + + result.Food = reader.ReadSingle(); + result.Wood = reader.ReadSingle(); + result.Gold = reader.ReadSingle(); + result.Stone = reader.ReadSingle(); + if (Version >= 1.18f) { + result.Ore = reader.ReadInt32(); + if (Version < 1.3f) { + result.Unknown = reader.ReadInt32(); + } + } + if (Version >= 1.22f) { + result.PopulationLimit = reader.ReadSingle(); + } + + return result; + } + + public GlobalVictoryInfo ReadGlobalVictoryInfo() + { + var result = factory.MakeGlobalVictoryInfo(); + + result.RequireConquest = reader.ReadBoolean(); + result.Ruins = reader.ReadInt32(); + result.Artifacts = reader.ReadInt32(); + result.Discovery = reader.ReadInt32(); + result.PercentExplored = reader.ReadInt32(); + result.Unknown = reader.ReadInt32(); + result.RequireAllCustom = reader.ReadBoolean(); + result.Mode = reader.ReadInt32(); + result.Score = reader.ReadInt32(); + result.Time = reader.ReadInt32(); + + return result; + } + + public Map ReadMap() + { + var result = factory.MakeMap(); + + if (Version >= 1.18f) { + result.CameraX = reader.ReadInt32(); + result.CameraY = reader.ReadInt32(); + } + if (Version >= 1.22f) { + result.GeneratorId = reader.ReadInt32(); + } + + result.SizeX = reader.ReadInt32(); + result.SizeY = reader.ReadInt32(); + + result.LinearTiles = new Map.Tile[result.SizeX * result.SizeY] + .Fill(() => new Map.Tile(reader.ReadByte(), reader.ReadByte(), reader.ReadByte())); + + return result; + } + + public IList ReadUnitInfoList() + { + var count = reader.ReadInt32(); + var result = new List().Fill(ReadUnitInfo, count); + return result; + } + + public UnitInfo ReadUnitInfo() + { + var result = factory.MakeUnitInfo(); + + result.PosX = reader.ReadSingle(); + result.PosY = reader.ReadSingle(); + result.Unknown1 = reader.ReadSingle(); + result.Id = reader.ReadInt32(); + result.UnitId = reader.ReadInt16(); + result.Unknown2 = reader.ReadByte(); + result.Rotation = reader.ReadSingle(); + if (Version >= 1.18f) { + result.InitialFrame = reader.ReadInt16(); + result.GarrisonnedInId = reader.ReadInt32(); + } + + return result; + } + + public void ReadSeparator(string name) { + int v = reader.ReadInt32(); + if (v != Scenario.Separator) { + var msg = "Separator \"{0}\" = {1}"; + msg = string.Format(msg, name, v); + throw new InvalidDataException(msg); + } + } + + #endregion + + #region Debug Methods + + private T[] FillArray(System.Func f, T[] array, int length) + { + if (array == null || array.Length != length) + array = new T[length]; + array.Fill(f); + return array; + } + + #endregion + } +} + diff --git a/src/lib/AdrianKousz.GenieEngine/ScnSerializerReader.cs b/src/lib/AdrianKousz.GenieEngine/ScnSerializerReader.cs index be74a27..7e6ce29 100644 --- a/src/lib/AdrianKousz.GenieEngine/ScnSerializerReader.cs +++ b/src/lib/AdrianKousz.GenieEngine/ScnSerializerReader.cs @@ -5,7 +5,7 @@ using AdrianKousz.Util; namespace AdrianKousz.GenieEngine { - internal class ScnSerializerReader + public class ScnSerializerReader { private IScnFactory factory; @@ -25,7 +25,7 @@ namespace AdrianKousz.GenieEngine var Version = 0f; var number = 0; var result = factory is ScnDefaultFactory - ? ((ScnDefaultFactory)factory).MakeScenario(false) + ? factory.MakeScenario(false) : factory.MakeScenario(); // Uncompressed header @@ -180,7 +180,7 @@ namespace AdrianKousz.GenieEngine // PlayerSettings 3 result.PlayerSettings.ForEach(x => { - x.Diplomacy = new int[Scenario.NumPlayers].Fill(reader.ReadInt32); + x.Diplomacy = new int[Scenario.NumPlayers].Fill(reader.ReadInt32()); }); result.RawIndividualVictory = reader.ReadBytes(11520); @@ -188,7 +188,7 @@ namespace AdrianKousz.GenieEngine ReadSeparator(reader, "Player Environment"); result.PlayerSettings.ForEach(x => { - x.AlliedVictory = reader.ReadBoolean(); + x.AlliedVictory = reader.ReadInt32(); }); if (Version >= 1.15f) number = (Scenario.NumPlayers * ( 20 /*Disables*/) + 3 /*Unknowns*/) * 4 /*Intsize*/; @@ -200,7 +200,7 @@ namespace AdrianKousz.GenieEngine x.StartingAge = reader.ReadInt32(); }); - ReadSeparator(reader, "Map"); + ReadSeparator("Map"); // Map diff --git a/src/lib/AdrianKousz.GenieEngine/ScnSerializerWriter.cs b/src/lib/AdrianKousz.GenieEngine/ScnSerializerWriter.cs index 011341d..5212be2 100644 --- a/src/lib/AdrianKousz.GenieEngine/ScnSerializerWriter.cs +++ b/src/lib/AdrianKousz.GenieEngine/ScnSerializerWriter.cs @@ -1,10 +1,11 @@ using System.IO; using System.IO.Compression; using AdrianKousz.Util; +using AdrianKousz.GenieEngine.Data; namespace AdrianKousz.GenieEngine { - internal class ScnSerializerWriter + public class ScnSerializerWriter { public static ScnSerializerWriter CreateDefault() { @@ -22,7 +23,7 @@ namespace AdrianKousz.GenieEngine // Uncompressed header - writer.WriteStringRaw(value.VersionString); + writer.Write(value.VersionString); writer.Write((int)UncompressedHeaderLength); writer.Write(value.Unknown1); writer.Write((System.Int32)DateTimes.ToUnixTime(value.Timestamp)); @@ -116,6 +117,9 @@ namespace AdrianKousz.GenieEngine writer.Write((int)writer.GetByteCount(x.ScriptAI)); writer.Write((int)writer.GetByteCount(x.ScriptCTY)); writer.Write((int)writer.GetByteCount(x.ScriptPER)); + }); + + value.PlayerSettings.ForEach(x => { writer.WriteStringRaw(x.ScriptAI); writer.WriteStringRaw(x.ScriptCTY); writer.WriteStringRaw(x.ScriptPER); diff --git a/src/lib/AdrianKousz.GenieEngine/ScnValidator.cs b/src/lib/AdrianKousz.GenieEngine/ScnValidator.cs new file mode 100644 index 0000000..ae90976 --- /dev/null +++ b/src/lib/AdrianKousz.GenieEngine/ScnValidator.cs @@ -0,0 +1,23 @@ +using AdrianKousz.GenieEngine.Data; + +namespace AdrianKousz.GenieEngine +{ + public class ScnValidator + { + public ScnValidator() + { + } + + public bool Validate(Scenario value) + { + return true; + } + + public class ValidationError + { + public T actualValue; + public T expectedValue; + public string message; + } + } +} diff --git a/src/lib/AdrianKousz.GenieEngine/ScnWriter.cs b/src/lib/AdrianKousz.GenieEngine/ScnWriter.cs new file mode 100644 index 0000000..2e78042 --- /dev/null +++ b/src/lib/AdrianKousz.GenieEngine/ScnWriter.cs @@ -0,0 +1,266 @@ +using System.IO; +using System.IO.Compression; +using System.Collections.Generic; +using AdrianKousz.Util; +using AdrianKousz.GenieEngine.Data; + +namespace AdrianKousz.GenieEngine +{ + public class ScnWriter + { + public float Version = 0; + + private ExtendedBinaryWriter writer; + + public ScnWriter(Stream output) + { + writer = new ExtendedBinaryWriter(output, Scenario.Encoding); + } + + public void Write(Scenario value) + { + Version = Version == 0 ? value.OriginalVersion : Version; + + Write(value.Header); + Flush(); + + // TODO: Make wrapper. + // This is a workaround for now. A wrapper for the reader and writer will be created. + var originalWriter = writer; + var compressedPart = new MemoryStream(); + var comp = new DeflateStream(compressedPart, CompressionMode.Compress); + writer = new ExtendedBinaryWriter(comp, Scenario.Encoding); + + writer.Write(value.NextId); + writer.Write(Version); + + value.PlayerNames.ForEach((x) => writer.WriteStringPadded(x, 256)); + if (Version >= 1.18f) { + value.StringTablePlayerNames.ForEach(writer.Write); + } + value.Players.ForEach(Write); + Write(value.Messages); + Write(value.Cinematics); + Write(value.AIData); + + WriteSeperator("Resources"); + + value.Resources.ForEach(Write); + + WriteSeperator("Victory"); + + Write(value.GlobalVictory); + value.LinearDiplomacy.ForEach(writer.Write); + writer.Write(value.RawIndividualVictory); + + WriteSeperator("Player Settings"); + + value.AlliedVictory.ForEach(writer.Write); + writer.Write(value.RawDisables); + value.StartingAges.ForEach(writer.Write); + + WriteSeperator("Map"); + + Write(value.Map); + + writer.Write((int)value.Units.Length); + value.ResourcesCopy.ForEach(Write); + value.Units.ForEach(Write); + + writer.Write(value.RawRemaining); + + Flush(); + + comp.Dispose(); + originalWriter.Write(compressedPart.ToArray()); + } + + public void Write(ScenarioHeader value) + { + var instructionlength = writer.GetZByteCount(value.Instructions); + writer.WriteStringRaw(value.OriginalVersion); + writer.Write(20 + instructionlength); + writer.Write(value.Unknown1); + writer.Write((System.Int32)DateTimes.ToUnixTime(value.Timestamp)); + writer.WriteZStringInt32(value.Instructions); + writer.Write(value.Unknown2); + writer.Write(value.PlayerCount); + } + + public void Write(PlayerInfo value) + { + writer.Write(value.Active); + writer.Write(value.Human); + writer.Write(value.Civ); + writer.Write(value.Unknown); + } + + public void Write(Messages value) + { + writer.Write(value.Unknown1); + writer.Write(value.Unknown2); + writer.Write(value.Unknown3); + writer.WriteStringInt16(value.OriginalFilename); + + if (Version >= 1.18f) { + writer.Write(value.StringTableInstructions); + writer.Write(value.StringTableHints); + writer.Write(value.StringTableVictory); + writer.Write(value.StringTableLoss); + writer.Write(value.StringTableHistory); + } + if (Version >= 1.22f) { + writer.Write(value.StringTableScouts); + } + + writer.WriteZStringInt16(value.TextInstructions); + writer.WriteZStringInt16(value.TextHints); + writer.WriteZStringInt16(value.TextVictory); + writer.WriteZStringInt16(value.TextLoss); + writer.WriteZStringInt16(value.TextHistory); + if (Version >= 1.22f) { + writer.WriteZStringInt16(value.TextScouts); + } + } + + public void Write(Cinematics value) + { + writer.WriteStringInt16(value.CinemaPregameFn); + writer.WriteStringInt16(value.CinemaVictoryFn); + writer.WriteStringInt16(value.CinemaLossFn); + writer.WriteStringInt16(value.BackgroundFn); + + var hasBitmap = value.RawBitmap.Length > 0; + + writer.Write(hasBitmap); + writer.Write(value.BitmapWidth); + writer.Write(value.BitmapHeight); + writer.Write(value.BitmapUnknown); + + if (hasBitmap) + writer.Write(value.RawBitmap); + } + + public void Write(AIData value) + { + value.PlayerAIs.ForEach(writer.WriteStringInt16); + value.PlayerCTYs.ForEach(writer.WriteStringInt16); + value.PlayerPERs.ForEach(writer.WriteStringInt16); + + for (var i = 0; i < Scenario.NumPlayers; ++i) { + var str1 = value.PlayerCustomAIs[i]; + var str2 = value.PlayerCustomCTYs[i]; + var str3 = value.PlayerCustomPERs[i]; + writer.Write(writer.GetByteCount(str1)); + writer.Write(writer.GetByteCount(str2)); + writer.Write(writer.GetByteCount(str3)); + writer.WriteStringRaw(str1); + writer.WriteStringRaw(str2); + writer.WriteStringRaw(str3); + } + + if (Version >= 1.18f) { + value.AITypes.ForEach(writer.Write); + } + } + + public void Write(ResourceInfo value) + { + writer.Write(value.Gold); + writer.Write(value.Wood); + writer.Write(value.Food); + writer.Write(value.Stone); + if (Version >= 1.18f) { + writer.Write(value.Ore); + if (Version < 1.3f) { + writer.Write(value.Unknown); + } + } + } + + public void Write(ResourceInfoCopy value) + { + writer.Write(value.Food); + writer.Write(value.Wood); + writer.Write(value.Gold); + writer.Write(value.Stone); + if (Version >= 1.18f) { + writer.Write(value.Ore); + if (Version < 1.3f) { + writer.Write(value.Unknown); + } + } + if (Version >= 1.22f) { + writer.Write(value.PopulationLimit); + } + } + + public void Write(GlobalVictoryInfo value) + { + writer.Write(value.RequireConquest); + writer.Write(value.Ruins); + writer.Write(value.Artifacts); + writer.Write(value.Discovery); + writer.Write(value.PercentExplored); + writer.Write(value.Unknown); + writer.Write(value.RequireAllCustom); + writer.Write(value.Mode); + writer.Write(value.Score); + writer.Write(value.Time); + } + + public void Write(Map value) + { + if (Version >= 1.18f) { + writer.Write(value.CameraX); + writer.Write(value.CameraY); + } + if (Version >= 1.22f) { + writer.Write(value.GeneratorId); + } + writer.Write(value.SizeX); + writer.Write(value.SizeY); + + value.LinearTiles.ForEach(x => { writer.Write(x.Id); writer.Write(x.Elevation); writer.Write(x.Unknown); }); + } + + public void Write(IList value) + { + int number = value.Count; + writer.Write(number); + value.ForEach(Write); + } + + public void Write(UnitInfo value) + { + writer.Write(value.PosX); + writer.Write(value.PosY); + writer.Write(value.Unknown1); + writer.Write(value.Id); + writer.Write(value.UnitId); + writer.Write(value.Unknown2); + writer.Write(value.Rotation); + if (Version >= 1.18f) { + writer.Write(value.InitialFrame); + writer.Write(value.GarrisonnedInId); + } + } + + public void WriteSeperator(string name) + { + writer.Write(Scenario.Separator); + } + + public void Flush() + { + writer.Flush(); + } + + private void EnsureSetVersion() + { + if (Version <= 1) + throw new System.InvalidOperationException("Set output version"); + } + } +} + diff --git a/src/lib/GenieEngineLib.csproj b/src/lib/GenieEngineLib.csproj index ce8797e..816f6d7 100644 --- a/src/lib/GenieEngineLib.csproj +++ b/src/lib/GenieEngineLib.csproj @@ -34,13 +34,16 @@ + + + + -