Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Dat/Types/S5Header.cs
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -79,4 +82,7 @@ public ReadOnlySpan<byte> Write()
public bool IsVanilla()
=> OriginalObjectFiles.GetFileSource(Name, Checksum, ObjectSource).Convert() != DatObjectSource.Custom;

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
=> [];

}
2 changes: 1 addition & 1 deletion Dat/Types/SCV5/GameStateSave1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidationResult> Validate(ValidationContext validationContext)
Expand Down
69 changes: 53 additions & 16 deletions Dat/Types/SCV5/S5File.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -18,7 +26,7 @@ public record S5File(
[property: LocoStructOffset(0x10952), LocoArrayLength(S5File.RequiredObjectsCount), Browsable(false)] List<S5Header> RequiredObjects,
IGameState? GameState,
[property: LocoStructOffset(0x4B4546)] List<TileElement>? TileElements,
List<(S5Header, byte[])> PackedObjects,
List<S5PackedObject> PackedObjects,
uint32_t Checksum
)
: ILocoStruct
Expand Down Expand Up @@ -72,12 +80,10 @@ public byte[] Write()
}

// packed
ReadOnlySpan<byte> packed = [];
byte[] packed = [];
if (Header.NumPackedObjects != 0)
{
// todo: add data here
// todo: copy ObjectManager::writePackedObjects
//packed = WritePackedObjects();
packed = WritePackedObjects();
}

// required
Expand All @@ -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)];
}
Expand All @@ -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);
Expand All @@ -134,14 +151,23 @@ public static S5File Read(ReadOnlySpan<byte> data)
}

// packed objects
List<(S5Header, byte[])> packedObjects = [];
List<S5PackedObject> 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
Expand Down Expand Up @@ -171,8 +197,6 @@ public static S5File Read(ReadOnlySpan<byte> data)
var gameStateA = SawyerStreamReader.ReadChunk<GameStateScenarioA>(ref data);
var gameStateB = SawyerStreamReader.ReadChunk<GameStateScenarioB>(ref data);
var gameStateC = SawyerStreamReader.ReadChunk<GameStateScenarioC>(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))
Expand All @@ -188,14 +212,12 @@ public static S5File Read(ReadOnlySpan<byte> data)
if (!fixFlags.HasFlag(S5FixFlags.FixFlag0) && !fixFlags.HasFlag(S5FixFlags.FixFlag1))
{
var gs2 = ByteReader.ReadLocoStruct<GameStateSave2>(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<GameStateSave1>(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();
Expand All @@ -208,6 +230,21 @@ public static S5File Read(ReadOnlySpan<byte> data)
return new S5File(header, scenarioOptions, saveDetails, requiredObjects, gameState, tileElements, packedObjects, checksum) { TileElementMap = tileElementMap, OriginalTileElementData = tileElementData };
}

byte[] WritePackedObjects()
{
var bytes = new List<byte>();
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<TileElement> tileElements)
=> tileElements.SelectMany(x => x.Write()).ToArray();

static (List<TileElement>, List<TileElement>[,]) ParseTileElements(ReadOnlySpan<byte> tileElementData, int mapWidth, int mapHeight)
{
var numTileElements = tileElementData.Length / TileElement.StructLength;
Expand Down
2 changes: 1 addition & 1 deletion Dat/Types/SCV5/ScenarioOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
97 changes: 85 additions & 12 deletions Dat/Types/SCV5/TileElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -54,22 +59,90 @@ public static TileElement Read(ReadOnlySpan<byte> 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
Expand Down
49 changes: 42 additions & 7 deletions Gui/ViewModels/Loco/SCV5ViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<GameObjDataFolder>(DownloadMissingObjects);
}
Expand All @@ -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)
Expand Down Expand Up @@ -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<string?> 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<string?> SaveAsAsync(SaveParameters saveParameters)
void SaveCore(string filename)
{
Logger.LogWarning("SaveAs is not currently implemented");
return Task.FromResult<string?>(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()
Expand Down
Loading