diff --git a/Dat/Types/S5Header.cs b/Dat/Types/S5Header.cs index d027ef66..aecad7ea 100644 --- a/Dat/Types/S5Header.cs +++ b/Dat/Types/S5Header.cs @@ -1,13 +1,16 @@ using Dat.Converters; using Dat.Data; using Dat.FileParsing; +using Definitions.ObjectModels; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; namespace Dat.Types; [TypeConverter(typeof(ExpandableObjectConverter))] [LocoStructSize(StructLength)] public class S5Header + : ILocoStruct { // this is necessary because Flags must be get-set to enable setters for SourceGame and ObjectType public S5Header(uint32_t flags, string name, uint32_t checksum) @@ -79,4 +82,7 @@ public ReadOnlySpan Write() public bool IsVanilla() => OriginalObjectFiles.GetFileSource(Name, Checksum, ObjectSource).Convert() != DatObjectSource.Custom; + public IEnumerable Validate(ValidationContext validationContext) + => []; + } diff --git a/Dat/Types/SCV5/GameStateSave1.cs b/Dat/Types/SCV5/GameStateSave1.cs index ac72aafa..32d1a736 100644 --- a/Dat/Types/SCV5/GameStateSave1.cs +++ b/Dat/Types/SCV5/GameStateSave1.cs @@ -175,7 +175,7 @@ public record GameStateScenarioC( [property: LocoStructOffset(0xC000), LocoArrayLength((int)Limits.kMaxWaves), Browsable(false)] Wave[] Waves, [property: LocoStructOffset(0xC180), LocoArrayLength((int)Limits.kMaxUserStrings * 32), Browsable(false)] uint8_t[] UserStrings, [property: LocoStructOffset(0x1C180), LocoArrayLength((int)(Limits.kMaxVehicles * Limits.kMaxRoutingsPerVehicle)), Browsable(false)] uint16_t[] Routings, - [property: LocoStructOffset(0x3B580), LocoArrayLength((int)Limits.kMaxWaves), Browsable(false)] uint8_t[] Orders + [property: LocoStructOffset(0x3B580), LocoArrayLength((int)Limits.kMaxOrders), Browsable(false)] uint8_t[] Orders ) : ILocoStruct { public IEnumerable Validate(ValidationContext validationContext) diff --git a/Dat/Types/SCV5/S5File.cs b/Dat/Types/SCV5/S5File.cs index 14590e7a..aecda641 100644 --- a/Dat/Types/SCV5/S5File.cs +++ b/Dat/Types/SCV5/S5File.cs @@ -3,12 +3,20 @@ using Dat.Data; using Dat.FileParsing; +using Dat.Types; using Definitions.ObjectModels; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Dat.Types.SCV5; +[TypeConverter(typeof(ExpandableObjectConverter))] +public sealed record S5PackedObject( + S5Header Header, + SawyerEncoding Encoding, + byte[] Data, + byte[]? RawChunk = null); + [TypeConverter(typeof(ExpandableObjectConverter))] [LocoStructSize(StructLength)] public record S5File( @@ -18,7 +26,7 @@ public record S5File( [property: LocoStructOffset(0x10952), LocoArrayLength(S5File.RequiredObjectsCount), Browsable(false)] List RequiredObjects, IGameState? GameState, [property: LocoStructOffset(0x4B4546)] List? TileElements, - List<(S5Header, byte[])> PackedObjects, + List PackedObjects, uint32_t Checksum ) : ILocoStruct @@ -72,12 +80,10 @@ public byte[] Write() } // packed - ReadOnlySpan packed = []; + byte[] packed = []; if (Header.NumPackedObjects != 0) { - // todo: add data here - // todo: copy ObjectManager::writePackedObjects - //packed = WritePackedObjects(); + packed = WritePackedObjects(); } // required @@ -98,7 +104,15 @@ public byte[] Write() } else { - if (GameState is GeneralStateSave gsv) + if (GameState is GameStateSave1 gs1) + { + gameState = [.. SawyerStreamWriter.WriteChunk(gs1, SawyerEncoding.RunLengthSingle)]; + } + else if (GameState is GameStateSave2 gs2) + { + gameState = [.. SawyerStreamWriter.WriteChunk(gs2, SawyerEncoding.RunLengthSingle)]; + } + else if (GameState is GeneralStateSave gsv) { gameState = [.. SawyerStreamWriter.WriteChunk(gsv, SawyerEncoding.RunLengthSingle)]; } @@ -109,7 +123,10 @@ public byte[] Write() throw new NotImplementedException(); } - tiles = SawyerStreamWriter.WriteChunkCore(OriginalTileElementData, SawyerEncoding.RunLengthMulti); + var tileData = TileElements is { Count: > 0 } + ? SerializeTileElements(TileElements) + : OriginalTileElementData; + tiles = SawyerStreamWriter.WriteChunkCore(tileData, SawyerEncoding.RunLengthMulti); byte[] data = [.. hdr, .. save, .. scenario, .. packed, .. required, .. gameState, .. tiles]; var checksum = data.Sum(x => x); @@ -134,14 +151,23 @@ public static S5File Read(ReadOnlySpan data) } // packed objects - List<(S5Header, byte[])> packedObjects = []; + List packedObjects = []; for (var i = 0; i < header.NumPackedObjects; ++i) { var obj = S5Header.Read(data[..S5Header.StructLength]); data = data[S5Header.StructLength..]; - var chunkData = SawyerStreamReader.ReadChunkCore(ref data); - packedObjects.Add((obj, chunkData.ToArray())); + var objectHeader = ObjectHeader.Read(data[..ObjectHeader.StructLength]); + data = data[ObjectHeader.StructLength..]; + + var encodedData = data[..(int)objectHeader.DataLength]; + data = data[(int)objectHeader.DataLength..]; + + var decodedData = SawyerStreamReader.Decode(objectHeader.Encoding, encodedData); + var rawChunk = new byte[ObjectHeader.StructLength + encodedData.Length]; + objectHeader.Write().CopyTo(rawChunk); + encodedData.CopyTo(rawChunk.AsSpan(ObjectHeader.StructLength)); + packedObjects.Add(new S5PackedObject(obj, objectHeader.Encoding, decodedData, rawChunk)); } // read required objects @@ -171,8 +197,6 @@ public static S5File Read(ReadOnlySpan data) var gameStateA = SawyerStreamReader.ReadChunk(ref data); var gameStateB = SawyerStreamReader.ReadChunk(ref data); var gameStateC = SawyerStreamReader.ReadChunk(ref data); - var newFlags = gameStateA.FixFlags | S5FixFlags.FixFlag0; // fixState - gameStateA = gameStateA with { FixFlags = newFlags }; // fixState gameState = new GameStateScenario(gameStateA, gameStateB, gameStateC); if (gameStateA.GameStateFlags.HasFlag(GameStateFlags.TileManagerLoaded)) @@ -188,14 +212,12 @@ public static S5File Read(ReadOnlySpan data) if (!fixFlags.HasFlag(S5FixFlags.FixFlag0) && !fixFlags.HasFlag(S5FixFlags.FixFlag1)) { var gs2 = ByteReader.ReadLocoStruct(chunkData); - var newFlags = gs2.GeneralState.FixFlags | S5FixFlags.FixFlag0; // fixState - gameState = gs2 with { GeneralState = gs2.GeneralState with { FixFlags = newFlags } }; // fixState + gameState = gs2; } else { var gs1 = ByteReader.ReadLocoStruct(chunkData); - var newFlags = gs1.GeneralState.FixFlags | S5FixFlags.FixFlag0; // fixState - gameState = gs1 with { GeneralState = gs1.GeneralState with { FixFlags = newFlags } }; // fixState + gameState = gs1; } tileElementData = SawyerStreamReader.ReadChunkCore(ref data).ToArray(); @@ -208,6 +230,21 @@ public static S5File Read(ReadOnlySpan data) return new S5File(header, scenarioOptions, saveDetails, requiredObjects, gameState, tileElements, packedObjects, checksum) { TileElementMap = tileElementMap, OriginalTileElementData = tileElementData }; } + byte[] WritePackedObjects() + { + var bytes = new List(); + foreach (var packedObject in PackedObjects) + { + bytes.AddRange(packedObject.Header.Write().ToArray()); + bytes.AddRange(packedObject.RawChunk ?? SawyerStreamWriter.WriteChunkCore(packedObject.Data, packedObject.Encoding)); + } + + return [.. bytes]; + } + + static byte[] SerializeTileElements(IEnumerable tileElements) + => tileElements.SelectMany(x => x.Write()).ToArray(); + static (List, List[,]) ParseTileElements(ReadOnlySpan tileElementData, int mapWidth, int mapHeight) { var numTileElements = tileElementData.Length / TileElement.StructLength; diff --git a/Dat/Types/SCV5/ScenarioOptions.cs b/Dat/Types/SCV5/ScenarioOptions.cs index c4dba6cb..dd759858 100644 --- a/Dat/Types/SCV5/ScenarioOptions.cs +++ b/Dat/Types/SCV5/ScenarioOptions.cs @@ -15,7 +15,7 @@ public record ScenarioOptions( [property: LocoStructOffset(0x06)] ScenarioFlags ScenarioFlags, [property: LocoStructOffset(0x08)] uint8_t MadeAnyChanges, [property: LocoStructOffset(0x09)] uint8_t var_9, - [property: LocoStructOffset(0x0A), LocoArrayLength(32)] LandDistributionPattern LandDistributionPatterns, + [property: LocoStructOffset(0x0A), LocoArrayLength(32)] LandDistributionPattern[] LandDistributionPatterns, [property: LocoStructOffset(0x2A), LocoArrayLength(64), Browsable(false)] char_t[] ScenarioName, // this is a string [property: LocoStructOffset(0x6A), LocoArrayLength(256), Browsable(false)] char_t[] ScenarioDetails, // this is a string [property: LocoStructOffset(0x16A)] S5Header ScenarioText, diff --git a/Dat/Types/SCV5/TileElement.cs b/Dat/Types/SCV5/TileElement.cs index fbda8f47..dae5dce5 100644 --- a/Dat/Types/SCV5/TileElement.cs +++ b/Dat/Types/SCV5/TileElement.cs @@ -26,7 +26,12 @@ public abstract class TileElement public const uint8_t FLAG_GHOST = 1 << 4; public const uint8_t FLAG_LAST = 1 << 7; - public ElementType Type { get; set; } + public uint8_t TypeByte { get; set; } + public ElementType Type + { + get => (ElementType)((TypeByte & 0x3C) >> 2); + set => TypeByte = (uint8_t)((TypeByte & ~0x3C) | (((uint8_t)value << 2) & 0x3C)); + } public uint8_t Flags { get; set; } public uint8_t BaseZ { get; set; } public uint8_t ClearZ { get; set; } @@ -54,22 +59,90 @@ public static TileElement Read(ReadOnlySpan data) { ArgumentOutOfRangeException.ThrowIfNotEqual(data.Length, StructLength); - var Type = (ElementType)((data[0] & 0x3C) >> 2); // https://github.com/OpenLoco/OpenLoco/blob/master/src/OpenLoco/src/Map/Tile.cpp#L23 + var type = (ElementType)((data[0] & 0x3C) >> 2); // https://github.com/OpenLoco/OpenLoco/blob/master/src/OpenLoco/src/Map/Tile.cpp#L23 - return Type switch + return type switch { - ElementType.Building => new BuildingElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = BitConverter.ToUInt16(data[6..8]) }, - ElementType.Industry => new IndustryElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], IndustryId = data[4], _5 = data[5], _6 = BitConverter.ToUInt16(data[6..8]) }, - ElementType.Road => new RoadElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] }, - ElementType.Signal => new SignalElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], LeftSide = new SignalElement.Side() { _4 = data[4], _5 = data[5] }, RightSide = new SignalElement.Side() { _4 = data[6], _5 = data[7] } }, - ElementType.Station => new StationElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], StationId = BitConverter.ToUInt16(data[6..8]) }, - ElementType.Surface => new SurfaceElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], Slope = data[4], Water = data[5], Terrain = data[6], _7 = data[7] }, - ElementType.Track => new TrackElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] }, - ElementType.Tree => new TreeElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] }, - ElementType.Wall => new WallElement() { Type = Type, Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] }, + ElementType.Building => new BuildingElement() { TypeByte = data[0], Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = BitConverter.ToUInt16(data[6..8]) }, + ElementType.Industry => new IndustryElement() { TypeByte = data[0], Flags = data[1], BaseZ = data[2], ClearZ = data[3], IndustryId = data[4], _5 = data[5], _6 = BitConverter.ToUInt16(data[6..8]) }, + ElementType.Road => new RoadElement() { TypeByte = data[0], Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] }, + ElementType.Signal => new SignalElement() { TypeByte = data[0], Flags = data[1], BaseZ = data[2], ClearZ = data[3], LeftSide = new SignalElement.Side() { _4 = data[4], _5 = data[5] }, RightSide = new SignalElement.Side() { _4 = data[6], _5 = data[7] } }, + ElementType.Station => new StationElement() { TypeByte = data[0], Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], StationId = BitConverter.ToUInt16(data[6..8]) }, + ElementType.Surface => new SurfaceElement() { TypeByte = data[0], Flags = data[1], BaseZ = data[2], ClearZ = data[3], Slope = data[4], Water = data[5], Terrain = data[6], _7 = data[7] }, + ElementType.Track => new TrackElement() { TypeByte = data[0], Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] }, + ElementType.Tree => new TreeElement() { TypeByte = data[0], Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] }, + ElementType.Wall => new WallElement() { TypeByte = data[0], Flags = data[1], BaseZ = data[2], ClearZ = data[3], _4 = data[4], _5 = data[5], _6 = data[6], _7 = data[7] }, _ => throw new NotImplementedException(), }; } + + public byte[] Write() + { + var data = new byte[StructLength]; + data[0] = TypeByte; + data[1] = Flags; + data[2] = BaseZ; + data[3] = ClearZ; + + switch (this) + { + case BuildingElement x: + data[4] = x._4; + data[5] = x._5; + BitConverter.GetBytes(x._6).CopyTo(data, 6); + break; + case IndustryElement x: + data[4] = x.IndustryId; + data[5] = x._5; + BitConverter.GetBytes(x._6).CopyTo(data, 6); + break; + case RoadElement x: + data[4] = x._4; + data[5] = x._5; + data[6] = x._6; + data[7] = x._7; + break; + case SignalElement x: + data[4] = x.LeftSide._4; + data[5] = x.LeftSide._5; + data[6] = x.RightSide._4; + data[7] = x.RightSide._5; + break; + case StationElement x: + data[4] = x._4; + data[5] = x._5; + BitConverter.GetBytes(x.StationId).CopyTo(data, 6); + break; + case SurfaceElement x: + data[4] = x.Slope; + data[5] = x.Water; + data[6] = x.Terrain; + data[7] = x._7; + break; + case TrackElement x: + data[4] = x._4; + data[5] = x._5; + data[6] = x._6; + data[7] = x._7; + break; + case TreeElement x: + data[4] = x._4; + data[5] = x._5; + data[6] = x._6; + data[7] = x._7; + break; + case WallElement x: + data[4] = x._4; + data[5] = x._5; + data[6] = x._6; + data[7] = x._7; + break; + default: + throw new NotImplementedException($"Unsupported tile element type {GetType().Name}"); + } + + return data; + } } public class BuildingElement : TileElement diff --git a/Gui/ViewModels/Loco/SCV5ViewModel.cs b/Gui/ViewModels/Loco/SCV5ViewModel.cs index 309609f8..469f5541 100644 --- a/Gui/ViewModels/Loco/SCV5ViewModel.cs +++ b/Gui/ViewModels/Loco/SCV5ViewModel.cs @@ -58,8 +58,6 @@ public SCV5ViewModel(FileSystemItem currentFile, ObjectEditorContext editorConte : base(currentFile, editorContext) { RequiredObjects = new RequiredObjectsListViewModel(editorContext); - SaveIsVisible = false; - SaveAsIsVisible = false; Load(); DownloadMissingObjectsToGameObjDataCommand = ReactiveCommand.CreateFromTask(DownloadMissingObjects); } @@ -86,7 +84,7 @@ public override void Load() .Select(x => x.Convert()); RequiredObjects.Replace(headers); - var po = Model.PackedObjects.ConvertAll(x => new ObjectModelHeaderViewModel(x.Item1.Convert())).OrderBy(x => x.Name); + var po = Model.PackedObjects.ConvertAll(x => new ObjectModelHeaderViewModel(x.Header.Convert())).OrderBy(x => x.Name); PackedObjects = [with([.. po])]; _ = this.WhenAnyValue(o => o.TileElementX) @@ -315,12 +313,49 @@ void DrawMap() } public override void Save() - => Logger.LogWarning("Save is not currently implemented"); + { + var savePath = CurrentFile.FileLocation == FileLocation.Local + ? CurrentFile.FileName + : Path.Combine(EditorContext.Settings.DownloadFolder, Path.ChangeExtension($"{CurrentFile.DisplayName}-{CurrentFile.Id}", ".sv5")); + + if (string.IsNullOrEmpty(savePath)) + { + Logger.LogError("Cannot save scenario/save file because save path is empty"); + return; + } + + SaveCore(savePath); + } + + public override async Task SaveAsAsync(SaveParameters saveParameters) + { + var saveFile = await PlatformSpecific.SaveFilePicker(PlatformSpecific.SCV5FileTypes); + if (saveFile == null) + { + return null; + } + + var savePath = saveFile.Path.LocalPath; + SaveCore(savePath); + return savePath; + } - public override Task SaveAsAsync(SaveParameters saveParameters) + void SaveCore(string filename) { - Logger.LogWarning("SaveAs is not currently implemented"); - return Task.FromResult(null); + if (Model == null) + { + Logger.LogError("Cannot save scenario/save file because model is null"); + return; + } + + var newFile = Model with + { + RequiredObjects = [.. RequiredObjects.Items.Select(x => x.Convert())], + }; + + var bytes = newFile.Write(); + File.WriteAllBytes(filename, bytes); + Logger.LogInformation("Saved scenario/save file to {Filename}", filename); } //public override void Save() diff --git a/Gui/Views/MainWindow.axaml b/Gui/Views/MainWindow.axaml index d0b8c163..aa95b93b 100644 --- a/Gui/Views/MainWindow.axaml +++ b/Gui/Views/MainWindow.axaml @@ -42,26 +42,7 @@ - - - - - - - - - - - - - - - - - - - - + diff --git a/Tests/SCV5SerializationTests.cs b/Tests/SCV5SerializationTests.cs new file mode 100644 index 00000000..b10be6eb --- /dev/null +++ b/Tests/SCV5SerializationTests.cs @@ -0,0 +1,146 @@ +using Dat.Data; +using Dat.FileParsing; +using Dat.Types; +using Dat.Types.SCV5; +using NUnit.Framework; +using Logger = Common.Logging.Logger; + +namespace Dat.Tests; + +[TestFixture] +public class SCV5SerializationTests +{ + [TestCase(S5FileType.Scenario, ".sc5")] + [TestCase(S5FileType.SavedGame, ".sv5")] + public void LoadThenSave_PreservesBytes(S5FileType fileType, string extension) + { + var inputBytes = CreateS5FileBytes(fileType); + var inputPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}{extension}"); + var outputPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}{extension}"); + try + { + File.WriteAllBytes(inputPath, inputBytes); + + var loaded = SawyerStreamReader.LoadSave(inputPath, new Logger()); + Assert.That(loaded, Is.Not.Null); + + File.WriteAllBytes(outputPath, loaded!.Write()); + + var outputBytes = File.ReadAllBytes(outputPath); + Assert.That(outputBytes, Is.EqualTo(inputBytes).AsCollection); + } + finally + { + if (File.Exists(inputPath)) + { + File.Delete(inputPath); + } + + if (File.Exists(outputPath)) + { + File.Delete(outputPath); + } + } + } + + static byte[] CreateS5FileBytes(S5FileType fileType) + { + var hasSaveDetails = fileType == S5FileType.SavedGame; + List<(S5Header Header, SawyerEncoding Encoding, byte[] Data)> packedObjects = fileType == S5FileType.Scenario + ? [(new S5Header(0x13, "PACKOBJ1", 0x12345678), SawyerEncoding.RunLengthSingle, new byte[] { 1, 1, 2, 2, 3, 4, 4, 4 })] + : []; + + var header = new S5FileHeader( + fileType, + hasSaveDetails ? HeaderFlags.HasSaveDetails : HeaderFlags.None, + (ushort)packedObjects.Count, + 0x62262, + 0x62300, + new byte[20]); + + var data = new List(); + data.AddRange(SawyerStreamWriter.WriteChunk(header, SawyerEncoding.Rotate).ToArray()); + + if (hasSaveDetails) + { + data.AddRange(SawyerStreamWriter.WriteChunkCore(CreateSaveDetailsRaw(), SawyerEncoding.Rotate)); + } + + if (fileType == S5FileType.Scenario) + { + data.AddRange(SawyerStreamWriter.WriteChunkCore(CreateScenarioOptionsRaw(), SawyerEncoding.Rotate)); + } + + foreach (var (packedHeader, encoding, packedData) in packedObjects) + { + data.AddRange(packedHeader.Write().ToArray()); + data.AddRange(SawyerStreamWriter.WriteChunkCore(packedData, encoding)); + } + + data.AddRange(SawyerStreamWriter.WriteChunkCore(CreateRequiredObjectsRaw(), SawyerEncoding.Rotate)); + + if (fileType == S5FileType.Scenario) + { + data.AddRange(SawyerStreamWriter.WriteChunkCore(CreateScenarioGameStateA(), SawyerEncoding.RunLengthSingle)); + data.AddRange(SawyerStreamWriter.WriteChunkCore(new byte[0x123480], SawyerEncoding.RunLengthSingle)); + data.AddRange(SawyerStreamWriter.WriteChunkCore(new byte[0x79D80], SawyerEncoding.RunLengthSingle)); + } + else + { + data.AddRange(SawyerStreamWriter.WriteChunkCore(CreateSaveGameStateRaw(), SawyerEncoding.RunLengthSingle)); + } + + data.AddRange(SawyerStreamWriter.WriteChunkCore(CreateTileElementsRaw(), SawyerEncoding.RunLengthMulti)); + + var checksum = (uint)data.Sum(x => x); + data.AddRange(BitConverter.GetBytes(checksum)); + return [.. data]; + } + + static byte[] CreateSaveDetailsRaw() + { + var saveDetails = new byte[SaveDetails.StructLength]; + BitConverter.GetBytes((ushort)2).CopyTo(saveDetails, 0xC59C); + BitConverter.GetBytes((ushort)1).CopyTo(saveDetails, 0xC59E); + return saveDetails; + } + + static byte[] CreateScenarioOptionsRaw() + { + var scenarioOptions = new byte[ScenarioOptions.StructLength]; + BitConverter.GetBytes((ushort)2).CopyTo(scenarioOptions, 0x41C4); + BitConverter.GetBytes((ushort)1).CopyTo(scenarioOptions, 0x41C6); + return scenarioOptions; + } + + static byte[] CreateScenarioGameStateA() + { + var stateA = new byte[0xB96C]; + BitConverter.GetBytes((uint)GameStateFlags.TileManagerLoaded).CopyTo(stateA, 0x10); + return stateA; + } + + static byte[] CreateSaveGameStateRaw() + { + var saveState = new byte[0x4A0644]; + BitConverter.GetBytes((ushort)S5FixFlags.FixFlag0).CopyTo(saveState, 0x434); + return saveState; + } + + static byte[] CreateRequiredObjectsRaw() + { + var required = new byte[S5File.RequiredObjectsCount * S5Header.StructLength]; + var emptyHeader = ObjectManager.FillHeader.Write().ToArray(); + for (var i = 0; i < S5File.RequiredObjectsCount; i++) + { + emptyHeader.CopyTo(required, i * S5Header.StructLength); + } + return required; + } + + static byte[] CreateTileElementsRaw() + => [ + 0x00, 0x80, 0x05, 0x05, 0x03, 0x00, 0x02, 0x00, + 0x06, 0x80, 0x06, 0x06, 0x01, 0x12, 0x30, 0x40 + ]; +}