Skip to content

Commit 6676c06

Browse files
committed
add start of encoding
1 parent d722ffc commit 6676c06

File tree

5 files changed

+145
-63
lines changed

5 files changed

+145
-63
lines changed

Dat/FileParsing/SawyerStreamReader.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -502,14 +502,15 @@ public static ILocoStruct GetLocoStruct(ObjectType objectType, ReadOnlySpan<byte
502502
};
503503

504504
// taken from openloco's SawyerStreamReader::readChunk
505-
public static byte[] Decode(SawyerEncoding encoding, ReadOnlySpan<byte> data, int minDecodedBytes = int.MaxValue) => encoding switch
506-
{
507-
SawyerEncoding.Uncompressed => data.ToArray(),
508-
SawyerEncoding.RunLengthSingle => DecodeRunLengthSingle(data, minDecodedBytes),
509-
SawyerEncoding.RunLengthMulti => DecodeRunLengthMulti(DecodeRunLengthSingle(data)),
510-
SawyerEncoding.Rotate => DecodeRotate(data),
511-
_ => throw new InvalidDataException("Unknown chunk encoding scheme"),
512-
};
505+
public static byte[] Decode(SawyerEncoding encoding, ReadOnlySpan<byte> data, int minDecodedBytes = int.MaxValue)
506+
=> encoding switch
507+
{
508+
SawyerEncoding.Uncompressed => data.ToArray(),
509+
SawyerEncoding.RunLengthSingle => DecodeRunLengthSingle(data, minDecodedBytes),
510+
SawyerEncoding.RunLengthMulti => DecodeRunLengthMulti(DecodeRunLengthSingle(data)),
511+
SawyerEncoding.Rotate => DecodeRotate(data),
512+
_ => throw new InvalidDataException("Unknown chunk encoding scheme"),
513+
};
513514

