diff --git a/EOLib.IO.Test/Map/MapFilePropertiesTest.cs b/EOLib.IO.Test/Map/MapFilePropertiesTest.cs index 095999b41..d7391947e 100644 --- a/EOLib.IO.Test/Map/MapFilePropertiesTest.cs +++ b/EOLib.IO.Test/Map/MapFilePropertiesTest.cs @@ -120,7 +120,7 @@ private static byte[] CreateExpectedBytes(IMapFileProperties props) ret.AddRange(props.Checksum); var fullName = Enumerable.Repeat((byte)0xFF, 24).ToArray(); - var encodedName = mapStringEncoderService.EncodeMapString(props.Name); + var encodedName = mapStringEncoderService.EncodeMapString(props.Name, props.Name.Length); Array.Copy(encodedName, 0, fullName, fullName.Length - encodedName.Length, encodedName.Length); ret.AddRange(fullName); diff --git a/EOLib.IO.Test/Map/MapFileTest.cs b/EOLib.IO.Test/Map/MapFileTest.cs index 5db9b0abd..ac7199a6d 100644 --- a/EOLib.IO.Test/Map/MapFileTest.cs +++ b/EOLib.IO.Test/Map/MapFileTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using EOLib.IO.Map; using EOLib.IO.Services; using EOLib.IO.Services.Serializers; @@ -89,7 +90,7 @@ public void MapFile_SerializeToByteArray_HasCorrectFormat() var mapData = CreateDataForMap(new MapFileProperties().WithWidth(2).WithHeight(2), TileSpec.Arena, 432); _mapFile = _serializer.DeserializeFromByteArray(mapData); - var actualData = _serializer.SerializeToByteArray(_mapFile); + var actualData = _serializer.SerializeToByteArray(_mapFile, rewriteChecksum: false); CollectionAssert.AreEqual(mapData, actualData); } @@ -107,6 +108,28 @@ public void MapFile_Width1Height1_HasExpectedGFXAndTiles() Assert.AreEqual(999, kvp.Value[1, 1]); } + [Test] + public void MapFile_StoresEmptyWarpRows() + { + _mapFile = new MapFile().WithMapID(1); + + var mapData = CreateDataForMap(new MapFileProperties().WithWidth(1).WithHeight(1), TileSpec.BankVault, 1234); + _mapFile = _serializer.DeserializeFromByteArray(mapData); + + Assert.That(_mapFile.EmptyWarpRows, Has.Count.EqualTo(1)); + } + + [Test] + public void MapFile_StoresEmptyTileRows() + { + _mapFile = new MapFile().WithMapID(1); + + var mapData = CreateDataForMap(new MapFileProperties().WithWidth(1).WithHeight(1), TileSpec.VultTypo, 4321); + _mapFile = _serializer.DeserializeFromByteArray(mapData); + + Assert.That(_mapFile.EmptyTileRows, Has.Count.EqualTo(1)); + } + private byte[] CreateDataForMap(IMapFileProperties mapFileProperties, TileSpec spec, int gfx = 1) { var ret = new List(); @@ -119,14 +142,18 @@ private byte[] CreateDataForMap(IMapFileProperties mapFileProperties, TileSpec s ret.AddRange(nes.EncodeNumber(0, 1)); //chest spawns //tiles - ret.AddRange(nes.EncodeNumber(1, 1)); //count + ret.AddRange(nes.EncodeNumber(2, 1)); //count (rows) ret.AddRange(nes.EncodeNumber(1, 1)); //y - ret.AddRange(nes.EncodeNumber(1, 1)); //count + ret.AddRange(nes.EncodeNumber(1, 1)); //count (cols) ret.AddRange(nes.EncodeNumber(1, 1)); //x ret.AddRange(nes.EncodeNumber((byte)spec, 1)); //tilespec + ret.AddRange(nes.EncodeNumber(0, 1)); //y + ret.AddRange(nes.EncodeNumber(0, 1)); //count (cols) (empty row) - //warps - ret.AddRange(nes.EncodeNumber(0, 1)); + //warps (empty row) + ret.AddRange(nes.EncodeNumber(1, 1)); //count + ret.AddRange(nes.EncodeNumber(1, 1)); //y + ret.AddRange(nes.EncodeNumber(0, 1)); //count //gfx foreach (var layer in (MapLayer[]) Enum.GetValues(typeof(MapLayer))) diff --git a/EOLib.IO.Test/Map/MapStringEncoderServiceTest.cs b/EOLib.IO.Test/Map/MapStringEncoderServiceTest.cs index 927e4c3e4..abcaf6d8c 100644 --- a/EOLib.IO.Test/Map/MapStringEncoderServiceTest.cs +++ b/EOLib.IO.Test/Map/MapStringEncoderServiceTest.cs @@ -23,7 +23,7 @@ public void EncodeThenDecode_ReturnsOriginalString() { const string expected = "Test map string to encode"; - var bytes = _service.EncodeMapString(expected); + var bytes = _service.EncodeMapString(expected, expected.Length); var actual = _service.DecodeMapString(bytes); Assert.AreEqual(expected, actual); @@ -38,7 +38,7 @@ public void EncodeString_ReturnsExpectedBytes_FromKnownString() 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 104, 41, 104, 94 }; - var actualBytes = _service.EncodeMapString(name); + var actualBytes = _service.EncodeMapString(name, name.Length); CollectionAssert.AreEqual(expectedBytes, actualBytes); } @@ -56,5 +56,39 @@ public void DecodeString_ReturnsExpectedString_FromKnownBytes() Assert.AreEqual(expected, actual); } + + [Test] + public void EncodeString_InvalidLength_Throws() + { + Assert.That(() => _service.EncodeMapString("123", 0), Throws.ArgumentException); + } + + [Test] + public void EncodeString_ExtraLength_PadsData() + { + const string TestString = "12345"; + const int LengthWithPadding = 8; + + var actual = _service.EncodeMapString(TestString, LengthWithPadding); + + Assert.That(actual, Has.Length.EqualTo(LengthWithPadding)); + + int i = 0; + for (; i < LengthWithPadding - TestString.Length; i++) + Assert.That(actual[i], Is.EqualTo((byte)0xFF)); + Assert.That(actual[i], Is.Not.EqualTo((byte)0xFF)); + } + + [Test] + public void EncodeString_ExtraLength_DecodesToExpectedValue() + { + const string TestString = "12345"; + const int LengthWithPadding = 8; + + var encoded = _service.EncodeMapString(TestString, LengthWithPadding); + var original = _service.DecodeMapString(encoded); + + Assert.That(original, Is.EqualTo(TestString)); + } } } diff --git a/EOLib.IO/Map/IMapFile.cs b/EOLib.IO/Map/IMapFile.cs index 1cdb63d7f..c5292854d 100644 --- a/EOLib.IO/Map/IMapFile.cs +++ b/EOLib.IO/Map/IMapFile.cs @@ -7,7 +7,9 @@ public interface IMapFile IMapFileProperties Properties { get; } IReadOnlyMatrix Tiles { get; } + IReadOnlyList EmptyTileRows { get; } IReadOnlyMatrix Warps { get; } + IReadOnlyList EmptyWarpRows { get; } IReadOnlyDictionary> GFX { get; } IReadOnlyDictionary> EmptyGFXRows { get; } IReadOnlyList NPCSpawns { get; } @@ -19,9 +21,9 @@ public interface IMapFile IMapFile WithMapProperties(IMapFileProperties mapFileProperties); - IMapFile WithTiles(Matrix tiles); + IMapFile WithTiles(Matrix tiles, List emptyTileRows); - IMapFile WithWarps(Matrix warps); + IMapFile WithWarps(Matrix warps, List emptyWarpRows); IMapFile WithGFX(Dictionary> gfx, Dictionary> emptyLayers); diff --git a/EOLib.IO/Map/MapFile.cs b/EOLib.IO/Map/MapFile.cs index 98b56678c..9aae4c7ea 100644 --- a/EOLib.IO/Map/MapFile.cs +++ b/EOLib.IO/Map/MapFile.cs @@ -11,7 +11,9 @@ public class MapFile : IMapFile public IMapFileProperties Properties { get; private set; } public IReadOnlyMatrix Tiles => _mutableTiles; + public IReadOnlyList EmptyTileRows => _mutableEmptyTileRows; public IReadOnlyMatrix Warps => _mutableWarps; + public IReadOnlyList EmptyWarpRows => _mutableEmptyWarpRows; public IReadOnlyDictionary> GFX => _readOnlyGFX; public IReadOnlyDictionary> EmptyGFXRows => _readOnlyEmptyGFXRows; public IReadOnlyList NPCSpawns => _mutableNPCSpawns; @@ -20,7 +22,9 @@ public class MapFile : IMapFile public IReadOnlyList Signs => _mutableSigns; private Matrix _mutableTiles; + private List _mutableEmptyTileRows; private Matrix _mutableWarps; + private List _mutableEmptyWarpRows; private Dictionary> _mutableGFX; private IReadOnlyDictionary> _readOnlyGFX; private Dictionary> _mutableEmptyGFXRows; @@ -33,7 +37,9 @@ public class MapFile : IMapFile public MapFile() : this(new MapFileProperties(), Matrix.Empty, + new List(), Matrix.Empty, + new List(), new Dictionary>(), new Dictionary>(), new List(), @@ -48,7 +54,9 @@ public MapFile() private MapFile(IMapFileProperties properties, Matrix tiles, + List emptyTileSpecRows, Matrix warps, + List emptyWarpRows, Dictionary> gfx, Dictionary> emptyGFXRows, List npcSpawns, @@ -58,7 +66,9 @@ public MapFile() { Properties = properties; _mutableTiles = tiles; + _mutableEmptyTileRows = emptyTileSpecRows; _mutableWarps = warps; + _mutableEmptyWarpRows = emptyWarpRows; _mutableGFX = gfx; _mutableEmptyGFXRows = emptyGFXRows; SetReadOnlyGFX(); @@ -81,17 +91,19 @@ public IMapFile WithMapProperties(IMapFileProperties mapFileProperties) return newMap; } - public IMapFile WithTiles(Matrix tiles) + public IMapFile WithTiles(Matrix tiles, List emptyTileRows) { var newMap = MakeCopy(this); newMap._mutableTiles = tiles; + newMap._mutableEmptyTileRows = emptyTileRows; return newMap; } - public IMapFile WithWarps(Matrix warps) + public IMapFile WithWarps(Matrix warps, List emptyWarpRows) { var newMap = MakeCopy(this); newMap._mutableWarps = warps; + newMap._mutableEmptyWarpRows = emptyWarpRows; return newMap; } @@ -182,7 +194,9 @@ private static MapFile MakeCopy(MapFile source) return new MapFile( source.Properties, source._mutableTiles, + source._mutableEmptyTileRows, source._mutableWarps, + source._mutableEmptyWarpRows, source._mutableGFX, source._mutableEmptyGFXRows, source._mutableNPCSpawns, diff --git a/EOLib.IO/Map/SignMapEntity.cs b/EOLib.IO/Map/SignMapEntity.cs index 6936d2b32..6e83a82df 100644 --- a/EOLib.IO/Map/SignMapEntity.cs +++ b/EOLib.IO/Map/SignMapEntity.cs @@ -14,16 +14,19 @@ public class SignMapEntity : IMapEntity public string Message { get; private set; } + public int RawLength { get; private set; } + public SignMapEntity() - : this(-1, -1, string.Empty, String.Empty) + : this(-1, -1, string.Empty, String.Empty, 0) { } - private SignMapEntity(int x, int y, string title, string message) + private SignMapEntity(int x, int y, string title, string message, int rawLength) { X = x; Y = y; Title = title; Message = message; + RawLength = rawLength; } public SignMapEntity WithX(int x) @@ -54,9 +57,16 @@ public SignMapEntity WithMessage(string message) return newEntity; } + public SignMapEntity WithRawLength(int length) + { + var newEntity = MakeCopy(this); + newEntity.RawLength = length; + return newEntity; + } + private static SignMapEntity MakeCopy(SignMapEntity src) { - return new SignMapEntity(src.X, src.Y, src.Title, src.Message); + return new SignMapEntity(src.X, src.Y, src.Title, src.Message, src.RawLength); } } } diff --git a/EOLib.IO/Services/IMapStringEncoderService.cs b/EOLib.IO/Services/IMapStringEncoderService.cs index 85910f97d..15749a993 100644 --- a/EOLib.IO/Services/IMapStringEncoderService.cs +++ b/EOLib.IO/Services/IMapStringEncoderService.cs @@ -4,6 +4,6 @@ public interface IMapStringEncoderService { string DecodeMapString(byte[] chars); - byte[] EncodeMapString(string s); + byte[] EncodeMapString(string s, int length); } } diff --git a/EOLib.IO/Services/MapStringEncoderService.cs b/EOLib.IO/Services/MapStringEncoderService.cs index 98cbb1a4b..2f2c19c0a 100644 --- a/EOLib.IO/Services/MapStringEncoderService.cs +++ b/EOLib.IO/Services/MapStringEncoderService.cs @@ -1,5 +1,6 @@ using AutomaticTypeMapper; using System; +using System.Linq; using System.Text; namespace EOLib.IO.Services @@ -9,16 +10,18 @@ public class MapStringEncoderService : IMapStringEncoderService { public string DecodeMapString(byte[] chars) { - Array.Reverse(chars); + var copy = new byte[chars.Length]; + Array.Copy(chars, copy, chars.Length); + Array.Reverse(copy); - bool flippy = chars.Length % 2 == 1; + bool flippy = copy.Length % 2 == 1; - for (int i = 0; i < chars.Length; ++i) + for (int i = 0; i < copy.Length; ++i) { - byte c = chars[i]; + byte c = copy[i]; if (c == 0xFF) { - Array.Resize(ref chars, i); + Array.Resize(ref copy, i); break; } @@ -35,17 +38,20 @@ public string DecodeMapString(byte[] chars) c = (byte)(0x9F - c); } - chars[i] = c; + copy[i] = c; flippy = !flippy; } - return Encoding.ASCII.GetString(chars); + return Encoding.ASCII.GetString(copy); } - public byte[] EncodeMapString(string s) + public byte[] EncodeMapString(string s, int length) { + if (length < s.Length) + throw new ArgumentException("Length should be greater than or equal to string length", nameof(length)); + byte[] chars = Encoding.ASCII.GetBytes(s); - bool flippy = chars.Length % 2 == 1; + bool flippy = length % 2 == 1; int i; for (i = 0; i < chars.Length; ++i) { @@ -66,6 +72,14 @@ public byte[] EncodeMapString(string s) flippy = !flippy; } Array.Reverse(chars); + + if (length > s.Length) + { + var tmp = Enumerable.Repeat((byte)0xFF, length).ToArray(); + chars.CopyTo(tmp, length - s.Length); + chars = tmp; + } + return chars; } } diff --git a/EOLib.IO/Services/Serializers/MapFileSerializer.cs b/EOLib.IO/Services/Serializers/MapFileSerializer.cs index 98bcdd0ac..ac333f553 100644 --- a/EOLib.IO/Services/Serializers/MapFileSerializer.cs +++ b/EOLib.IO/Services/Serializers/MapFileSerializer.cs @@ -83,8 +83,8 @@ public IMapFile DeserializeFromByteArray(byte[] data) var npcSpawns = ReadNPCSpawns(ms); var unknowns = ReadUnknowns(ms); var mapChests = ReadMapChests(ms); - var tileSpecs = ReadTileSpecs(ms, properties); - var warpTiles = ReadWarpTiles(ms, properties); + var (tileSpecs, emptyTileSpecRows) = ReadTileSpecs(ms, properties); + var (warpTiles, emptyWarpRows) = ReadWarpTiles(ms, properties); var (gfxLayers, emptyLayers) = ReadGFXLayers(ms, properties); var mapSigns = new List(); @@ -99,8 +99,8 @@ public IMapFile DeserializeFromByteArray(byte[] data) .WithNPCSpawns(npcSpawns) .WithUnknowns(unknowns) .WithChests(mapChests) - .WithTiles(tileSpecs) - .WithWarps(warpTiles) + .WithTiles(tileSpecs, emptyTileSpecRows) + .WithWarps(warpTiles, emptyWarpRows) .WithGFX(gfxLayers, emptyLayers) .WithSigns(mapSigns); } @@ -156,9 +156,10 @@ private List ReadMapChests(MemoryStream ms) return chestSpawns; } - private Matrix ReadTileSpecs(MemoryStream ms, IMapFileProperties properties) + private (Matrix, List) ReadTileSpecs(MemoryStream ms, IMapFileProperties properties) { var tiles = new Matrix(properties.Height + 1, properties.Width + 1, DEFAULT_TILE); + var emptyTileRows = new List(); var numberOfTileRows = _numberEncoderService.DecodeNumber((byte)ms.ReadByte()); for (int i = 0; i < numberOfTileRows; ++i) @@ -166,6 +167,9 @@ private Matrix ReadTileSpecs(MemoryStream ms, IMapFileProperties prope var y = _numberEncoderService.DecodeNumber((byte)ms.ReadByte()); var numberOfTileColumns = _numberEncoderService.DecodeNumber((byte)ms.ReadByte()); + if (numberOfTileColumns == 0) + emptyTileRows.Add(y); + for (int j = 0; j < numberOfTileColumns; ++j) { var x = _numberEncoderService.DecodeNumber((byte)ms.ReadByte()); @@ -176,12 +180,13 @@ private Matrix ReadTileSpecs(MemoryStream ms, IMapFileProperties prope } } - return tiles; + return (tiles, emptyTileRows); } - private Matrix ReadWarpTiles(MemoryStream ms, IMapFileProperties properties) + private (Matrix, List) ReadWarpTiles(MemoryStream ms, IMapFileProperties properties) { var warps = new Matrix(properties.Height + 1, properties.Width + 1, DEFAULT_WARP); + var emptyWarpRows = new List(); var numberOfWarpRows = _numberEncoderService.DecodeNumber((byte)ms.ReadByte()); for (int i = 0; i < numberOfWarpRows; ++i) @@ -189,6 +194,9 @@ private Matrix ReadWarpTiles(MemoryStream ms, IMapFileProperties var y = _numberEncoderService.DecodeNumber((byte)ms.ReadByte()); var numberOfWarpColumns = _numberEncoderService.DecodeNumber((byte)ms.ReadByte()); + if (numberOfWarpColumns == 0) + emptyWarpRows.Add(y); + for (int j = 0; j < numberOfWarpColumns; ++j) { var rawWarpData = new byte[WarpMapEntity.DATA_SIZE]; @@ -201,7 +209,7 @@ private Matrix ReadWarpTiles(MemoryStream ms, IMapFileProperties } } - return warps; + return (warps, emptyWarpRows); } private (Dictionary>, Dictionary>) ReadGFXLayers(MemoryStream ms, IMapFileProperties properties) @@ -315,8 +323,9 @@ private List WriteTileSpecs(IMapFile mapFile) var ret = new List(); var tileRows = mapFile.Tiles - .Select((row, i) => new { EntityItems = row, Y = i }) - .Where(rowList => rowList.EntityItems.Any(item => item != DEFAULT_TILE)) + .Select((row, i) => (row, i)) + .Where(rowList => rowList.row.Any(item => item != DEFAULT_TILE)) + .Concat<(IList EntityItems, int Y)>(mapFile.EmptyTileRows.Select(rowNdx => ((IList)new List(), rowNdx))) .ToList(); ret.AddRange(_numberEncoderService.EncodeNumber(tileRows.Count, 1)); @@ -335,6 +344,7 @@ private List WriteTileSpecs(IMapFile mapFile) ret.AddRange(_numberEncoderService.EncodeNumber((byte)item.Value, 1)); } } + return ret; } @@ -343,8 +353,9 @@ private List WriteWarpTiles(IMapFile mapFile) var ret = new List(); var warpRows = mapFile.Warps - .Select((row, i) => new { EntityItems = row, Y = i }) - .Where(rowList => rowList.EntityItems.Any(item => item != DEFAULT_WARP)) + .Select((row, i) => (row, i)) + .Where(rowList => rowList.row.Any(item => item != DEFAULT_WARP)) + .Concat<(IList EntityItems, int Y)>(mapFile.EmptyWarpRows.Select(rowNdx => ((IList)new List(), rowNdx))) .ToList(); ret.AddRange(_numberEncoderService.EncodeNumber(warpRows.Count, 1)); diff --git a/EOLib.IO/Services/Serializers/MapPropertiesSerializer.cs b/EOLib.IO/Services/Serializers/MapPropertiesSerializer.cs index 6045d8ebd..4bc14cc7c 100644 --- a/EOLib.IO/Services/Serializers/MapPropertiesSerializer.cs +++ b/EOLib.IO/Services/Serializers/MapPropertiesSerializer.cs @@ -90,7 +90,7 @@ private byte[] EncodeMapName(IMapFileProperties mapEntity) var padding = Enumerable.Repeat((byte)0, 24 - mapEntity.Name.Length).ToArray(); var nameToEncode = $"{mapEntity.Name}{Encoding.ASCII.GetString(padding)}"; - var encodedName = _mapStringEncoderService.EncodeMapString(nameToEncode); + var encodedName = _mapStringEncoderService.EncodeMapString(nameToEncode, nameToEncode.Length); var formattedName = encodedName.Select(x => x == 0 ? (byte)255 : x).ToArray(); return formattedName; } diff --git a/EOLib.IO/Services/Serializers/SignMapEntitySerializer.cs b/EOLib.IO/Services/Serializers/SignMapEntitySerializer.cs index bedcc954f..13b9815a5 100644 --- a/EOLib.IO/Services/Serializers/SignMapEntitySerializer.cs +++ b/EOLib.IO/Services/Serializers/SignMapEntitySerializer.cs @@ -27,7 +27,7 @@ public byte[] SerializeToByteArray(SignMapEntity mapEntity) retBytes.AddRange(numberEncoderService.EncodeNumber(mapEntity.X, 1)); retBytes.AddRange(numberEncoderService.EncodeNumber(mapEntity.Y, 1)); - var fileMsg = mapStringEncoderService.EncodeMapString(mapEntity.Title + mapEntity.Message); + var fileMsg = mapStringEncoderService.EncodeMapString(mapEntity.Title + mapEntity.Message, mapEntity.RawLength); retBytes.AddRange(numberEncoderService.EncodeNumber(fileMsg.Length + 1, 2)); retBytes.AddRange(fileMsg); @@ -54,7 +54,8 @@ public SignMapEntity DeserializeFromByteArray(byte[] data) var titleAndMessage = mapStringEncoderService.DecodeMapString(rawTitleAndMessage); sign = sign.WithTitle(titleAndMessage.Substring(0, titleLength)) - .WithMessage(titleAndMessage.Substring(titleLength)); + .WithMessage(titleAndMessage.Substring(titleLength)) + .WithRawLength(titleAndMessageLength - 1); return sign; }