514515
public static (RiffWavHeader header, byte[] data) LoadWavFile(string filename)
515516
=> LoadWavFile(File.ReadAllBytes(filename));
@@ -578,7 +579,7 @@ public static ReadOnlySpan<byte> ReadChunkCore(ref ReadOnlySpan<byte> data)
578579
// taken from openloco SawyerStreamReader::decodeRunLengthSingle
579580
static byte[] DecodeRunLengthSingle(ReadOnlySpan<byte> data, int minDecodedBytes = int.MaxValue)
580581
{
581-
var ms = new MemoryStream();
582+
using var ms = new MemoryStream();
582583

583584
for (var i = 0; i < data.Length; ++i)
584585
{

Dat/FileParsing/SawyerStreamWriter.cs

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,13 @@ public static void ExportMusicAsWave(string filename, RiffWavHeader header, byte
9898
}
9999
}
100100

101-
public static void Save(string filename, string objName, SourceGame sourceGame, ILocoObject locoObject, ILogger logger, bool allowWritingAsVanilla)
101+
public static void Save(string filename, string objName, SourceGame sourceGame, SawyerEncoding encoding, ILocoObject locoObject, ILogger logger, bool allowWritingAsVanilla)
102102
{
103103
ArgumentNullException.ThrowIfNull(locoObject);
104104

105105
logger.Info($"Writing \"{objName}\" to {filename}");
106106

107-
var objBytes = WriteLocoObject(objName, sourceGame, logger, locoObject, allowWritingAsVanilla);
107+
var objBytes = WriteLocoObject(objName, sourceGame, encoding, logger, locoObject, allowWritingAsVanilla);
108108

109109
try
110110
{
@@ -123,43 +123,110 @@ public static void Save(string filename, string objName, SourceGame sourceGame,
123123
logger.Info($"{objName} successfully saved to {filename}");
124124
}
125125

126-
public static ReadOnlySpan<byte> WriteLocoObject(string objName, SourceGame sourceGame, ILogger logger, ILocoObject obj, bool allowWritingAsVanilla)
127-
=> WriteLocoObjectStream(objName, sourceGame, logger, obj, allowWritingAsVanilla).ToArray();
126+
public static ReadOnlySpan<byte> WriteLocoObject(string objName, SourceGame sourceGame, SawyerEncoding encoding, ILogger logger, ILocoObject obj, bool allowWritingAsVanilla)
127+
=> WriteLocoObjectStream(objName, sourceGame, encoding, logger, obj, allowWritingAsVanilla).ToArray();
128+
public static byte[] Encode(SawyerEncoding encoding, ReadOnlySpan<byte> data)
129+
=> data.ToArray();
128130

129-
public static MemoryStream WriteLocoObjectStream(string objName, SourceGame sourceGame, ILogger logger, ILocoObject obj, bool allowWritingAsVanilla)
131+
//public static byte[] Encode(SawyerEncoding encoding, ReadOnlySpan<byte> data)
132+
// => encoding switch
133+
// {
134+
// SawyerEncoding.Uncompressed => data.ToArray(),
135+
// SawyerEncoding.RunLengthSingle => EncodeRunLengthSingle(data),
136+
// SawyerEncoding.RunLengthMulti => throw new NotImplementedException(),
137+
// SawyerEncoding.Rotate => throw new NotImplementedException(),
138+
// _ => throw new InvalidDataException("Unknown chunk encoding scheme"),
139+
// };
140+
141+
public static byte[] EncodeRunLengthSingle(ReadOnlySpan<byte> data)
130142
{
131-
using var objStream = new MemoryStream();
143+
using var buffer = new MemoryStream();
144+
145+
var src = 0;
146+
var srcEnd = data.Length;
147+
var srcNormStart = 0;
148+
byte count = 0;
149+
150+
while (src < srcEnd - 1)
151+
{
152+
if ((count != 0 && data[src] == data[src + 1]) || count > 125)
153+
{
154+
buffer.WriteByte((byte)(count - 1));
155+
buffer.Write(data[srcNormStart..(srcNormStart + count)]);
156+
srcNormStart += count;
157+
count = 0;
158+
}
159+
if (data[src] == data[src + 1])
160+
{
161+
for (; count < 125 && src + count < srcEnd; count++)
162+
{
163+
if (data[src] != data[src + count])
164+
{
165+
break;
166+
}
167+
}
168+
buffer.WriteByte((byte)(257 - count));
169+
buffer.WriteByte(data[src]);
170+
src += count;
171+
srcNormStart = src;
172+
count = 0;
173+
}
174+
else
175+
{
176+
count++;
177+
src++;
178+
}
179+
}
180+
if (src == srcEnd - 1)
181+
{
182+
count++;
183+
}
184+
if (count != 0)
185+
{
186+
buffer.WriteByte((byte)(count - 1));
187+
buffer.Write(data[srcNormStart..(srcNormStart + count)]);
188+
}
189+
190+
return buffer.ToArray();
191+
}
192+
193+
public static MemoryStream WriteLocoObjectStream(string objName, SourceGame sourceGame, SawyerEncoding encoding, ILogger logger, ILocoObject obj, bool allowWritingAsVanilla)
194+
{
195+
using var rawObjStream = new MemoryStream();
132196

133197
// obj
134198
var objBytes = ByteWriter.WriteLocoStruct(obj.Object);
135-
objStream.Write(objBytes);
199+
rawObjStream.Write(objBytes);
136200

137201
// string table
138202
foreach (var ste in obj.StringTable.Table)
139203
{
140204
foreach (var language in ste.Value.Where(str => !string.IsNullOrEmpty(str.Value))) // skip strings with empty content
141205
{
142-
objStream.WriteByte((byte)language.Key);
206+
rawObjStream.WriteByte((byte)language.Key);
143207

144208
var strBytes = Encoding.Latin1.GetBytes(language.Value);
145-
objStream.Write(strBytes, 0, strBytes.Length);
146-
objStream.WriteByte((byte)'\0');
209+
rawObjStream.Write(strBytes, 0, strBytes.Length);
210+
rawObjStream.WriteByte((byte)'\0');
147211
}
148212

149-
objStream.WriteByte(0xff);
213+
rawObjStream.WriteByte(0xff);
150214
}
151215

152216
// variable data
153217
if (obj.Object is ILocoStructVariableData objV)
154218
{
155219
var variableBytes = objV.Save();
156-
objStream.Write(variableBytes);
220+
rawObjStream.Write(variableBytes);
157221
}
158222

159223
// graphics data
160-
SaveImageTable(obj.G1Elements, objStream);
224+
SaveImageTable(obj.G1Elements, rawObjStream);
225+
226+
rawObjStream.Flush();
161227

162-
objStream.Flush();
228+
//todo: actually use encoding
229+
using var encodedObjStream = new MemoryStream(Encode(encoding, rawObjStream.ToArray()));
163230

164231
// now obj is written, we can calculate the few bits of metadata (checksum and length) for the headers
165232

@@ -181,9 +248,9 @@ public static MemoryStream WriteLocoObjectStream(string objName, SourceGame sour
181248
// calculate checksum
182249
var headerFlag = BitConverter.GetBytes(s5Header.Flags).AsSpan()[0..1];
183250
var asciiName = objName.PadRight(8, ' ').Take(8).Select(c => (byte)c).ToArray();
184-
s5Header.Checksum = SawyerStreamUtils.ComputeObjectChecksum(headerFlag, asciiName, objStream.ToArray());
251+
s5Header.Checksum = SawyerStreamUtils.ComputeObjectChecksum(headerFlag, asciiName, encodedObjStream.ToArray());
185252

186-
var objHeader = new ObjectHeader(SawyerEncoding.Uncompressed, (uint32_t)objStream.Length);
253+
var objHeader = new ObjectHeader(SawyerEncoding.Uncompressed, (uint32_t)encodedObjStream.Length);
187254

188255
// actual writing
189256
var headerStream = new MemoryStream();
@@ -195,13 +262,13 @@ public static MemoryStream WriteLocoObjectStream(string objName, SourceGame sour
195262
headerStream.Write(objHeader.Write());
196263

197264
// loco object itself, including string and graphics table
198-
headerStream.Write(objStream.ToArray());
265+
headerStream.Write(encodedObjStream.ToArray());
199266

200267
// stream cleanup
201268
headerStream.Flush();
202269

203270
headerStream.Close();
204-
objStream.Close();
271+
encodedObjStream.Close();
205272

206273
return headerStream;
207274
}

Gui/ViewModels/DatTypes/DatObjectEditorViewModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using OpenLoco.Common.Logging;
22
using OpenLoco.Dat;
3+
using OpenLoco.Dat.Data;
34
using OpenLoco.Dat.FileParsing;
45
using OpenLoco.Dat.Objects.Sound;
56
using OpenLoco.Dat.Types;
@@ -181,6 +182,7 @@ void SaveCore(string filename)
181182
SawyerStreamWriter.Save(filename,
182183
S5HeaderViewModel?.Name ?? CurrentObject.DatFileInfo.S5Header.Name,
183184
S5HeaderViewModel?.SourceGame ?? CurrentObject.DatFileInfo.S5Header.SourceGame,
185+
SawyerEncoding.Uncompressed, // todo: change based on what user selected
184186
CurrentObject.LocoObject,
185187
Logger,
186188
Model.Settings.AllowSavingAsVanillaObject);

Tests/LoadSaveTests.cs

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,71 +17,55 @@ public class LoadSaveTests
1717
// TODO: find a way to not have to hardcode a path here (but this may be impossible as it will depend on a user's PC and Loco install path)
1818
// TODO: find a nicer (and more automated) way to check Name+Image fields, StringTable and G1Table
1919

20-
static (ILocoObject, T) LoadObject<T>(string filename) where T : ILocoStruct
21-
{
22-
filename = Path.Combine(BaseObjDataPath, filename);
23-
var fileSize = new FileInfo(filename).Length;
24-
var logger = new Logger();
25-
var loaded = SawyerStreamReader.LoadFullObjectFromFile(filename, logger);
20+
static (DatFileInfo, ILocoObject, T) LoadObject<T>(string filename) where T : ILocoStruct
21+
=> LoadObject<T>(File.ReadAllBytes(Path.Combine(BaseObjDataPath, filename)));
2622

27-
Assert.That(loaded, Is.Not.Null);
28-
var (datFileInfo, locoObject) = loaded.Value;
29-
30-
Assert.Multiple(() =>
31-
{
32-
Assert.That(datFileInfo.S5Header.Checksum, Is.EqualTo(OriginalObjectFiles.Names[datFileInfo.S5Header.Name].SteamChecksum));
33-
Assert.That(locoObject, Is.Not.Null);
34-
Assert.That(datFileInfo!.ObjectHeader.DataLength, Is.EqualTo(fileSize - S5Header.StructLength - ObjectHeader.StructLength), "ObjectHeader.Length didn't match actual size of struct");
35-
});
36-
37-
return (locoObject!, (T)locoObject!.Object);
38-
}
39-
40-
static (ILocoObject, T) LoadObject<T>(ReadOnlySpan<byte> data) where T : ILocoStruct
23+
static (DatFileInfo, ILocoObject, T) LoadObject<T>(ReadOnlySpan<byte> data) where T : ILocoStruct
4124
{
4225
var logger = new Logger();
43-
var (datFileInfo, locoObject) = SawyerStreamReader.LoadFullObjectFromStream(data, logger: logger);
26+
var (datFileInfo, locoObject) = SawyerStreamReader.LoadFullObjectFromStream(data, logger);
4427

4528
#pragma warning disable IDE0079 // Remove unnecessary suppression
4629
#pragma warning disable NUnit2045 // Use Assert.Multiple - cannot use a ReadOnlySpan inside an anonymous method
30+
Assert.That(datFileInfo.S5Header.Checksum, Is.EqualTo(OriginalObjectFiles.Names[datFileInfo.S5Header.Name].SteamChecksum));
4731
Assert.That(locoObject, Is.Not.Null);
4832
#pragma warning restore NUnit2045 // Use Assert.Multiple
4933
#pragma warning restore IDE0079 // Remove unnecessary suppression
5034
Assert.That(datFileInfo.ObjectHeader.DataLength, Is.EqualTo(data.Length - S5Header.StructLength - ObjectHeader.StructLength), "ObjectHeader.Length didn't match actual size of struct");
5135

52-
return (locoObject!, (T)locoObject!.Object);
36+
return (datFileInfo, locoObject!, (T)locoObject!.Object);
5337
}
5438

5539
static void LoadSaveGenericTest<T>(string filename, Action<ILocoObject, T> assertFunc) where T : ILocoStruct
5640
{
57-
var (obj1, struc1) = LoadObject<T>(filename);
41+
var (datInfo1, obj1, struc1) = LoadObject<T>(filename);
5842
assertFunc(obj1, struc1);
5943

6044
var logger = new Logger();
6145
var objectName = filename.Split('.')[0];
62-
var bytes1 = SawyerStreamWriter.WriteLocoObject(objectName, SourceGame.Vanilla, logger, obj1, false);
46+
var bytes1 = SawyerStreamWriter.WriteLocoObject(objectName, SourceGame.Vanilla, datInfo1.ObjectHeader.Encoding, logger, obj1, false);
6347

64-
var (obj2, struc2) = LoadObject<T>(bytes1);
48+
var (datInfo2, obj2, struc2) = LoadObject<T>(bytes1);
6549
assertFunc(obj2, struc2);
6650

67-
var bytes2 = SawyerStreamWriter.WriteLocoObject(objectName, SourceGame.Vanilla, logger, obj2, false);
51+
var bytes2 = SawyerStreamWriter.WriteLocoObject(objectName, SourceGame.Vanilla, datInfo2.ObjectHeader.Encoding, logger, obj2, false);
6852

6953
// we could just simply compare byte arrays and be done, but i wanted something that makes it easier to diagnose problems
7054

7155
// grab headers first
7256
var s5Header1 = S5Header.Read(bytes1[0..S5Header.StructLength]);
7357
var s5Header2 = S5Header.Read(bytes2[0..S5Header.StructLength]);
58+
AssertS5Headers(s5Header1, s5Header2);
7459

7560
var objHeader1 = ObjectHeader.Read(bytes1[S5Header.StructLength..(S5Header.StructLength + ObjectHeader.StructLength)]);
7661
var objHeader2 = ObjectHeader.Read(bytes2[S5Header.StructLength..(S5Header.StructLength + ObjectHeader.StructLength)]);
62+
AssertObjHeaders(objHeader1, objHeader2);
7763

7864
// then grab object bytes
7965
var bytes1ObjArr = bytes1[21..].ToArray();
8066
var bytes2ObjArr = bytes2[21..].ToArray();
81-
82-
AssertS5Headers(s5Header1, s5Header2);
83-
AssertObjHeaders(objHeader1, objHeader2);
8467
Assert.That(bytes1ObjArr.ToArray(), Is.EqualTo(bytes2ObjArr.ToArray()));
68+
8569
}
8670

8771
static void AssertS5Headers(S5Header expected, S5Header actual)
@@ -99,6 +83,36 @@ static void AssertObjHeaders(ObjectHeader expected, ObjectHeader actual)
9983
Assert.That(actual.DataLength, Is.EqualTo(expected.DataLength));
10084
});
10185

86+
[TestCase]
87+
public void TestEncoding()
88+
{
89+
var logger = new Logger();
90+
91+
//var filename = "OGFOWL.dat";
92+
//var path = "C:\\Users\\bigba\\source\\repos\\OpenLoco\\OpenGraphics\\objects\\Vehicle\\Train\\4F";
93+
//var fullData = File.ReadAllBytes(Path.Combine(path, filename));
94+
95+
var filename = "707.dat";
96+
var fullData = File.ReadAllBytes(Path.Combine(BaseObjDataPath, filename));
97+
98+
if (!SawyerStreamReader.TryGetHeadersFromBytes(fullData, out var hdrs, logger))
99+
{
100+
Assert.Fail();
101+
return;
102+
}
103+
104+
var remainingData = fullData[(S5Header.StructLength + ObjectHeader.StructLength)..];
105+
106+
var decoded = SawyerStreamReader.Decode(hdrs.Obj.Encoding, remainingData);
107+
var encoded = SawyerStreamWriter.Encode(hdrs.Obj.Encoding, decoded);
108+
var decoded2 = SawyerStreamReader.Decode(hdrs.Obj.Encoding, encoded);
109+
var encoded2 = SawyerStreamWriter.Encode(hdrs.Obj.Encoding, decoded2);
110+
111+
Assert.That(remainingData, Is.EqualTo(encoded));
112+
Assert.That(decoded, Is.EqualTo(decoded2));
113+
Assert.That(encoded, Is.EqualTo(encoded2));
114+
}
115+
102116
[TestCase("AIRPORT1.DAT")]
103117
public void AirportObject(string objectName)
104118
{
@@ -656,7 +670,7 @@ void assertFunc(ILocoObject obj, RoadStationObject struc) => Assert.Multiple(()
656670
Assert.That(struc.NumCompatible, Is.EqualTo(0), nameof(struc.NumCompatible));
657671
Assert.That(struc.ObsoleteYear, Is.EqualTo(1945), nameof(struc.ObsoleteYear));
658672
Assert.That(struc.PaintStyle, Is.EqualTo(0), nameof(struc.PaintStyle));
659-
Assert.That(struc.RoadPieces, Is.EqualTo(0), nameof(struc.RoadPieces));
673+
Assert.That(struc.RoadPieces, Is.EqualTo(RoadTraitFlags.None), nameof(struc.RoadPieces));
660674
Assert.That(struc.SellCostFactor, Is.EqualTo(-17), nameof(struc.SellCostFactor));
661675
});
662676
LoadSaveGenericTest<RoadStationObject>(objectName, assertFunc);

Tests/ViewModelTests.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text;
5-
using System.Threading.Tasks;
1+
using NUnit.Framework;
62

73
namespace Tests
84
{
9-
class ViewModelTests
5+
[TestFixture]
6+
public class ViewModelTests
107
{
8+
119
}
1210
}

0 commit comments

Comments
 (0)