From 75e7f41844902a3a43e2641fa073f24d0e1e91d1 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Wed, 16 Mar 2022 13:36:22 -0700 Subject: [PATCH] Make pub files/records immutable. Separate serialization of pub files/records into separate serializer objects. Use generic approach to data storage for pub records. EOLib.IO(.Test) builds and tests pass. --- EOLib.IO.Test/EIFRecordExtensionsTest.cs | 131 ++------ EOLib.IO.Test/Pub/BasePubFileTest.cs | 102 ------ EOLib.IO.Test/Pub/DummyFile.cs | 31 -- EOLib.IO.Test/Pub/DummyRecord.cs | 35 -- EOLib.IO.Test/Pub/ECFFileTest.cs | 110 ++---- EOLib.IO.Test/Pub/ECFRecordTest.cs | 185 ----------- EOLib.IO.Test/Pub/EIFFileTest.cs | 112 +++---- EOLib.IO.Test/Pub/EIFRecordTest.cs | 313 ------------------ EOLib.IO.Test/Pub/ENFFileTest.cs | 110 ++---- EOLib.IO.Test/Pub/ENFRecordTest.cs | 202 ----------- EOLib.IO.Test/Pub/ESFFileTest.cs | 110 ++---- EOLib.IO.Test/Pub/ESFRecordTest.cs | 215 ------------ EOLib.IO/Caster.cs | 37 +++ EOLib.IO/Extensions/EIFFileExtensions.cs | 8 +- EOLib.IO/Pub/BasePubFile.cs | 105 +++--- EOLib.IO/Pub/ECFFile.cs | 42 +-- EOLib.IO/Pub/ECFRecord.cs | 88 ++--- EOLib.IO/Pub/EIFFile.cs | 42 +-- EOLib.IO/Pub/EIFRecord.cs | 282 ++++------------ EOLib.IO/Pub/ENFFile.cs | 42 +-- EOLib.IO/Pub/ENFRecord.cs | 149 ++------- EOLib.IO/Pub/ESFFile.cs | 50 +-- EOLib.IO/Pub/ESFRecord.cs | 198 +++-------- EOLib.IO/Pub/IPubFile.cs | 15 +- EOLib.IO/Pub/IPubRecord.cs | 34 +- EOLib.IO/Pub/PubRecord.cs | 99 ++++++ EOLib.IO/Pub/PubRecordProperty.cs | 145 ++++++-- EOLib.IO/Pub/RecordData.cs | 46 +++ EOLib.IO/Pub/RecordDataAttribute.cs | 18 + EOLib.IO/Services/ClassFileLoadService.cs | 13 +- EOLib.IO/Services/IPubFileSaveService.cs | 3 +- EOLib.IO/Services/IPubLoadService.cs | 2 +- EOLib.IO/Services/ItemFileLoadService.cs | 13 +- EOLib.IO/Services/NPCFileLoadService.cs | 13 +- EOLib.IO/Services/PubFileSaveService.cs | 12 +- .../Serializers/IPubFileDeserializer.cs | 17 + .../Serializers/IPubRecordSerializer.cs | 12 + .../Services/Serializers/PubFileSerializer.cs | 99 ++++++ .../Serializers/PubRecordSerializer.cs | 78 +++++ EOLib.IO/Services/SpellFileLoadService.cs | 13 +- 40 files changed, 1034 insertions(+), 2297 deletions(-) delete mode 100644 EOLib.IO.Test/Pub/BasePubFileTest.cs delete mode 100644 EOLib.IO.Test/Pub/DummyFile.cs delete mode 100644 EOLib.IO.Test/Pub/DummyRecord.cs delete mode 100644 EOLib.IO.Test/Pub/ECFRecordTest.cs delete mode 100644 EOLib.IO.Test/Pub/EIFRecordTest.cs delete mode 100644 EOLib.IO.Test/Pub/ENFRecordTest.cs delete mode 100644 EOLib.IO.Test/Pub/ESFRecordTest.cs create mode 100644 EOLib.IO/Caster.cs create mode 100644 EOLib.IO/Pub/PubRecord.cs create mode 100644 EOLib.IO/Pub/RecordData.cs create mode 100644 EOLib.IO/Pub/RecordDataAttribute.cs create mode 100644 EOLib.IO/Services/Serializers/IPubFileDeserializer.cs create mode 100644 EOLib.IO/Services/Serializers/IPubRecordSerializer.cs create mode 100644 EOLib.IO/Services/Serializers/PubFileSerializer.cs create mode 100644 EOLib.IO/Services/Serializers/PubRecordSerializer.cs diff --git a/EOLib.IO.Test/EIFRecordExtensionsTest.cs b/EOLib.IO.Test/EIFRecordExtensionsTest.cs index f8990f935..bb14d36a8 100644 --- a/EOLib.IO.Test/EIFRecordExtensionsTest.cs +++ b/EOLib.IO.Test/EIFRecordExtensionsTest.cs @@ -8,100 +8,41 @@ namespace EOLib.IO.Test [TestFixture, ExcludeFromCodeCoverage] public class EIFRecordExtensionsTest { - [Test] - public void GetEquipLocation_Accessory_ReturnsAccessory() - { - Assert.AreEqual(EquipLocation.Accessory, new EIFRecord {Type = ItemType.Accessory}.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Armlet_ReturnsArmlet1() - { - Assert.AreEqual(EquipLocation.Armlet1, new EIFRecord { Type = ItemType.Armlet }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Armor_ReturnsArmor() - { - Assert.AreEqual(EquipLocation.Armor, new EIFRecord { Type = ItemType.Armor }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Belt_ReturnsBelt() - { - Assert.AreEqual(EquipLocation.Belt, new EIFRecord { Type = ItemType.Belt }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Boots_ReturnsBoots() - { - Assert.AreEqual(EquipLocation.Boots, new EIFRecord { Type = ItemType.Boots }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Bracer_ReturnsBracer() - { - Assert.AreEqual(EquipLocation.Bracer1, new EIFRecord { Type = ItemType.Bracer }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Gloves_ReturnsGloves() - { - Assert.AreEqual(EquipLocation.Gloves, new EIFRecord { Type = ItemType.Gloves }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Hat_ReturnsHat() - { - Assert.AreEqual(EquipLocation.Hat, new EIFRecord { Type = ItemType.Hat }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Necklace_ReturnsNecklace() - { - Assert.AreEqual(EquipLocation.Necklace, new EIFRecord { Type = ItemType.Necklace }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Ring_ReturnsRing1() - { - Assert.AreEqual(EquipLocation.Ring1, new EIFRecord { Type = ItemType.Ring }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Shield_ReturnsShield() - { - Assert.AreEqual(EquipLocation.Shield, new EIFRecord { Type = ItemType.Shield }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Weapon_ReturnsWeapon() - { - Assert.AreEqual(EquipLocation.Weapon, new EIFRecord { Type = ItemType.Weapon }.GetEquipLocation()); - } - - [Test] - public void GetEquipLocation_Unsupported_ReturnsPaperdollMax() - { - var unsupported = new[] - { - ItemType.Beer, - ItemType.CureCurse, - ItemType.EXPReward, - ItemType.EffectPotion, - ItemType.HairDye, - ItemType.Heal, - ItemType.Key, - ItemType.Money, - ItemType.SkillReward, - ItemType.StatReward, - ItemType.Static, - ItemType.Teleport, - ItemType.UnknownType1 - }; - - foreach (var type in unsupported) - Assert.AreEqual(EquipLocation.PAPERDOLL_MAX, new EIFRecord {Type = type}.GetEquipLocation()); - } + [TestCase(EquipLocation.Accessory, ItemType.Accessory)] + [TestCase(EquipLocation.Armlet1, ItemType.Armlet)] + [TestCase(EquipLocation.Armor, ItemType.Armor)] + [TestCase(EquipLocation.Belt, ItemType.Belt)] + [TestCase(EquipLocation.Boots, ItemType.Boots)] + [TestCase(EquipLocation.Bracer1, ItemType.Bracer)] + [TestCase(EquipLocation.Gloves, ItemType.Gloves)] + [TestCase(EquipLocation.Hat, ItemType.Hat)] + [TestCase(EquipLocation.Necklace, ItemType.Necklace)] + [TestCase(EquipLocation.Ring1, ItemType.Ring)] + [TestCase(EquipLocation.Shield, ItemType.Shield)] + [TestCase(EquipLocation.Weapon, ItemType.Weapon)] + public void GetEquipLocation_Matches_ItemType(EquipLocation equipLocation, ItemType itemType) + { + Assert.That(WithItemType(itemType).GetEquipLocation(), Is.EqualTo(equipLocation)); + } + + [TestCase(ItemType.Beer)] + [TestCase(ItemType.CureCurse)] + [TestCase(ItemType.EXPReward)] + [TestCase(ItemType.EffectPotion)] + [TestCase(ItemType.HairDye)] + [TestCase(ItemType.Heal)] + [TestCase(ItemType.Key)] + [TestCase(ItemType.Money)] + [TestCase(ItemType.SkillReward)] + [TestCase(ItemType.StatReward)] + [TestCase(ItemType.Static)] + [TestCase(ItemType.Teleport)] + [TestCase(ItemType.UnknownType1)] + public void GetEquipLocation_Unsupported_ReturnsPaperdollMax(ItemType type) + { + Assert.That(WithItemType(type).GetEquipLocation(), Is.EqualTo(EquipLocation.PAPERDOLL_MAX)); + } + + private static EIFRecord WithItemType(ItemType type) => (EIFRecord)new EIFRecord().WithProperty(PubRecordProperty.ItemType, (int)type); } } diff --git a/EOLib.IO.Test/Pub/BasePubFileTest.cs b/EOLib.IO.Test/Pub/BasePubFileTest.cs deleted file mode 100644 index 4b2647ece..000000000 --- a/EOLib.IO.Test/Pub/BasePubFileTest.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using EOLib.IO.Pub; -using EOLib.IO.Services; -using NUnit.Framework; - -namespace EOLib.IO.Test.Pub -{ - [TestFixture, ExcludeFromCodeCoverage] - public class BasePubFileTest - { - //This covers the BasePubFile abstract class. - private BasePubFile _baseFile; - - [SetUp] - public void SetUp() - { - _baseFile = new DummyFile(); - } - - [Test] - public void PubFile_HasExpectedChecksumAndLength() - { - Assert.AreEqual(0, _baseFile.CheckSum); - Assert.AreEqual(0, _baseFile.Length); - } - - [Test] - public void PubFile_WithOneItemRecord_HasExpectedLength() - { - var bytes = MakeDummyFile(new DummyRecord { ID = 1, Name = "TestItem" }); - - _baseFile.DeserializeFromByteArray(bytes, new NumberEncoderService()); - - Assert.AreEqual(1, _baseFile.Length); - } - - [Test] - public void PubFile_Indexing_ReturnsNullWhenLessThan1() - { - var bytes = MakeDummyFile(new DummyRecord { ID = 1, Name = "TestItem" }, - new DummyRecord { ID = 2, Name = "Test2" }, - new DummyRecord { ID = 3, Name = "Test3" }, - new DummyRecord { ID = 4, Name = "Test4" }); - - _baseFile.DeserializeFromByteArray(bytes, new NumberEncoderService()); - - Assert.AreEqual(4, _baseFile.Length); - Assert.IsNull(_baseFile[0]); - } - - [Test] - public void PubFile_Indexing_ReturnsNullWhenGreaterThanCount() - { - var bytes = MakeDummyFile(new DummyRecord { ID = 1, Name = "TestItem" }, - new DummyRecord { ID = 2, Name = "Test2" }); - - _baseFile.DeserializeFromByteArray(bytes, new NumberEncoderService()); - - Assert.AreEqual(2, _baseFile.Length); - Assert.IsNull(_baseFile[3]); - } - - [Test] - public void PubFile_Indexing_ReturnsExpectedItemWhenRequestedByID() - { - var records = new[] - { - new DummyRecord {ID = 1, Name = "TestItem"}, - new DummyRecord {ID = 2, Name = "Test2"}, - new DummyRecord {ID = 3, Name = "Test3"}, - new DummyRecord {ID = 4, Name = "Test4"}, - new DummyRecord {ID = 5, Name = "Test5"}, - new DummyRecord {ID = 6, Name = "Test6"}, - new DummyRecord {ID = 7, Name = "Test7"}, - new DummyRecord {ID = 8, Name = "Test8"} - }; - - var bytes = MakeDummyFile(records); - - _baseFile.DeserializeFromByteArray(bytes, new NumberEncoderService()); - - Assert.AreEqual(records.Length, _baseFile.Length); - - for (int i = 0; i < records.Length; ++i) - Assert.AreEqual(records[i].Name, _baseFile[records[i].ID].Name, "Failed at index {0}", i); - } - - private byte[] MakeDummyFile(params DummyRecord[] records) - { - var numberEncoderService = new NumberEncoderService(); - - var bytes = new List(); - - bytes.Add((byte)records.Length); - foreach (var record in records) - bytes.AddRange(record.SerializeToByteArray(numberEncoderService)); - - return bytes.ToArray(); - } - } -} diff --git a/EOLib.IO.Test/Pub/DummyFile.cs b/EOLib.IO.Test/Pub/DummyFile.cs deleted file mode 100644 index e6705093b..000000000 --- a/EOLib.IO.Test/Pub/DummyFile.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Text; -using EOLib.IO.Pub; -using EOLib.IO.Services; - -namespace EOLib.IO.Test.Pub -{ - [ExcludeFromCodeCoverage] - internal class DummyFile : BasePubFile - { - public override string FileType => " "; - - public override void DeserializeFromByteArray(byte[] bytes, INumberEncoderService numberEncoderService) - { - using (var ms = new MemoryStream(bytes)) - { - var num = ms.ReadByte(); - - for (int i = 0; i < num; ++i) - { - var nameLen = ms.ReadByte(); - var rawName = new byte[nameLen]; - ms.Read(rawName, 0, nameLen); - - _data.Add(new DummyRecord {ID = i + 1, Name = Encoding.ASCII.GetString(rawName)}); - } - } - } - } -} diff --git a/EOLib.IO.Test/Pub/DummyRecord.cs b/EOLib.IO.Test/Pub/DummyRecord.cs deleted file mode 100644 index f33c33c8b..000000000 --- a/EOLib.IO.Test/Pub/DummyRecord.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using EOLib.IO.Pub; -using EOLib.IO.Services; - -namespace EOLib.IO.Test.Pub -{ - [ExcludeFromCodeCoverage] - internal class DummyRecord : IPubRecord - { - public int RecordSize => 0; - - public int ID { get; set; } - - public string Name { get; set; } - - public TValue Get(PubRecordProperty type) - { - return default(TValue); - } - - public byte[] SerializeToByteArray(INumberEncoderService numberEncoderService) - { - var bytes = new byte[Name.Length + 1]; - bytes[0] = (byte) Name.Length; - Array.Copy(Encoding.ASCII.GetBytes(Name), 0, bytes, 1, Name.Length); - return bytes; - } - - public void DeserializeFromByteArray(byte[] recordBytes, INumberEncoderService numberEncoderService) - { - } - } -} diff --git a/EOLib.IO.Test/Pub/ECFFileTest.cs b/EOLib.IO.Test/Pub/ECFFileTest.cs index 5d84b757f..e13e75ee7 100644 --- a/EOLib.IO.Test/Pub/ECFFileTest.cs +++ b/EOLib.IO.Test/Pub/ECFFileTest.cs @@ -1,100 +1,70 @@ -using System.Collections.Generic; +using EOLib.IO.Pub; +using EOLib.IO.Services; +using EOLib.IO.Services.Serializers; +using NUnit.Framework; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Text; -using EOLib.IO.Pub; -using EOLib.IO.Services; -using NUnit.Framework; namespace EOLib.IO.Test.Pub { [TestFixture, ExcludeFromCodeCoverage] public class ECFFileTest { - private IPubFile _classFile; - - [SetUp] - public void SetUp() - { - _classFile = new ECFFile(); - } - [Test] public void HasCorrectFileType() { - Assert.AreEqual("ECF", _classFile.FileType); + Assert.That(new ECFFile().FileType, Is.EqualTo("ECF")); } [Test] public void SerializeToByteArray_ReturnsExpectedBytes() { var expectedBytes = MakeECFFile(55565554, - new ECFRecord { ID = 1, Name = "TestFixture" }, - new ECFRecord { ID = 2, Name = "Test2" }, - new ECFRecord { ID = 3, Name = "Test3" }, - new ECFRecord { ID = 4, Name = "Test4" }, - new ECFRecord { ID = 5, Name = "Test5" }, - new ECFRecord { ID = 6, Name = "Test6" }, - new ECFRecord { ID = 7, Name = "Test7" }, - new ECFRecord { ID = 8, Name = "Test8" }, - new ECFRecord { ID = 9, Name = "eof" }); + new ECFRecord().WithID(1).WithNames(new List { "TestFixture" }), + new ECFRecord().WithID(2).WithNames(new List { "Test2" }), + new ECFRecord().WithID(3).WithNames(new List { "Test3" }), + new ECFRecord().WithID(4).WithNames(new List { "Test4" }), + new ECFRecord().WithID(5).WithNames(new List { "Test5" }), + new ECFRecord().WithID(6).WithNames(new List { "Test6" }), + new ECFRecord().WithID(7).WithNames(new List { "Test7" }), + new ECFRecord().WithID(8).WithNames(new List { "Test8" }), + new ECFRecord().WithID(9).WithNames(new List { "eof" })); - _classFile.DeserializeFromByteArray(expectedBytes, new NumberEncoderService()); + var serializer = CreateFileSerializer(); + var file = serializer.DeserializeFromByteArray(expectedBytes, () => new ECFFile()); - var actualBytes = _classFile.SerializeToByteArray(new NumberEncoderService(), rewriteChecksum: false); + var actualBytes = serializer.SerializeToByteArray(file, rewriteChecksum: false); CollectionAssert.AreEqual(expectedBytes, actualBytes); } - [Test] - public void HeaderFormat_IsCorrect() - { - var nes = new NumberEncoderService(); - - var actualBytes = _classFile.SerializeToByteArray(nes, rewriteChecksum: false); - - CollectionAssert.AreEqual(Encoding.ASCII.GetBytes(_classFile.FileType), actualBytes.Take(3).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(_classFile.CheckSum, 4), actualBytes.Skip(3).Take(4).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(_classFile.Length, 2), actualBytes.Skip(7).Take(2).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(1, 1), actualBytes.Skip(9).Take(1).ToArray()); - } - - [Test] - public void LengthMismatch_ThrowsIOException() - { - var bytes = MakeECFFileWithWrongLength(12345678, 5, - new ECFRecord { ID = 1, Name = "Class1" }, - new ECFRecord { ID = 2, Name = "Class2" }, - new ECFRecord { ID = 3, Name = "Class3" }); - - Assert.Throws(() => _classFile.DeserializeFromByteArray(bytes, new NumberEncoderService())); - } - [Test] public void DeserializeFromByteArray_HasExpectedIDAndNames() { var records = new[] { - new ECFRecord {ID = 1, Name = "TestFixture"}, - new ECFRecord {ID = 2, Name = "Test2"}, - new ECFRecord {ID = 3, Name = "Test3"}, - new ECFRecord {ID = 4, Name = "Test4"}, - new ECFRecord {ID = 5, Name = "Test5"}, - new ECFRecord {ID = 6, Name = "Test6"}, - new ECFRecord {ID = 7, Name = "Test7"}, - new ECFRecord {ID = 8, Name = "Test8"}, - new ECFRecord {ID = 9, Name = "eof"} + new ECFRecord().WithID(1).WithNames(new List { "TestFixture" }), + new ECFRecord().WithID(2).WithNames(new List { "Test2" }), + new ECFRecord().WithID(3).WithNames(new List { "Test3" }), + new ECFRecord().WithID(4).WithNames(new List { "Test4" }), + new ECFRecord().WithID(5).WithNames(new List { "Test5" }), + new ECFRecord().WithID(6).WithNames(new List { "Test6" }), + new ECFRecord().WithID(7).WithNames(new List { "Test7" }), + new ECFRecord().WithID(8).WithNames(new List { "Test8" }), + new ECFRecord().WithID(9).WithNames(new List { "eof" }) }; var bytes = MakeECFFile(55565554, records); - _classFile.DeserializeFromByteArray(bytes, new NumberEncoderService()); + var serializer = CreateFileSerializer(); + var file = serializer.DeserializeFromByteArray(bytes, () => new ECFFile()); CollectionAssert.AreEqual(records.Select(x => new { x.ID, x.Name }).ToList(), - _classFile.Data.Select(x => new { x.ID, x.Name }).ToList()); + file.Select(x => new { x.ID, x.Name }).ToList()); } - private byte[] MakeECFFile(int checksum, params ECFRecord[] records) + private byte[] MakeECFFile(int checksum, params IPubRecord[] records) { var numberEncoderService = new NumberEncoderService(); @@ -103,25 +73,17 @@ private byte[] MakeECFFile(int checksum, params ECFRecord[] records) bytes.AddRange(numberEncoderService.EncodeNumber(checksum, 4)); bytes.AddRange(numberEncoderService.EncodeNumber(records.Length, 2)); bytes.Add(numberEncoderService.EncodeNumber(1, 1)[0]); + + var recordSerializer = new PubRecordSerializer(numberEncoderService); foreach (var record in records) - bytes.AddRange(record.SerializeToByteArray(numberEncoderService)); + bytes.AddRange(recordSerializer.SerializeToByteArray(record)); return bytes.ToArray(); } - private byte[] MakeECFFileWithWrongLength(int checksum, int length, params ECFRecord[] records) + private static IPubFileSerializer CreateFileSerializer() { - var numberEncoderService = new NumberEncoderService(); - - var bytes = new List(); - bytes.AddRange(Encoding.ASCII.GetBytes("ECF")); - bytes.AddRange(numberEncoderService.EncodeNumber(checksum, 4)); - bytes.AddRange(numberEncoderService.EncodeNumber(length, 2)); - bytes.Add(numberEncoderService.EncodeNumber(1, 1)[0]); - foreach (var record in records) - bytes.AddRange(record.SerializeToByteArray(numberEncoderService)); - - return bytes.ToArray(); + return new PubFileSerializer(new NumberEncoderService(), new PubRecordSerializer(new NumberEncoderService())); } } } diff --git a/EOLib.IO.Test/Pub/ECFRecordTest.cs b/EOLib.IO.Test/Pub/ECFRecordTest.cs deleted file mode 100644 index 3cf23216a..000000000 --- a/EOLib.IO.Test/Pub/ECFRecordTest.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Text; -using EOLib.IO.Pub; -using EOLib.IO.Services; -using NUnit.Framework; - -namespace EOLib.IO.Test.Pub -{ - [TestFixture, ExcludeFromCodeCoverage] - public class ECFRecordTest - { - [Test] - public void ECFRecord_GetGlobalPropertyID_GetsRecordID() - { - const int expected = 44; - var rec = new ECFRecord { ID = expected }; - - var actual = rec.Get(PubRecordProperty.GlobalID); - - Assert.AreEqual(expected, actual); - } - - [Test] - public void ECFRecord_GetGlobalPropertyName_GetsRecordName() - { - const string expected = "some name"; - var rec = new ECFRecord { Name = expected }; - - var actual = rec.Get(PubRecordProperty.GlobalName); - - Assert.AreEqual(expected, actual); - } - - [Test] - public void ECFRecord_GetClassPropertiesComprehensive_NoException() - { - var classProperties = Enum.GetNames(typeof(PubRecordProperty)) - .Where(x => x.StartsWith("Class")) - .Select(x => (PubRecordProperty)Enum.Parse(typeof(PubRecordProperty), x)) - .ToArray(); - - Assert.AreNotEqual(0, classProperties.Length); - - var record = new ECFRecord(); - - foreach (var property in classProperties) - { - var dummy = record.Get(property); - Assert.IsNotNull(dummy); - } - } - - [Test] - public void ECFRecord_GetItemProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.ItemSubType; - - var record = new ECFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void ECFRecord_GetSpellProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.SpellAccuracy; - - var record = new ECFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void ECFRecord_GetNPCProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.NPCAccuracy; - - var record = new ECFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void ECFRecord_InvalidPropertyReturnType_ThrowsInvalidCastException() - { - var rec = new ECFRecord { Name = "" }; - - Assert.Throws(() => rec.Get(PubRecordProperty.GlobalName)); - } - - [Test] - public void ECFRecord_SerializeToByteArray_WritesExpectedFormat() - { - var numberEncoderService = new NumberEncoderService(); - var record = CreateRecordWithSomeGoodTestData(); - - var actualBytes = record.SerializeToByteArray(numberEncoderService); - - var expectedBytes = GetExpectedBytes(record, numberEncoderService); - - CollectionAssert.AreEqual(expectedBytes, actualBytes); - } - - [Test] - public void ECFRecord_DeserializeFromByteArray_HasCorrectData() - { - var numberEncoderService = new NumberEncoderService(); - var sourceRecord = CreateRecordWithSomeGoodTestData(); - var sourceRecordBytes = GetExpectedBytesWithoutName(sourceRecord, numberEncoderService); - - var record = new ECFRecord { ID = sourceRecord.ID, Name = sourceRecord.Name }; - record.DeserializeFromByteArray(sourceRecordBytes, numberEncoderService); - - var properties = record.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); - - Assert.IsTrue(properties.Length > 0); - - foreach (var property in properties) - { - var expectedValue = property.GetValue(sourceRecord); - var actualValue = property.GetValue(record); - - Assert.AreEqual(expectedValue, actualValue, "Property: {0}", property.Name); - } - } - - [Test] - public void ECFRecord_DeserializeFromByteArray_InvalidArrayLength_ThrowsException() - { - var record = new ECFRecord(); - - Assert.Throws(() => record.DeserializeFromByteArray(new byte[] { 1, 2, 3 }, new NumberEncoderService())); - } - - private static ECFRecord CreateRecordWithSomeGoodTestData() - { - return new ECFRecord - { - ID = 1, - Name = "TestName", - - Base = 33, - Type = 99, - - Str = 10, - Int = 20, - Wis = 30, - Agi = 200, - Con = 190, - Cha = 180 - }; - } - - private static byte[] GetExpectedBytes(ECFRecord rec, INumberEncoderService nes) - { - var ret = new List(); - - ret.AddRange(nes.EncodeNumber(rec.Name.Length, 1)); - ret.AddRange(Encoding.ASCII.GetBytes(rec.Name)); - ret.AddRange(GetExpectedBytesWithoutName(rec, nes)); - - return ret.ToArray(); - } - - private static byte[] GetExpectedBytesWithoutName(ECFRecord rec, INumberEncoderService nes) - { - var ret = new List(); - - ret.AddRange(nes.EncodeNumber(rec.Base, 1)); - ret.AddRange(nes.EncodeNumber(rec.Type, 1)); - ret.AddRange(nes.EncodeNumber(rec.Str, 2)); - ret.AddRange(nes.EncodeNumber(rec.Int, 2)); - ret.AddRange(nes.EncodeNumber(rec.Wis, 2)); - ret.AddRange(nes.EncodeNumber(rec.Agi, 2)); - ret.AddRange(nes.EncodeNumber(rec.Con, 2)); - ret.AddRange(nes.EncodeNumber(rec.Cha, 2)); - - return ret.ToArray(); - } - } -} diff --git a/EOLib.IO.Test/Pub/EIFFileTest.cs b/EOLib.IO.Test/Pub/EIFFileTest.cs index 03dcd31dd..68375846f 100644 --- a/EOLib.IO.Test/Pub/EIFFileTest.cs +++ b/EOLib.IO.Test/Pub/EIFFileTest.cs @@ -1,48 +1,41 @@ -using System.Collections.Generic; +using EOLib.IO.Pub; +using EOLib.IO.Services; +using EOLib.IO.Services.Serializers; +using NUnit.Framework; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Text; -using EOLib.IO.Pub; -using EOLib.IO.Services; -using NUnit.Framework; namespace EOLib.IO.Test.Pub { [TestFixture, ExcludeFromCodeCoverage] public class EIFFileTest { - private IPubFile _itemFile; - - [SetUp] - public void SetUp() - { - _itemFile = new EIFFile(); - } - [Test] public void HasCorrectFileType() { - Assert.AreEqual("EIF", _itemFile.FileType); + Assert.That(new EIFFile().FileType, Is.EqualTo("EIF")); } [Test] public void SerializeToByteArray_ReturnsExpectedBytes() { var expectedBytes = MakeEIFFile(55565554, - new EIFRecord {ID = 1, Name = "TestItem"}, - new EIFRecord {ID = 2, Name = "Test2"}, - new EIFRecord {ID = 3, Name = "Test3"}, - new EIFRecord {ID = 4, Name = "Test4"}, - new EIFRecord {ID = 5, Name = "Test5"}, - new EIFRecord {ID = 6, Name = "Test6"}, - new EIFRecord {ID = 7, Name = "Test7"}, - new EIFRecord {ID = 8, Name = "Test8"}, - new EIFRecord {ID = 9, Name = "eof"}); + new EIFRecord().WithID(1).WithNames(new List { "TestFixture" }), + new EIFRecord().WithID(2).WithNames(new List { "Test2" }), + new EIFRecord().WithID(3).WithNames(new List { "Test3" }), + new EIFRecord().WithID(4).WithNames(new List { "Test4" }), + new EIFRecord().WithID(5).WithNames(new List { "Test5" }), + new EIFRecord().WithID(6).WithNames(new List { "Test6" }), + new EIFRecord().WithID(7).WithNames(new List { "Test7" }), + new EIFRecord().WithID(8).WithNames(new List { "Test8" }), + new EIFRecord().WithID(9).WithNames(new List { "eof" })); - _itemFile.DeserializeFromByteArray(expectedBytes, new NumberEncoderService()); + var serializer = CreateFileSerializer(); + var file = serializer.DeserializeFromByteArray(expectedBytes, () => new EIFFile()); - var actualBytes = _itemFile.SerializeToByteArray(new NumberEncoderService(), rewriteChecksum: false); + var actualBytes = serializer.SerializeToByteArray(file, rewriteChecksum: false); CollectionAssert.AreEqual(expectedBytes, actualBytes); } @@ -52,49 +45,26 @@ public void DeserializeFromByteArray_HasExpectedIDAndNames() { var records = new[] { - new EIFRecord {ID = 1, Name = "TestItem"}, - new EIFRecord {ID = 2, Name = "Test2"}, - new EIFRecord {ID = 3, Name = "Test3"}, - new EIFRecord {ID = 4, Name = "Test4"}, - new EIFRecord {ID = 5, Name = "Test5"}, - new EIFRecord {ID = 6, Name = "Test6"}, - new EIFRecord {ID = 7, Name = "Test7"}, - new EIFRecord {ID = 8, Name = "Test8"}, - new EIFRecord {ID = 9, Name = "eof"} + new EIFRecord().WithID(1).WithNames(new List { "TestFixture" }), + new EIFRecord().WithID(2).WithNames(new List { "Test2" }), + new EIFRecord().WithID(3).WithNames(new List { "Test3" }), + new EIFRecord().WithID(4).WithNames(new List { "Test4" }), + new EIFRecord().WithID(5).WithNames(new List { "Test5" }), + new EIFRecord().WithID(6).WithNames(new List { "Test6" }), + new EIFRecord().WithID(7).WithNames(new List { "Test7" }), + new EIFRecord().WithID(8).WithNames(new List { "Test8" }), + new EIFRecord().WithID(9).WithNames(new List { "eof" }) }; var bytes = MakeEIFFile(55565554, records); - _itemFile.DeserializeFromByteArray(bytes, new NumberEncoderService()); + var serializer = CreateFileSerializer(); + var file = serializer.DeserializeFromByteArray(bytes, () => new EIFFile()); - CollectionAssert.AreEqual(records.Select(x => new {x.ID, x.Name}).ToList(), - _itemFile.Data.Select(x => new {x.ID, x.Name}).ToList()); + CollectionAssert.AreEqual(records.Select(x => new { x.ID, x.Name }).ToList(), + file.Select(x => new { x.ID, x.Name }).ToList()); } - [Test] - public void HeaderFormat_IsCorrect() - { - var nes = new NumberEncoderService(); - - var actualBytes = _itemFile.SerializeToByteArray(nes, rewriteChecksum: false); - - CollectionAssert.AreEqual(Encoding.ASCII.GetBytes(_itemFile.FileType), actualBytes.Take(3).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(_itemFile.CheckSum, 4), actualBytes.Skip(3).Take(4).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(_itemFile.Length, 2), actualBytes.Skip(7).Take(2).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(1, 1), actualBytes.Skip(9).Take(1).ToArray()); - } - - [Test] - public void LengthMismatch_ThrowsIOException() - { - var bytes = MakeEIFFileWithWrongLength(12345678, 5, - new EIFRecord {ID = 1, Name = "Item1"}, - new EIFRecord {ID = 2, Name = "Item2"}, - new EIFRecord {ID = 3, Name = "Item3"}); - - Assert.Throws(() => _itemFile.DeserializeFromByteArray(bytes, new NumberEncoderService())); - } - - private byte[] MakeEIFFile(int checksum, params EIFRecord[] records) + private byte[] MakeEIFFile(int checksum, params IPubRecord[] records) { var numberEncoderService = new NumberEncoderService(); @@ -103,25 +73,17 @@ private byte[] MakeEIFFile(int checksum, params EIFRecord[] records) bytes.AddRange(numberEncoderService.EncodeNumber(checksum, 4)); bytes.AddRange(numberEncoderService.EncodeNumber(records.Length, 2)); bytes.Add(numberEncoderService.EncodeNumber(1, 1)[0]); + + var recordSerializer = new PubRecordSerializer(numberEncoderService); foreach (var record in records) - bytes.AddRange(record.SerializeToByteArray(numberEncoderService)); + bytes.AddRange(recordSerializer.SerializeToByteArray(record)); return bytes.ToArray(); } - private byte[] MakeEIFFileWithWrongLength(int checksum, int length, params EIFRecord[] records) + private static IPubFileSerializer CreateFileSerializer() { - var numberEncoderService = new NumberEncoderService(); - - var bytes = new List(); - bytes.AddRange(Encoding.ASCII.GetBytes("EIF")); - bytes.AddRange(numberEncoderService.EncodeNumber(checksum, 4)); - bytes.AddRange(numberEncoderService.EncodeNumber(length, 2)); - bytes.Add(numberEncoderService.EncodeNumber(1, 1)[0]); - foreach (var record in records) - bytes.AddRange(record.SerializeToByteArray(numberEncoderService)); - - return bytes.ToArray(); + return new PubFileSerializer(new NumberEncoderService(), new PubRecordSerializer(new NumberEncoderService())); } } } diff --git a/EOLib.IO.Test/Pub/EIFRecordTest.cs b/EOLib.IO.Test/Pub/EIFRecordTest.cs deleted file mode 100644 index 3c51d803b..000000000 --- a/EOLib.IO.Test/Pub/EIFRecordTest.cs +++ /dev/null @@ -1,313 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Text; -using EOLib.IO.Pub; -using EOLib.IO.Services; -using NUnit.Framework; - -namespace EOLib.IO.Test.Pub -{ - [TestFixture, ExcludeFromCodeCoverage] - public class EIFRecordTest - { - [Test] - public void EIFRecord_GetGlobalPropertyID_GetsRecordID() - { - const int expected = 44; - var rec = new EIFRecord {ID = expected}; - - var actual = rec.Get(PubRecordProperty.GlobalID); - - Assert.AreEqual(expected, actual); - } - - [Test] - public void EIFRecord_GetGlobalPropertyName_GetsRecordName() - { - const string expected = "some name"; - var rec = new EIFRecord { Name = expected }; - - var actual = rec.Get(PubRecordProperty.GlobalName); - - Assert.AreEqual(expected, actual); - } - - [Test] - public void EIFRecord_GetItemPropertiesComprehensive_NoException() - { - var itemProperties = Enum.GetNames(typeof (PubRecordProperty)) - .Where(x => x.StartsWith("Item")) - .Select(x => (PubRecordProperty) Enum.Parse(typeof (PubRecordProperty), x)) - .ToArray(); - - Assert.AreNotEqual(0, itemProperties.Length); - - var record = new EIFRecord(); - - foreach (var property in itemProperties) - { - var dummy = record.Get(property); - Assert.IsNotNull(dummy); - } - } - - [Test] - public void EIFRecord_GetNPCProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.NPCAccuracy; - - var record = new EIFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void EIFRecord_GetSpellProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.SpellAccuracy; - - var record = new EIFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void EIFRecord_GetClassProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.ClassAgi; - - var record = new EIFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void EIFRecord_InvalidPropertyReturnType_ThrowsInvalidCastException() - { - var rec = new EIFRecord {Name = ""}; - - Assert.Throws(() => rec.Get(PubRecordProperty.GlobalName)); - } - - [Test] - public void EIFRecord_SerializeToByteArray_WritesExpectedFormat() - { - var numberEncoderService = new NumberEncoderService(); - var record = CreateRecordWithSomeGoodTestData(); - - var actualBytes = record.SerializeToByteArray(numberEncoderService); - - var expectedBytes = GetExpectedBytes(record, numberEncoderService); - - CollectionAssert.AreEqual(expectedBytes, actualBytes); - } - - [Test] - public void EIFRecord_DeserializeFromByteArray_HasCorrectData() - { - var numberEncoderService = new NumberEncoderService(); - var sourceRecord = CreateRecordWithSomeGoodTestData(); - var sourceRecordBytes = GetExpectedBytesWithoutName(sourceRecord, numberEncoderService); - - var record = new EIFRecord { ID = sourceRecord.ID, Name = sourceRecord.Name }; - record.DeserializeFromByteArray(sourceRecordBytes, numberEncoderService); - - var properties = record.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); - - Assert.IsTrue(properties.Length > 0); - - foreach (var property in properties) - { - var expectedValue = property.GetValue(sourceRecord); - var actualValue = property.GetValue(record); - - Assert.AreEqual(expectedValue, actualValue, "Property: {0}", property.Name); - } - } - - [Test] - public void EIFRecord_DeserializeFromByteArray_InvalidArrayLength_ThrowsException() - { - var record = new EIFRecord(); - - Assert.Throws(() => record.DeserializeFromByteArray(new byte[] { 1, 2, 3 }, new NumberEncoderService())); - } - - [Test] - public void EIFRecord_GunOverride_ConvertsItemSubTypeToRanged() - { - var record = new EIFRecord {ID = 365, Name = "Gun", SubType = ItemSubType.Arrows}; - record.DeserializeFromByteArray(Enumerable.Repeat((byte)128, EIFRecord.DATA_SIZE).ToArray(), new NumberEncoderService()); - - Assert.AreEqual(ItemSubType.Ranged, record.SubType); - } - - [Test] - public void EIFRecord_SharedValueSet1_HaveSameValue() - { - var properties = new[] { "ScrollMap", "DollGraphic", "ExpReward", "HairColor", "Effect", "Key" }; - var record = new EIFRecord(); - - var value = new Random().Next(100); - foreach (var prop in properties) - { - record.GetType().GetProperty(prop, BindingFlags.Instance | BindingFlags.Public) - .SetValue(record, value); - - Assert.AreEqual(value, record.ScrollMap); - Assert.AreEqual(value, record.DollGraphic); - Assert.AreEqual(value, record.ExpReward); - Assert.AreEqual(value, record.HairColor); - Assert.AreEqual(value, record.Effect); - Assert.AreEqual(value, record.Key); - - value++; - } - } - - [Test] - public void EIFRecord_SharedValueSet2_HaveSameValue() - { - var properties = new[] { "Gender", "ScrollX" }; - var record = new EIFRecord(); - - var value = new Random().Next(100); - foreach (var prop in properties) - { - record.GetType().GetProperty(prop, BindingFlags.Instance | BindingFlags.Public) - .SetValue(record, (byte)value); - - Assert.AreEqual(value, record.Gender); - Assert.AreEqual(value, record.ScrollX); - - value++; - } - } - - [Test] - public void EIFRecord_SharedValueSet3_HaveSameValue() - { - var properties = new[] { "ScrollY", "DualWieldDollGraphic" }; - var record = new EIFRecord(); - - var value = new Random().Next(100); - foreach (var prop in properties) - { - record.GetType().GetProperty(prop, BindingFlags.Instance | BindingFlags.Public) - .SetValue(record, (byte)value); - - Assert.AreEqual(value, record.ScrollY); - Assert.AreEqual(value, record.DualWieldDollGraphic); - - value++; - } - } - - private static EIFRecord CreateRecordWithSomeGoodTestData() - { - return new EIFRecord - { - ID = 1, - Name = "TestName", - Graphic = 123, - Type = ItemType.Bracer, - SubType = ItemSubType.Ranged, - Special = ItemSpecial.Unique, - HP = 456, - TP = 654, - MinDam = 33, - MaxDam = 66, - Accuracy = 100, - Evade = 200, - Armor = 300, - Str = 40, - Int = 50, - Wis = 60, - Agi = 70, - Con = 80, - Cha = 90, - Light = 3, - Dark = 6, - Earth = 9, - Air = 12, - Water = 15, - Fire = 18, - ScrollMap = 33, - Gender = 44, - ScrollY = 55, - LevelReq = 66, - ClassReq = 77, - StrReq = 88, - IntReq = 99, - WisReq = 30, - AgiReq = 20, - ConReq = 10, - ChaReq = 5, - Weight = 200, - Size = ItemSize.Size2x3 - }; - } - - private static byte[] GetExpectedBytes(EIFRecord rec, INumberEncoderService nes) - { - var ret = new List(); - - ret.AddRange(nes.EncodeNumber(rec.Name.Length, 1)); - ret.AddRange(Encoding.ASCII.GetBytes(rec.Name)); - ret.AddRange(GetExpectedBytesWithoutName(rec, nes)); - - return ret.ToArray(); - } - - private static byte[] GetExpectedBytesWithoutName(EIFRecord rec, INumberEncoderService nes) - { - var ret = new List(); - - ret.AddRange(nes.EncodeNumber(rec.Graphic, 2)); - ret.AddRange(nes.EncodeNumber((byte)rec.Type, 1)); - ret.AddRange(nes.EncodeNumber((byte)rec.SubType, 1)); - ret.AddRange(nes.EncodeNumber((byte)rec.Special, 1)); - ret.AddRange(nes.EncodeNumber(rec.HP, 2)); - ret.AddRange(nes.EncodeNumber(rec.TP, 2)); - ret.AddRange(nes.EncodeNumber(rec.MinDam, 2)); - ret.AddRange(nes.EncodeNumber(rec.MaxDam, 2)); - ret.AddRange(nes.EncodeNumber(rec.Accuracy, 2)); - ret.AddRange(nes.EncodeNumber(rec.Evade, 2)); - ret.AddRange(nes.EncodeNumber(rec.Armor, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte19, 1)); - ret.AddRange(nes.EncodeNumber(rec.Str, 1)); - ret.AddRange(nes.EncodeNumber(rec.Int, 1)); - ret.AddRange(nes.EncodeNumber(rec.Wis, 1)); - ret.AddRange(nes.EncodeNumber(rec.Agi, 1)); - ret.AddRange(nes.EncodeNumber(rec.Con, 1)); - ret.AddRange(nes.EncodeNumber(rec.Cha, 1)); - ret.AddRange(nes.EncodeNumber(rec.Light, 1)); - ret.AddRange(nes.EncodeNumber(rec.Dark, 1)); - ret.AddRange(nes.EncodeNumber(rec.Earth, 1)); - ret.AddRange(nes.EncodeNumber(rec.Air, 1)); - ret.AddRange(nes.EncodeNumber(rec.Water, 1)); - ret.AddRange(nes.EncodeNumber(rec.Fire, 1)); - ret.AddRange(nes.EncodeNumber(rec.ScrollMap, 3)); - ret.AddRange(nes.EncodeNumber(rec.ScrollX, 1)); - ret.AddRange(nes.EncodeNumber(rec.ScrollY, 1)); - ret.AddRange(nes.EncodeNumber(rec.LevelReq, 2)); - ret.AddRange(nes.EncodeNumber(rec.ClassReq, 2)); - ret.AddRange(nes.EncodeNumber(rec.StrReq, 2)); - ret.AddRange(nes.EncodeNumber(rec.IntReq, 2)); - ret.AddRange(nes.EncodeNumber(rec.WisReq, 2)); - ret.AddRange(nes.EncodeNumber(rec.AgiReq, 2)); - ret.AddRange(nes.EncodeNumber(rec.ConReq, 2)); - ret.AddRange(nes.EncodeNumber(rec.ChaReq, 2)); - ret.AddRange(nes.EncodeNumber(rec.Element, 1)); - ret.AddRange(nes.EncodeNumber(rec.ElementPower, 1)); - ret.AddRange(nes.EncodeNumber(rec.Weight, 1)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte56, 1)); - ret.AddRange(nes.EncodeNumber((byte)rec.Size, 1)); - - return ret.ToArray(); - } - } -} diff --git a/EOLib.IO.Test/Pub/ENFFileTest.cs b/EOLib.IO.Test/Pub/ENFFileTest.cs index c24c829e0..aba624965 100644 --- a/EOLib.IO.Test/Pub/ENFFileTest.cs +++ b/EOLib.IO.Test/Pub/ENFFileTest.cs @@ -1,100 +1,70 @@ -using System.Collections.Generic; +using EOLib.IO.Pub; +using EOLib.IO.Services; +using EOLib.IO.Services.Serializers; +using NUnit.Framework; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Text; -using EOLib.IO.Pub; -using EOLib.IO.Services; -using NUnit.Framework; namespace EOLib.IO.Test.Pub { [TestFixture, ExcludeFromCodeCoverage] public class ENFFileTest { - private IPubFile _npcFile; - - [SetUp] - public void SetUp() - { - _npcFile = new ENFFile(); - } - [Test] public void HasCorrectFileType() { - Assert.AreEqual("ENF", _npcFile.FileType); + Assert.That(new ENFFile().FileType, Is.EqualTo("ENF")); } [Test] public void SerializeToByteArray_ReturnsExpectedBytes() { var expectedBytes = MakeENFFile(55565554, - new ENFRecord { ID = 1, Name = "TestNPC" }, - new ENFRecord { ID = 2, Name = "Test2" }, - new ENFRecord { ID = 3, Name = "Test3" }, - new ENFRecord { ID = 4, Name = "Test4" }, - new ENFRecord { ID = 5, Name = "Test5" }, - new ENFRecord { ID = 6, Name = "Test6" }, - new ENFRecord { ID = 7, Name = "Test7" }, - new ENFRecord { ID = 8, Name = "Test8" }, - new ENFRecord { ID = 9, Name = "eof" }); + new ENFRecord().WithID(1).WithNames(new List { "TestFixture" }), + new ENFRecord().WithID(2).WithNames(new List { "Test2" }), + new ENFRecord().WithID(3).WithNames(new List { "Test3" }), + new ENFRecord().WithID(4).WithNames(new List { "Test4" }), + new ENFRecord().WithID(5).WithNames(new List { "Test5" }), + new ENFRecord().WithID(6).WithNames(new List { "Test6" }), + new ENFRecord().WithID(7).WithNames(new List { "Test7" }), + new ENFRecord().WithID(8).WithNames(new List { "Test8" }), + new ENFRecord().WithID(9).WithNames(new List { "eof" })); - _npcFile.DeserializeFromByteArray(expectedBytes, new NumberEncoderService()); + var serializer = CreateFileSerializer(); + var file = serializer.DeserializeFromByteArray(expectedBytes, () => new ENFFile()); - var actualBytes = _npcFile.SerializeToByteArray(new NumberEncoderService(), rewriteChecksum: false); + var actualBytes = serializer.SerializeToByteArray(file, rewriteChecksum: false); CollectionAssert.AreEqual(expectedBytes, actualBytes); } - [Test] - public void HeaderFormat_IsCorrect() - { - var nes = new NumberEncoderService(); - - var actualBytes = _npcFile.SerializeToByteArray(nes, rewriteChecksum: false); - - CollectionAssert.AreEqual(Encoding.ASCII.GetBytes(_npcFile.FileType), actualBytes.Take(3).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(_npcFile.CheckSum, 4), actualBytes.Skip(3).Take(4).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(_npcFile.Length, 2), actualBytes.Skip(7).Take(2).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(1, 1), actualBytes.Skip(9).Take(1).ToArray()); - } - - [Test] - public void LengthMismatch_ThrowsIOException() - { - var bytes = MakeENFFileWithWrongLength(12345678, 5, - new ENFRecord { ID = 1, Name = "NPC1" }, - new ENFRecord { ID = 2, Name = "NPC2" }, - new ENFRecord { ID = 3, Name = "NPC3" }); - - Assert.Throws(() => _npcFile.DeserializeFromByteArray(bytes, new NumberEncoderService())); - } - [Test] public void DeserializeFromByteArray_HasExpectedIDAndNames() { var records = new[] { - new ENFRecord {ID = 1, Name = "Test"}, - new ENFRecord {ID = 2, Name = "Test2"}, - new ENFRecord {ID = 3, Name = "Test3"}, - new ENFRecord {ID = 4, Name = "Test4"}, - new ENFRecord {ID = 5, Name = "Test5"}, - new ENFRecord {ID = 6, Name = "Test6"}, - new ENFRecord {ID = 7, Name = "Test7"}, - new ENFRecord {ID = 8, Name = "Test8"}, - new ENFRecord {ID = 9, Name = "eof"} + new ENFRecord().WithID(1).WithNames(new List { "TestFixture" }), + new ENFRecord().WithID(2).WithNames(new List { "Test2" }), + new ENFRecord().WithID(3).WithNames(new List { "Test3" }), + new ENFRecord().WithID(4).WithNames(new List { "Test4" }), + new ENFRecord().WithID(5).WithNames(new List { "Test5" }), + new ENFRecord().WithID(6).WithNames(new List { "Test6" }), + new ENFRecord().WithID(7).WithNames(new List { "Test7" }), + new ENFRecord().WithID(8).WithNames(new List { "Test8" }), + new ENFRecord().WithID(9).WithNames(new List { "eof" }) }; var bytes = MakeENFFile(55565554, records); - _npcFile.DeserializeFromByteArray(bytes, new NumberEncoderService()); + var serializer = CreateFileSerializer(); + var file = serializer.DeserializeFromByteArray(bytes, () => new ENFFile()); CollectionAssert.AreEqual(records.Select(x => new { x.ID, x.Name }).ToList(), - _npcFile.Data.Select(x => new { x.ID, x.Name }).ToList()); + file.Select(x => new { x.ID, x.Name }).ToList()); } - private byte[] MakeENFFile(int checksum, params ENFRecord[] records) + private byte[] MakeENFFile(int checksum, params IPubRecord[] records) { var numberEncoderService = new NumberEncoderService(); @@ -103,25 +73,17 @@ private byte[] MakeENFFile(int checksum, params ENFRecord[] records) bytes.AddRange(numberEncoderService.EncodeNumber(checksum, 4)); bytes.AddRange(numberEncoderService.EncodeNumber(records.Length, 2)); bytes.Add(numberEncoderService.EncodeNumber(1, 1)[0]); + + var recordSerializer = new PubRecordSerializer(numberEncoderService); foreach (var record in records) - bytes.AddRange(record.SerializeToByteArray(numberEncoderService)); + bytes.AddRange(recordSerializer.SerializeToByteArray(record)); return bytes.ToArray(); } - private byte[] MakeENFFileWithWrongLength(int checksum, int length, params ENFRecord[] records) + private static IPubFileSerializer CreateFileSerializer() { - var numberEncoderService = new NumberEncoderService(); - - var bytes = new List(); - bytes.AddRange(Encoding.ASCII.GetBytes("ENF")); - bytes.AddRange(numberEncoderService.EncodeNumber(checksum, 4)); - bytes.AddRange(numberEncoderService.EncodeNumber(length, 2)); - bytes.Add(numberEncoderService.EncodeNumber(1, 1)[0]); - foreach (var record in records) - bytes.AddRange(record.SerializeToByteArray(numberEncoderService)); - - return bytes.ToArray(); + return new PubFileSerializer(new NumberEncoderService(), new PubRecordSerializer(new NumberEncoderService())); } } } diff --git a/EOLib.IO.Test/Pub/ENFRecordTest.cs b/EOLib.IO.Test/Pub/ENFRecordTest.cs deleted file mode 100644 index 29fdb80f7..000000000 --- a/EOLib.IO.Test/Pub/ENFRecordTest.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Text; -using EOLib.IO.Pub; -using EOLib.IO.Services; -using NUnit.Framework; - -namespace EOLib.IO.Test.Pub -{ - [TestFixture, ExcludeFromCodeCoverage] - public class ENFRecordTest - { - [Test] - public void ENFRecord_GetGlobalPropertyID_GetsRecordID() - { - const int expected = 44; - var rec = new ENFRecord { ID = expected }; - - var actual = rec.Get(PubRecordProperty.GlobalID); - - Assert.AreEqual(expected, actual); - } - - [Test] - public void ENFRecord_GetGlobalPropertyName_GetsRecordName() - { - const string expected = "some name"; - var rec = new ENFRecord { Name = expected }; - - var actual = rec.Get(PubRecordProperty.GlobalName); - - Assert.AreEqual(expected, actual); - } - - [Test] - public void ENFRecord_GetNPCPropertiesComprehensive_NoException() - { - var npcProperties = Enum.GetNames(typeof(PubRecordProperty)) - .Where(x => x.StartsWith("NPC")) - .Select(x => (PubRecordProperty)Enum.Parse(typeof(PubRecordProperty), x)) - .ToArray(); - - Assert.AreNotEqual(0, npcProperties.Length); - - var record = new ENFRecord(); - - foreach (var property in npcProperties) - { - var dummy = record.Get(property); - Assert.IsNotNull(dummy); - } - } - - [Test] - public void ENFRecord_GetItemProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.ItemSubType; - - var record = new ENFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void ENFRecord_GetSpellProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.SpellAccuracy; - - var record = new ENFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void ENFRecord_GetClassProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.ClassAgi; - - var record = new ENFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void ENFRecord_InvalidPropertyReturnType_ThrowsInvalidCastException() - { - var rec = new ENFRecord { Name = "" }; - - Assert.Throws(() => rec.Get(PubRecordProperty.GlobalName)); - } - - [Test] - public void ENFRecord_SerializeToByteArray_WritesExpectedFormat() - { - var numberEncoderService = new NumberEncoderService(); - var record = CreateRecordWithSomeGoodTestData(); - - var actualBytes = record.SerializeToByteArray(numberEncoderService); - - var expectedBytes = GetExpectedBytes(record, numberEncoderService); - - CollectionAssert.AreEqual(expectedBytes, actualBytes); - } - - [Test] - public void ENFRecord_DeserializeFromByteArray_HasCorrectData() - { - var numberEncoderService = new NumberEncoderService(); - var sourceRecord = CreateRecordWithSomeGoodTestData(); - var sourceRecordBytes = GetExpectedBytesWithoutName(sourceRecord, numberEncoderService); - - var record = new ENFRecord { ID = sourceRecord.ID, Name = sourceRecord.Name }; - record.DeserializeFromByteArray(sourceRecordBytes, numberEncoderService); - - var properties = record.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); - - Assert.IsTrue(properties.Length > 0); - - foreach (var property in properties) - { - var expectedValue = property.GetValue(sourceRecord); - var actualValue = property.GetValue(record); - - Assert.AreEqual(expectedValue, actualValue, "Property: {0}", property.Name); - } - } - - [Test] - public void ENFRecord_DeserializeFromByteArray_InvalidArrayLength_ThrowsException() - { - var record = new ENFRecord(); - - Assert.Throws(() => record.DeserializeFromByteArray(new byte[] { 1, 2, 3 }, new NumberEncoderService())); - } - - private static ENFRecord CreateRecordWithSomeGoodTestData() - { - return new ENFRecord - { - ID = 1, - Name = "TestName", - Graphic = 123, - Boss = 321, - Child = 4321, - Type = NPCType.Barber, - - VendorID = 1234, - - HP = 123456, - Exp = 44332, - MinDam = 16543, - MaxDam = 16544, - - Accuracy = 31313, - Evade = 13131, - Armor = 222 - }; - } - - private static byte[] GetExpectedBytes(ENFRecord rec, INumberEncoderService nes) - { - var ret = new List(); - - ret.AddRange(nes.EncodeNumber(rec.Name.Length, 1)); - ret.AddRange(Encoding.ASCII.GetBytes(rec.Name)); - ret.AddRange(GetExpectedBytesWithoutName(rec, nes)); - - return ret.ToArray(); - } - - private static byte[] GetExpectedBytesWithoutName(ENFRecord rec, INumberEncoderService nes) - { - var ret = new List(); - - ret.AddRange(nes.EncodeNumber(rec.Graphic, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte2, 1)); - ret.AddRange(nes.EncodeNumber(rec.Boss, 2)); - ret.AddRange(nes.EncodeNumber(rec.Child, 2)); - ret.AddRange(nes.EncodeNumber((short)rec.Type, 2)); - ret.AddRange(nes.EncodeNumber(rec.VendorID, 2)); - ret.AddRange(nes.EncodeNumber(rec.HP, 3)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort14, 2)); - ret.AddRange(nes.EncodeNumber(rec.MinDam, 2)); - ret.AddRange(nes.EncodeNumber(rec.MaxDam, 2)); - ret.AddRange(nes.EncodeNumber(rec.Accuracy, 2)); - ret.AddRange(nes.EncodeNumber(rec.Evade, 2)); - ret.AddRange(nes.EncodeNumber(rec.Armor, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte26, 1)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort27, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort29, 2)); - ret.AddRange(nes.EncodeNumber(rec.ElementWeak, 2)); - ret.AddRange(nes.EncodeNumber(rec.ElementWeakPower, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte35, 1)); - ret.AddRange(nes.EncodeNumber(rec.Exp, 3)); - - return ret.ToArray(); - } - } -} diff --git a/EOLib.IO.Test/Pub/ESFFileTest.cs b/EOLib.IO.Test/Pub/ESFFileTest.cs index cb958441c..7af8d4fe9 100644 --- a/EOLib.IO.Test/Pub/ESFFileTest.cs +++ b/EOLib.IO.Test/Pub/ESFFileTest.cs @@ -1,100 +1,70 @@ -using System.Collections.Generic; +using EOLib.IO.Pub; +using EOLib.IO.Services; +using EOLib.IO.Services.Serializers; +using NUnit.Framework; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Text; -using EOLib.IO.Pub; -using EOLib.IO.Services; -using NUnit.Framework; namespace EOLib.IO.Test.Pub { [TestFixture, ExcludeFromCodeCoverage] public class ESFFileTest { - private IPubFile _spellFile; - - [SetUp] - public void SetUp() - { - _spellFile = new ESFFile(); - } - [Test] public void HasCorrectFileType() { - Assert.AreEqual("ESF", _spellFile.FileType); + Assert.That(new ESFFile().FileType, Is.EqualTo("ESF")); } [Test] public void SerializeToByteArray_ReturnsExpectedBytes() { var expectedBytes = MakeESFFile(55565554, - new ESFRecord { ID = 1, Name = "TestSpell", Shout = "TestShout" }, - new ESFRecord { ID = 2, Name = "Test2", Shout = "TestShout2" }, - new ESFRecord { ID = 3, Name = "Test3", Shout = "TestShout3" }, - new ESFRecord { ID = 4, Name = "Test4", Shout = "TestShout4" }, - new ESFRecord { ID = 5, Name = "Test5", Shout = "TestShout5" }, - new ESFRecord { ID = 6, Name = "Test6", Shout = "TestShout6" }, - new ESFRecord { ID = 7, Name = "Test7", Shout = "TestShout7" }, - new ESFRecord { ID = 8, Name = "Test8", Shout = "TestShout8" }, - new ESFRecord { ID = 9, Name = "eof", Shout = "-" }); + new ESFRecord().WithID(1).WithNames(new List { "TestFixture", "Shout" }), + new ESFRecord().WithID(2).WithNames(new List { "Test2", "Shout" }), + new ESFRecord().WithID(3).WithNames(new List { "Test3", "Shout" }), + new ESFRecord().WithID(4).WithNames(new List { "Test4", "Shout" }), + new ESFRecord().WithID(5).WithNames(new List { "Test5", "Shout" }), + new ESFRecord().WithID(6).WithNames(new List { "Test6", "Shout" }), + new ESFRecord().WithID(7).WithNames(new List { "Test7", "Shout" }), + new ESFRecord().WithID(8).WithNames(new List { "Test8", "Shout" }), + new ESFRecord().WithID(9).WithNames(new List { "eof", "eof" })); - _spellFile.DeserializeFromByteArray(expectedBytes, new NumberEncoderService()); + var serializer = CreateFileSerializer(); + var file = serializer.DeserializeFromByteArray(expectedBytes, () => new ESFFile()); - var actualBytes = _spellFile.SerializeToByteArray(new NumberEncoderService(), rewriteChecksum: false); + var actualBytes = serializer.SerializeToByteArray(file, rewriteChecksum: false); CollectionAssert.AreEqual(expectedBytes, actualBytes); } - [Test] - public void HeaderFormat_IsCorrect() - { - var nes = new NumberEncoderService(); - - var actualBytes = _spellFile.SerializeToByteArray(nes, rewriteChecksum: false); - - CollectionAssert.AreEqual(Encoding.ASCII.GetBytes(_spellFile.FileType), actualBytes.Take(3).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(_spellFile.CheckSum, 4), actualBytes.Skip(3).Take(4).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(_spellFile.Length, 2), actualBytes.Skip(7).Take(2).ToArray()); - CollectionAssert.AreEqual(nes.EncodeNumber(1, 1), actualBytes.Skip(9).Take(1).ToArray()); - } - - [Test] - public void LengthMismatch_ThrowsIOException() - { - var bytes = MakeESFFileWithWrongLength(12345678, 5, - new ESFRecord { ID = 1, Name = "Spell1", Shout = "Spell1" }, - new ESFRecord { ID = 2, Name = "Spell2", Shout = "Spell2" }, - new ESFRecord { ID = 3, Name = "Spell3", Shout = "Spell3" }); - - Assert.Throws(() => _spellFile.DeserializeFromByteArray(bytes, new NumberEncoderService())); - } - [Test] public void DeserializeFromByteArray_HasExpectedIDAndNames() { var records = new[] { - new ESFRecord {ID = 1, Name = "Test", Shout = "Test"}, - new ESFRecord {ID = 2, Name = "Test2", Shout = "Test2"}, - new ESFRecord {ID = 3, Name = "Test3", Shout = "Test3"}, - new ESFRecord {ID = 4, Name = "Test4", Shout = "Test4"}, - new ESFRecord {ID = 5, Name = "Test5", Shout = "Test5"}, - new ESFRecord {ID = 6, Name = "Test6", Shout = "Test6"}, - new ESFRecord {ID = 7, Name = "Test7", Shout = "Test7"}, - new ESFRecord {ID = 8, Name = "Test8", Shout = "Test8"}, - new ESFRecord {ID = 9, Name = "eof", Shout = ""} + new ESFRecord().WithID(1).WithNames(new List { "TestFixture", "Shout" }), + new ESFRecord().WithID(2).WithNames(new List { "Test2", "Shout" }), + new ESFRecord().WithID(3).WithNames(new List { "Test3", "Shout" }), + new ESFRecord().WithID(4).WithNames(new List { "Test4", "Shout" }), + new ESFRecord().WithID(5).WithNames(new List { "Test5", "Shout" }), + new ESFRecord().WithID(6).WithNames(new List { "Test6", "Shout" }), + new ESFRecord().WithID(7).WithNames(new List { "Test7", "Shout" }), + new ESFRecord().WithID(8).WithNames(new List { "Test8", "Shout" }), + new ESFRecord().WithID(9).WithNames(new List { "eof", "eof" }) }; var bytes = MakeESFFile(55565554, records); - _spellFile.DeserializeFromByteArray(bytes, new NumberEncoderService()); + var serializer = CreateFileSerializer(); + var file = serializer.DeserializeFromByteArray(bytes, () => new ESFFile()); CollectionAssert.AreEqual(records.Select(x => new { x.ID, x.Name }).ToList(), - _spellFile.Data.Select(x => new { x.ID, x.Name }).ToList()); + file.Select(x => new { x.ID, x.Name }).ToList()); } - private byte[] MakeESFFile(int checksum, params ESFRecord[] records) + private byte[] MakeESFFile(int checksum, params IPubRecord[] records) { var numberEncoderService = new NumberEncoderService(); @@ -103,25 +73,17 @@ private byte[] MakeESFFile(int checksum, params ESFRecord[] records) bytes.AddRange(numberEncoderService.EncodeNumber(checksum, 4)); bytes.AddRange(numberEncoderService.EncodeNumber(records.Length, 2)); bytes.Add(numberEncoderService.EncodeNumber(1, 1)[0]); + + var recordSerializer = new PubRecordSerializer(numberEncoderService); foreach (var record in records) - bytes.AddRange(record.SerializeToByteArray(numberEncoderService)); + bytes.AddRange(recordSerializer.SerializeToByteArray(record)); return bytes.ToArray(); } - private byte[] MakeESFFileWithWrongLength(int checksum, int length, params ESFRecord[] records) + private static IPubFileSerializer CreateFileSerializer() { - var numberEncoderService = new NumberEncoderService(); - - var bytes = new List(); - bytes.AddRange(Encoding.ASCII.GetBytes("ESF")); - bytes.AddRange(numberEncoderService.EncodeNumber(checksum, 4)); - bytes.AddRange(numberEncoderService.EncodeNumber(length, 2)); - bytes.Add(numberEncoderService.EncodeNumber(1, 1)[0]); - foreach (var record in records) - bytes.AddRange(record.SerializeToByteArray(numberEncoderService)); - - return bytes.ToArray(); + return new PubFileSerializer(new NumberEncoderService(), new PubRecordSerializer(new NumberEncoderService())); } } } diff --git a/EOLib.IO.Test/Pub/ESFRecordTest.cs b/EOLib.IO.Test/Pub/ESFRecordTest.cs deleted file mode 100644 index 31206d294..000000000 --- a/EOLib.IO.Test/Pub/ESFRecordTest.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Text; -using EOLib.IO.Pub; -using EOLib.IO.Services; -using NUnit.Framework; - -namespace EOLib.IO.Test.Pub -{ - [TestFixture, ExcludeFromCodeCoverage] - public class ESFRecordTest - { - [Test] - public void ESFRecord_GetGlobalPropertyID_GetsRecordID() - { - const int expected = 44; - var rec = new ESFRecord { ID = expected }; - - var actual = rec.Get(PubRecordProperty.GlobalID); - - Assert.AreEqual(expected, actual); - } - - [Test] - public void ESFRecord_GetGlobalPropertyName_GetsRecordName() - { - const string expected = "some name"; - var rec = new ESFRecord { Name = expected }; - - var actual = rec.Get(PubRecordProperty.GlobalName); - - Assert.AreEqual(expected, actual); - } - - [Test] - public void ESFRecord_GetSpellPropertiesComprehensive_NoException() - { - var spellProperties = Enum.GetNames(typeof(PubRecordProperty)) - .Where(x => x.StartsWith("Spell")) - .Select(x => (PubRecordProperty)Enum.Parse(typeof(PubRecordProperty), x)) - .ToArray(); - - Assert.AreNotEqual(0, spellProperties.Length); - - var record = new ESFRecord {Shout = ""}; - - foreach (var property in spellProperties) - { - var dummy = record.Get(property); - Assert.IsNotNull(dummy); - } - } - - [Test] - public void ESFRecord_GetItemProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.ItemSubType; - - var record = new ESFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void ESFRecord_GetNPCProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.NPCAccuracy; - - var record = new ESFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void ESFRecord_GetClassProperty_ThrowsArgumentOutOfRangeException() - { - const PubRecordProperty invalidProperty = PubRecordProperty.ClassAgi; - - var record = new ESFRecord(); - - Assert.Throws(() => record.Get(invalidProperty)); - } - - [Test] - public void ESFRecord_InvalidPropertyReturnType_ThrowsInvalidCastException() - { - var rec = new ESFRecord { Name = "" }; - - Assert.Throws(() => rec.Get(PubRecordProperty.GlobalName)); - } - - [Test] - public void ESFRecord_SerializeToByteArray_WritesExpectedFormat() - { - var numberEncoderService = new NumberEncoderService(); - var record = CreateRecordWithSomeGoodTestData(); - - var actualBytes = record.SerializeToByteArray(numberEncoderService); - - var expectedBytes = GetExpectedBytes(record, numberEncoderService); - - CollectionAssert.AreEqual(expectedBytes, actualBytes); - } - - [Test] - public void ESFRecord_DeserializeFromByteArray_HasCorrectData() - { - var numberEncoderService = new NumberEncoderService(); - var sourceRecord = CreateRecordWithSomeGoodTestData(); - var sourceRecordBytes = GetExpectedBytesWithoutNames(sourceRecord, numberEncoderService); - - var record = new ESFRecord { ID = sourceRecord.ID, Name = sourceRecord.Name, Shout = sourceRecord.Shout }; - record.DeserializeFromByteArray(sourceRecordBytes, numberEncoderService); - - var properties = record.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); - - Assert.IsTrue(properties.Length > 0); - - foreach (var property in properties) - { - var expectedValue = property.GetValue(sourceRecord); - var actualValue = property.GetValue(record); - - Assert.AreEqual(expectedValue, actualValue, "Property: {0}", property.Name); - } - } - - [Test] - public void ESFRecord_DeserializeFromByteArray_InvalidArrayLength_ThrowsException() - { - var record = new ESFRecord(); - - Assert.Throws(() => record.DeserializeFromByteArray(new byte[] { 1, 2, 3 }, new NumberEncoderService())); - } - - private static ESFRecord CreateRecordWithSomeGoodTestData() - { - return new ESFRecord - { - ID = 1, - Name = "TestName", - Shout = "TestShout", - Icon = 321, - Graphic = 123, - TP = 400, - SP = 900, - - CastTime = 12, - - Type = SpellType.Bard, - TargetRestrict = SpellTargetRestrict.Opponent, - Target = SpellTarget.Unknown1, - - MinDam = 3212, - MaxDam = 16543, - Accuracy = 222, - HP = 777 - }; - } - - private static byte[] GetExpectedBytes(ESFRecord rec, INumberEncoderService nes) - { - var ret = new List(); - - ret.AddRange(nes.EncodeNumber(rec.Name.Length, 1)); - ret.AddRange(nes.EncodeNumber(rec.Shout.Length, 1)); - ret.AddRange(Encoding.ASCII.GetBytes(rec.Name)); - ret.AddRange(Encoding.ASCII.GetBytes(rec.Shout)); - ret.AddRange(GetExpectedBytesWithoutNames(rec, nes)); - - return ret.ToArray(); - } - - private static byte[] GetExpectedBytesWithoutNames(ESFRecord rec, INumberEncoderService nes) - { - var ret = new List(); - - ret.AddRange(nes.EncodeNumber(rec.Icon, 2)); - ret.AddRange(nes.EncodeNumber(rec.Graphic, 2)); - ret.AddRange(nes.EncodeNumber(rec.TP, 2)); - ret.AddRange(nes.EncodeNumber(rec.SP, 2)); - ret.AddRange(nes.EncodeNumber(rec.CastTime, 1)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte9, 1)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte10, 1)); - ret.AddRange(nes.EncodeNumber((int)rec.Type, 3)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte14, 1)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort15, 2)); - ret.AddRange(nes.EncodeNumber((byte)rec.TargetRestrict, 1)); - ret.AddRange(nes.EncodeNumber((byte)rec.Target, 1)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte19, 1)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte20, 1)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort21, 2)); - ret.AddRange(nes.EncodeNumber(rec.MinDam, 2)); - ret.AddRange(nes.EncodeNumber(rec.MaxDam, 2)); - ret.AddRange(nes.EncodeNumber(rec.Accuracy, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort29, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort31, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte33, 1)); - ret.AddRange(nes.EncodeNumber(rec.HP, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort36, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkByte38, 1)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort39, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort41, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort43, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort45, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort47, 2)); - ret.AddRange(nes.EncodeNumber(rec.UnkShort49, 2)); - - return ret.ToArray(); - } - } -} diff --git a/EOLib.IO/Caster.cs b/EOLib.IO/Caster.cs new file mode 100644 index 000000000..a05496011 --- /dev/null +++ b/EOLib.IO/Caster.cs @@ -0,0 +1,37 @@ +// This implementation was yoinked directly from: https://stackoverflow.com/a/23391746/2562283 + +using System; +using System.Linq.Expressions; + +namespace EOLib.IO +{ + /// + /// Class to cast to type + /// + /// Target type + public static class CastTo + { + /// + /// Casts to . + /// This does not cause boxing for value types. + /// Useful in generic methods. + /// + /// Source type to cast from. Usually a generic type. + public static T From(S s) + { + return Cache.caster(s); + } + + private static class Cache + { + public static readonly Func caster = Get(); + + private static Func Get() + { + var p = Expression.Parameter(typeof(S)); + var c = Expression.ConvertChecked(p, typeof(T)); + return Expression.Lambda>(c, p).Compile(); + } + } + } +} diff --git a/EOLib.IO/Extensions/EIFFileExtensions.cs b/EOLib.IO/Extensions/EIFFileExtensions.cs index 21bba5cf9..ce6221bbd 100644 --- a/EOLib.IO/Extensions/EIFFileExtensions.cs +++ b/EOLib.IO/Extensions/EIFFileExtensions.cs @@ -7,10 +7,10 @@ public static class EIFFileExtensions { public static bool IsShieldOnBack(this IPubFile itemFile, short graphic) { - if (itemFile == null || itemFile.Data == null) + if (itemFile == null) return false; - var shieldInfo = itemFile.Data.FirstOrDefault(x => x.Type == ItemType.Shield && x.DollGraphic == graphic); + var shieldInfo = itemFile.FirstOrDefault(x => x.Type == ItemType.Shield && x.DollGraphic == graphic); return shieldInfo != null && (shieldInfo.Name == "Bag" || @@ -20,10 +20,10 @@ public static bool IsShieldOnBack(this IPubFile itemFile, short graph public static bool IsRangedWeapon(this IPubFile itemFile, short graphic) { - if (itemFile == null || itemFile.Data == null) + if (itemFile == null) return false; - var weaponInfo = itemFile.Data.FirstOrDefault(x => x.Type == ItemType.Weapon && x.DollGraphic == graphic); + var weaponInfo = itemFile.FirstOrDefault(x => x.Type == ItemType.Weapon && x.DollGraphic == graphic); return weaponInfo != null && (weaponInfo.Name == "Gun" || weaponInfo.SubType == ItemSubType.Ranged); } diff --git a/EOLib.IO/Pub/BasePubFile.cs b/EOLib.IO/Pub/BasePubFile.cs index 118aa75ef..8d594dc88 100644 --- a/EOLib.IO/Pub/BasePubFile.cs +++ b/EOLib.IO/Pub/BasePubFile.cs @@ -1,73 +1,82 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.IO; -using System.Text; -using EOLib.IO.Services; namespace EOLib.IO.Pub { - public abstract class BasePubFile : IPubFile - where T : class, IPubRecord, new() + public abstract class BasePubFile : IPubFile + where TRecord : class, IPubRecord, new() { - protected readonly List _data; + private readonly List _data; public abstract string FileType { get; } - public int CheckSum { get; set; } + public int CheckSum { get; private set; } public int Length => _data.Count; - public T this[int id] + // pub files use 1-based indexing + public TRecord this[int id] => _data[id - 1]; + + protected BasePubFile() + { + _data = new List(); + } + + protected BasePubFile(int checksum, List data) { - get - { - if (id < 1 || id > _data.Count) - return null; + CheckSum = checksum; + _data = data; + } - return _data[id - 1]; - } + public IPubFile WithCheckSum(int checksum) + { + var copy = MakeCopy(); + copy.CheckSum = checksum; + return copy; } - public IReadOnlyList Data => _data; + public IPubFile WithAddedRecord(TRecord record) + { + var copy = MakeCopy(); + copy._data.Add(record); + return copy; + } - protected BasePubFile() + public IPubFile WithUpdatedRecord(TRecord record) { - _data = new List(); + if (_data.Count <= record.ID) + throw new ArgumentException($"Record {record.ID} ({record.Name}) is not part of the pub file"); + + var copy = MakeCopy(); + copy._data[record.ID] = record; + return copy; + } + + public IPubFile WithRemovedRecord(TRecord record) + { + var copy = MakeCopy(); + if (copy._data.Remove(record)) + AdjustIDs(copy._data); + return copy; + } + + public IEnumerator GetEnumerator() + { + return _data.GetEnumerator(); } - public byte[] SerializeToByteArray(INumberEncoderService numberEncoderService, bool rewriteChecksum = true) + IEnumerator IEnumerable.GetEnumerator() { - byte[] fileBytes; - - using (var mem = new MemoryStream()) //write to memory so we can get a CRC for the new RID value - { - mem.Write(Encoding.ASCII.GetBytes(FileType), 0, 3); - mem.Write(numberEncoderService.EncodeNumber(0, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(0, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Length, 2), 0, 2); - - mem.WriteByte(numberEncoderService.EncodeNumber(1, 1)[0]); - - foreach (var dataRecord in _data) - { - var toWrite = dataRecord.SerializeToByteArray(numberEncoderService); - mem.Write(toWrite, 0, toWrite.Length); - } - - fileBytes = mem.ToArray(); - } - - var checksumBytes = numberEncoderService.EncodeNumber(CheckSum, 4); - if (rewriteChecksum) - { - var checksum = CRC32.Check(fileBytes); - checksumBytes = numberEncoderService.EncodeNumber((int)checksum, 4); - } - - Array.Copy(checksumBytes, 0, fileBytes, 3, 4); - return fileBytes; + return _data.GetEnumerator(); } - public abstract void DeserializeFromByteArray(byte[] bytes, INumberEncoderService numberEncoderService); + protected abstract BasePubFile MakeCopy(); + + private static void AdjustIDs(List data) + { + for (int i = 0; i < data.Count; i++) + data[i] = (TRecord)data[i].WithID(i); + } } } diff --git a/EOLib.IO/Pub/ECFFile.cs b/EOLib.IO/Pub/ECFFile.cs index 7fca2ae8d..05c29f38e 100644 --- a/EOLib.IO/Pub/ECFFile.cs +++ b/EOLib.IO/Pub/ECFFile.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Text; -using EOLib.IO.Services; +using System.Collections.Generic; namespace EOLib.IO.Pub { @@ -8,42 +6,18 @@ public class ECFFile : BasePubFile { public override string FileType => "ECF"; - public override void DeserializeFromByteArray(byte[] bytes, INumberEncoderService numberEncoderService) + public ECFFile() { - using (var mem = new MemoryStream(bytes)) - ReadFromStream(mem, numberEncoderService); } - private void ReadFromStream(Stream mem, INumberEncoderService numberEncoderService) + public ECFFile(int checksum, List data) + : base(checksum, data) { - mem.Seek(3, SeekOrigin.Begin); - - var checksum = new byte[4]; - mem.Read(checksum, 0, 4); - CheckSum = numberEncoderService.DecodeNumber(checksum); - - var lenBytes = new byte[2]; - mem.Read(lenBytes, 0, 2); - var recordsInFile = (short)numberEncoderService.DecodeNumber(lenBytes); - - mem.Seek(1, SeekOrigin.Current); - - var rawData = new byte[ECFRecord.DATA_SIZE]; - for (int i = 1; i <= recordsInFile && mem.Position < mem.Length; ++i) - { - var nameLength = numberEncoderService.DecodeNumber((byte)mem.ReadByte()); - var rawName = new byte[nameLength]; - mem.Read(rawName, 0, nameLength); - mem.Read(rawData, 0, ECFRecord.DATA_SIZE); - - var record = new ECFRecord { ID = i, Name = Encoding.ASCII.GetString(rawName) }; - record.DeserializeFromByteArray(rawData, numberEncoderService); - - _data.Add(record); - } + } - if (recordsInFile != Length) - throw new IOException("Mismatch between expected length and actual length!"); + protected override BasePubFile MakeCopy() + { + return new ECFFile(CheckSum, new List(this)); } } } diff --git a/EOLib.IO/Pub/ECFRecord.cs b/EOLib.IO/Pub/ECFRecord.cs index f348f64af..84019807b 100644 --- a/EOLib.IO/Pub/ECFRecord.cs +++ b/EOLib.IO/Pub/ECFRecord.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -7,81 +8,36 @@ namespace EOLib.IO.Pub { - public class ECFRecord : IPubRecord + public class ECFRecord : PubRecord { - public const int DATA_SIZE = 14; - - public int RecordSize => DATA_SIZE; - - public int ID { get; set; } - - public string Name { get; set; } - - public byte Base { get; set; } - public byte Type { get; set; } - - public short Str { get; set; } - public short Int { get; set; } - public short Wis { get; set; } - public short Agi { get; set; } - public short Con { get; set; } - public short Cha { get; set; } - - public TValue Get(PubRecordProperty type) + public byte Base => Get(PubRecordProperty.ClassBase); + public byte Type => Get(PubRecordProperty.ClassType); + + public short Str => Get(PubRecordProperty.ClassStr); + public short Int => Get(PubRecordProperty.ClassInt); + public short Wis => Get(PubRecordProperty.ClassWis); + public short Agi => Get(PubRecordProperty.ClassAgi); + public short Con => Get(PubRecordProperty.ClassCon); + public short Cha => Get(PubRecordProperty.ClassCha); + + public ECFRecord() + : this(0, string.Empty) { - var name = Enum.GetName(type.GetType(), type) ?? ""; - if (!name.StartsWith("Global") && !name.StartsWith("Class")) - throw new ArgumentOutOfRangeException(nameof(type), "Unsupported property requested for ECFRecord"); - - if (name.StartsWith("Global")) - name = name.Substring(6); - else if (name.StartsWith("Class")) - name = name.Substring(5); - - var propertyInfo = GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public); - var boxedValue = propertyInfo.GetValue(this); - - return (TValue)boxedValue; } - public byte[] SerializeToByteArray(INumberEncoderService numberEncoderService) + public ECFRecord(int id, string name) + : base(id, name, PubRecordProperty.Class) { - var ret = Enumerable.Repeat(254, DATA_SIZE + 1 + Name.Length).ToArray(); - - using (var mem = new MemoryStream(ret)) - { - mem.WriteByte(numberEncoderService.EncodeNumber(Name.Length, 1)[0]); - var name = Encoding.ASCII.GetBytes(Name); - mem.Write(name, 0, name.Length); - - mem.WriteByte(numberEncoderService.EncodeNumber(Base, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Type, 1)[0]); - - mem.Write(numberEncoderService.EncodeNumber(Str, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Int, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Wis, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Agi, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Con, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Cha, 2), 0, 2); - } - - return ret; } - public void DeserializeFromByteArray(byte[] recordBytes, INumberEncoderService numberEncoderService) + private ECFRecord(int id, List names, Dictionary propertyBag) + : base(id, names, propertyBag) { - if (recordBytes.Length != DATA_SIZE) - throw new ArgumentOutOfRangeException(nameof(recordBytes), "Data is not properly sized for correct deserialization"); - - Base = (byte)numberEncoderService.DecodeNumber(recordBytes[0]); - Type = (byte)numberEncoderService.DecodeNumber(recordBytes[1]); + } - Str = (short)numberEncoderService.DecodeNumber(recordBytes[2], recordBytes[3]); - Int = (short)numberEncoderService.DecodeNumber(recordBytes[4], recordBytes[5]); - Wis = (short)numberEncoderService.DecodeNumber(recordBytes[6], recordBytes[7]); - Agi = (short)numberEncoderService.DecodeNumber(recordBytes[8], recordBytes[9]); - Con = (short)numberEncoderService.DecodeNumber(recordBytes[10], recordBytes[11]); - Cha = (short)numberEncoderService.DecodeNumber(recordBytes[12], recordBytes[13]); + protected override PubRecord MakeCopy(List names, Dictionary propertyBag) + { + return new ECFRecord(ID, new List(names), new Dictionary(propertyBag)); } } } diff --git a/EOLib.IO/Pub/EIFFile.cs b/EOLib.IO/Pub/EIFFile.cs index 602c6a274..5a00f9730 100644 --- a/EOLib.IO/Pub/EIFFile.cs +++ b/EOLib.IO/Pub/EIFFile.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Text; -using EOLib.IO.Services; +using System.Collections.Generic; namespace EOLib.IO.Pub { @@ -8,42 +6,18 @@ public class EIFFile : BasePubFile { public override string FileType => "EIF"; - public override void DeserializeFromByteArray(byte[] bytes, INumberEncoderService numberEncoderService) + public EIFFile() { - using (var mem = new MemoryStream(bytes)) - ReadFromStream(mem, numberEncoderService); } - private void ReadFromStream(Stream mem, INumberEncoderService numberEncoderService) + public EIFFile(int checksum, List data) + : base (checksum, data) { - mem.Seek(3, SeekOrigin.Begin); - - var checksum = new byte[4]; - mem.Read(checksum, 0, 4); - CheckSum = numberEncoderService.DecodeNumber(checksum); - - var lenBytes = new byte[2]; - mem.Read(lenBytes, 0, 2); - var recordsInFile = (short)numberEncoderService.DecodeNumber(lenBytes); - - mem.Seek(1, SeekOrigin.Current); - - var rawData = new byte[EIFRecord.DATA_SIZE]; - for (int i = 1; i <= recordsInFile && mem.Position < mem.Length; ++i) - { - var nameLength = numberEncoderService.DecodeNumber((byte)mem.ReadByte()); - var rawName = new byte[nameLength]; - mem.Read(rawName, 0, nameLength); - mem.Read(rawData, 0, EIFRecord.DATA_SIZE); - - var record = new EIFRecord {ID = i, Name = Encoding.ASCII.GetString(rawName)}; - record.DeserializeFromByteArray(rawData, numberEncoderService); - - _data.Add(record); - } + } - if (recordsInFile != Length) - throw new IOException("Mismatch between expected length and actual length!"); + protected override BasePubFile MakeCopy() + { + return new EIFFile(CheckSum, new List(this)); } } } diff --git a/EOLib.IO/Pub/EIFRecord.cs b/EOLib.IO/Pub/EIFRecord.cs index dab8a562f..8c3ea5efc 100644 --- a/EOLib.IO/Pub/EIFRecord.cs +++ b/EOLib.IO/Pub/EIFRecord.cs @@ -1,224 +1,88 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using EOLib.IO.Services; +using System.Collections.Generic; namespace EOLib.IO.Pub { - public class EIFRecord : IPubRecord + public class EIFRecord : PubRecord { - public const int DATA_SIZE = 58; - - public int RecordSize => DATA_SIZE; - - public int ID { get; set; } - - public string Name { get; set; } - - public short Graphic { get; set; } - public ItemType Type { get; set; } - public ItemSubType SubType { get; set; } - - public ItemSpecial Special { get; set; } - public short HP { get; set; } - public short TP { get; set; } - public short MinDam { get; set; } - public short MaxDam { get; set; } - public short Accuracy { get; set; } - public short Evade { get; set; } - public short Armor { get; set; } - - public byte UnkByte19 { get; set; } - - public byte Str { get; set; } - public byte Int { get; set; } - public byte Wis { get; set; } - public byte Agi { get; set; } - public byte Con { get; set; } - public byte Cha { get; set; } - - public byte Light { get; set; } - public byte Dark { get; set; } - public byte Earth { get; set; } - public byte Air { get; set; } - public byte Water { get; set; } - public byte Fire { get; set; } - - private int _itemSpecificParam1; - private byte _itemSpecificParam2; - private byte _itemSpecificParam3; - - public int ScrollMap { get { return _itemSpecificParam1; } set { _itemSpecificParam1 = value; } } - public int DollGraphic { get { return _itemSpecificParam1; } set { _itemSpecificParam1 = value; } } - public int ExpReward { get { return _itemSpecificParam1; } set { _itemSpecificParam1 = value; } } - public int HairColor { get { return _itemSpecificParam1; } set { _itemSpecificParam1 = value; } } - public int Effect { get { return _itemSpecificParam1; } set { _itemSpecificParam1 = value; } } - public int Key { get { return _itemSpecificParam1; } set { _itemSpecificParam1 = value; } } - - public byte Gender { get { return _itemSpecificParam2; } set { _itemSpecificParam2 = value; } } - public byte ScrollX { get { return _itemSpecificParam2; } set { _itemSpecificParam2 = value; } } - - public byte ScrollY { get { return _itemSpecificParam3; } set { _itemSpecificParam3 = value; } } - public byte DualWieldDollGraphic { get { return _itemSpecificParam3; } set { _itemSpecificParam3 = value; } } - - public short LevelReq { get; set; } - public short ClassReq { get; set; } - public short StrReq { get; set; } - public short IntReq { get; set; } - public short WisReq { get; set; } - public short AgiReq { get; set; } - public short ConReq { get; set; } - public short ChaReq { get; set; } - - public byte Element { get; set; } - public byte ElementPower { get; set; } - - public byte Weight { get; set; } - - public byte UnkByte56 { get; set; } - - public ItemSize Size { get; set; } - - public TValue Get(PubRecordProperty type) + public short Graphic => Get(PubRecordProperty.ItemGraphic); + + public ItemType Type => Get(PubRecordProperty.ItemType); + public ItemSubType SubType => Get(PubRecordProperty.ItemSubType); + + public ItemSpecial Special => Get(PubRecordProperty.ItemSpecial); + public short HP => Get(PubRecordProperty.ItemHP); + public short TP => Get(PubRecordProperty.ItemTP); + public short MinDam => Get(PubRecordProperty.ItemMinDam); + public short MaxDam => Get(PubRecordProperty.ItemMaxDam); + public short Accuracy => Get(PubRecordProperty.ItemAccuracy); + public short Evade => Get(PubRecordProperty.ItemEvade); + public short Armor => Get(PubRecordProperty.ItemArmor); + + public byte UnkByte19 => Get(PubRecordProperty.ItemUnkByte19); + + public byte Str => Get(PubRecordProperty.ItemStr); + public byte Int => Get(PubRecordProperty.ItemInt); + public byte Wis => Get(PubRecordProperty.ItemWis); + public byte Agi => Get(PubRecordProperty.ItemAgi); + public byte Con => Get(PubRecordProperty.ItemCon); + public byte Cha => Get(PubRecordProperty.ItemCha); + + public byte Light => Get(PubRecordProperty.ItemLight); + public byte Dark => Get(PubRecordProperty.ItemDark); + public byte Earth => Get(PubRecordProperty.ItemEarth); + public byte Air => Get(PubRecordProperty.ItemAir); + public byte Water => Get(PubRecordProperty.ItemWater); + public byte Fire => Get(PubRecordProperty.ItemFire); + + public int ScrollMap => Get(PubRecordProperty.ItemScrollMap); + public int DollGraphic => Get(PubRecordProperty.ItemDollGraphic); + public int ExpReward => Get(PubRecordProperty.ItemExpReward); + public int HairColor => Get(PubRecordProperty.ItemHairColor); + public int Effect => Get(PubRecordProperty.ItemEffect); + public int Key => Get(PubRecordProperty.ItemKey); + + public byte Gender => Get(PubRecordProperty.ItemGender); + public byte ScrollX => Get(PubRecordProperty.ItemScrollX); + + public byte ScrollY => Get(PubRecordProperty.ItemScrollY); + public byte DualWieldDollGraphic => Get(PubRecordProperty.ItemDualWieldDollGraphic); + + public short LevelReq => Get(PubRecordProperty.ItemLevelReq); + public short ClassReq => Get(PubRecordProperty.ItemClassReq); + public short StrReq => Get(PubRecordProperty.ItemStrReq); + public short IntReq => Get(PubRecordProperty.ItemIntReq); + public short WisReq => Get(PubRecordProperty.ItemWisReq); + public short AgiReq => Get(PubRecordProperty.ItemAgiReq); + public short ConReq => Get(PubRecordProperty.ItemConReq); + public short ChaReq => Get(PubRecordProperty.ItemChaReq); + + public byte Element => Get(PubRecordProperty.ItemElement); + public byte ElementPower => Get(PubRecordProperty.ItemElementPower); + + public byte Weight => Get(PubRecordProperty.ItemWeight); + + public byte UnkByte56 => Get(PubRecordProperty.ItemUnkByte56); + + public ItemSize Size => Get(PubRecordProperty.ItemSize); + + public EIFRecord() + : this(0, string.Empty) { - var name = Enum.GetName(type.GetType(), type) ?? ""; - if (!name.StartsWith("Global") && !name.StartsWith("Item")) - throw new ArgumentOutOfRangeException(nameof(type), "Unsupported property requested for EIFRecord"); - - if (name.StartsWith("Global")) - name = name.Substring(6); - else if (name.StartsWith("Item")) - name = name.Substring(4); - - var propertyInfo = GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public); - var boxedValue = propertyInfo.GetValue(this); + } - return (TValue)boxedValue; + public EIFRecord(int id, string name) + : base (id, name, PubRecordProperty.Item) + { } - public byte[] SerializeToByteArray(INumberEncoderService numberEncoderService) + private EIFRecord(int id, List names, Dictionary propertyBag) + : base(id, names, propertyBag) { - var ret = Enumerable.Repeat(254, DATA_SIZE + 1 + Name.Length).ToArray(); - - using (var mem = new MemoryStream(ret)) - { - mem.WriteByte(numberEncoderService.EncodeNumber(Name.Length, 1)[0]); - var name = Encoding.ASCII.GetBytes(Name); - mem.Write(name, 0, name.Length); - - mem.Write(numberEncoderService.EncodeNumber(Graphic, 2), 0, 2); - mem.WriteByte(numberEncoderService.EncodeNumber((byte)Type, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber((byte)SubType, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber((byte)Special, 1)[0]); - - mem.Write(numberEncoderService.EncodeNumber(HP, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(TP, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(MinDam, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(MaxDam, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Accuracy, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Evade, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Armor, 2), 0, 2); - - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte19, 1)[0]); - - mem.WriteByte(numberEncoderService.EncodeNumber(Str, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Int, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Wis, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Agi, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Con, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Cha, 1)[0]); - - mem.WriteByte(numberEncoderService.EncodeNumber(Light, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Dark, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Earth, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Air, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Water, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Fire, 1)[0]); - - mem.Write(numberEncoderService.EncodeNumber(ScrollMap, 3), 0, 3); - mem.WriteByte(numberEncoderService.EncodeNumber(ScrollX, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(ScrollY, 1)[0]); - - mem.Write(numberEncoderService.EncodeNumber(LevelReq, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(ClassReq, 2), 0, 2); - - mem.Write(numberEncoderService.EncodeNumber(StrReq, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(IntReq, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(WisReq, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(AgiReq, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(ConReq, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(ChaReq, 2), 0, 2); - - mem.WriteByte(numberEncoderService.EncodeNumber(Element, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(ElementPower, 1)[0]); - - mem.WriteByte(numberEncoderService.EncodeNumber(Weight, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte56, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber((byte)Size, 1)[0]); - } - - return ret; } - public void DeserializeFromByteArray(byte[] recordBytes, INumberEncoderService numberEncoderService) + protected override PubRecord MakeCopy(List names, Dictionary propertyBag) { - if (recordBytes.Length != DATA_SIZE) - throw new ArgumentOutOfRangeException(nameof(recordBytes), "Data is not properly sized for correct deserialization"); - - Graphic = (short)numberEncoderService.DecodeNumber(recordBytes[0], recordBytes[1]); - Type = (ItemType)numberEncoderService.DecodeNumber(recordBytes[2]); - SubType = (ItemSubType)numberEncoderService.DecodeNumber(recordBytes[3]); - - Special = (ItemSpecial)numberEncoderService.DecodeNumber(recordBytes[4]); - HP = (short)numberEncoderService.DecodeNumber(recordBytes[5], recordBytes[6]); - TP = (short)numberEncoderService.DecodeNumber(recordBytes[7], recordBytes[8]); - MinDam = (short)numberEncoderService.DecodeNumber(recordBytes[9], recordBytes[10]); - MaxDam = (short)numberEncoderService.DecodeNumber(recordBytes[11], recordBytes[12]); - Accuracy = (short)numberEncoderService.DecodeNumber(recordBytes[13], recordBytes[14]); - Evade = (short)numberEncoderService.DecodeNumber(recordBytes[15], recordBytes[16]); - Armor = (short)numberEncoderService.DecodeNumber(recordBytes[17], recordBytes[18]); - - UnkByte19 = (byte)numberEncoderService.DecodeNumber(recordBytes[19]); - - Str = (byte)numberEncoderService.DecodeNumber(recordBytes[20]); - Int = (byte)numberEncoderService.DecodeNumber(recordBytes[21]); - Wis = (byte)numberEncoderService.DecodeNumber(recordBytes[22]); - Agi = (byte)numberEncoderService.DecodeNumber(recordBytes[23]); - Con = (byte)numberEncoderService.DecodeNumber(recordBytes[24]); - Cha = (byte)numberEncoderService.DecodeNumber(recordBytes[25]); - - Light = (byte)numberEncoderService.DecodeNumber(recordBytes[26]); - Dark = (byte)numberEncoderService.DecodeNumber(recordBytes[27]); - Earth = (byte)numberEncoderService.DecodeNumber(recordBytes[28]); - Air = (byte)numberEncoderService.DecodeNumber(recordBytes[29]); - Water = (byte)numberEncoderService.DecodeNumber(recordBytes[30]); - Fire = (byte)numberEncoderService.DecodeNumber(recordBytes[31]); - - ScrollMap = numberEncoderService.DecodeNumber(recordBytes[32], recordBytes[33], recordBytes[34]); - ScrollX = (byte)numberEncoderService.DecodeNumber(recordBytes[35]); - ScrollY = (byte)numberEncoderService.DecodeNumber(recordBytes[36]); - - LevelReq = (short)numberEncoderService.DecodeNumber(recordBytes[37], recordBytes[38]); - ClassReq = (short)numberEncoderService.DecodeNumber(recordBytes[39], recordBytes[40]); - - StrReq = (short)numberEncoderService.DecodeNumber(recordBytes[41], recordBytes[42]); - IntReq = (short)numberEncoderService.DecodeNumber(recordBytes[43], recordBytes[44]); - WisReq = (short)numberEncoderService.DecodeNumber(recordBytes[45], recordBytes[46]); - AgiReq = (short)numberEncoderService.DecodeNumber(recordBytes[47], recordBytes[48]); - ConReq = (short)numberEncoderService.DecodeNumber(recordBytes[49], recordBytes[50]); - ChaReq = (short)numberEncoderService.DecodeNumber(recordBytes[51], recordBytes[52]); - - Element = (byte)numberEncoderService.DecodeNumber(recordBytes[53]); - ElementPower = (byte)numberEncoderService.DecodeNumber(recordBytes[54]); - - Weight = (byte)numberEncoderService.DecodeNumber(recordBytes[55]); - UnkByte56 = (byte)numberEncoderService.DecodeNumber(recordBytes[56]); - Size = (ItemSize)numberEncoderService.DecodeNumber(recordBytes[57]); - - if (ID == 365 && Name == "Gun") - SubType = ItemSubType.Ranged; + return new EIFRecord(ID, new List(names), new Dictionary(propertyBag)); } } } \ No newline at end of file diff --git a/EOLib.IO/Pub/ENFFile.cs b/EOLib.IO/Pub/ENFFile.cs index 6d15fda4d..714a6a6b1 100644 --- a/EOLib.IO/Pub/ENFFile.cs +++ b/EOLib.IO/Pub/ENFFile.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Text; -using EOLib.IO.Services; +using System.Collections.Generic; namespace EOLib.IO.Pub { @@ -8,42 +6,18 @@ public class ENFFile : BasePubFile { public override string FileType => "ENF"; - public override void DeserializeFromByteArray(byte[] bytes, INumberEncoderService numberEncoderService) + public ENFFile() { - using (var mem = new MemoryStream(bytes)) - ReadFromStream(mem, numberEncoderService); } - private void ReadFromStream(Stream mem, INumberEncoderService numberEncoderService) + public ENFFile(int checksum, List data) + : base(checksum, data) { - mem.Seek(3, SeekOrigin.Begin); - - var checksum = new byte[4]; - mem.Read(checksum, 0, 4); - CheckSum = numberEncoderService.DecodeNumber(checksum); - - var lenBytes = new byte[2]; - mem.Read(lenBytes, 0, 2); - var recordsInFile = (short)numberEncoderService.DecodeNumber(lenBytes); - - mem.Seek(1, SeekOrigin.Current); - - var rawData = new byte[ENFRecord.DATA_SIZE]; - for (int i = 1; i <= recordsInFile && mem.Position < mem.Length; ++i) - { - var nameLength = numberEncoderService.DecodeNumber((byte)mem.ReadByte()); - var rawName = new byte[nameLength]; - mem.Read(rawName, 0, nameLength); - mem.Read(rawData, 0, ENFRecord.DATA_SIZE); - - var record = new ENFRecord { ID = i, Name = Encoding.ASCII.GetString(rawName) }; - record.DeserializeFromByteArray(rawData, numberEncoderService); - - _data.Add(record); - } + } - if (recordsInFile != Length) - throw new IOException("Mismatch between expected length and actual length!"); + protected override BasePubFile MakeCopy() + { + return new ENFFile(CheckSum, new List(this)); } } } diff --git a/EOLib.IO/Pub/ENFRecord.cs b/EOLib.IO/Pub/ENFRecord.cs index 0ccab1957..59297d599 100644 --- a/EOLib.IO/Pub/ENFRecord.cs +++ b/EOLib.IO/Pub/ENFRecord.cs @@ -1,144 +1,59 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using EOLib.IO.Services; +using System.Collections.Generic; namespace EOLib.IO.Pub { - public class ENFRecord : IPubRecord + public class ENFRecord : PubRecord { - public const int DATA_SIZE = 39; + public int Graphic => Get(PubRecordProperty.NPCGraphic); - public int RecordSize => DATA_SIZE; + public byte UnkByte2 => Get(PubRecordProperty.NPCUnkByte2); - public int ID { get; set; } + public short Boss => Get(PubRecordProperty.NPCBoss); + public short Child => Get(PubRecordProperty.NPCChild); + public NPCType Type => Get(PubRecordProperty.NPCType); - public string Name { get; set; } + public short UnkShort14 => Get(PubRecordProperty.NPCUnkShort14); - public int Graphic { get; set; } + public short VendorID => Get(PubRecordProperty.NPCVendorID); - public byte UnkByte2 { get; set; } + public int HP => Get(PubRecordProperty.NPCHP); - public short Boss { get; set; } - public short Child { get; set; } - public NPCType Type { get; set; } + public short MinDam => Get(PubRecordProperty.NPCMinDam); + public short MaxDam => Get(PubRecordProperty.NPCMaxDam); - public short UnkShort14 { get; set; } + public short Accuracy => Get(PubRecordProperty.NPCAccuracy); + public short Evade => Get(PubRecordProperty.NPCEvade); + public short Armor => Get(PubRecordProperty.NPCArmor); - public short VendorID { get; set; } + public byte UnkByte26 => Get(PubRecordProperty.NPCUnkByte26); + public short UnkShort27 => Get(PubRecordProperty.NPCUnkShort27); + public short UnkShort29 => Get(PubRecordProperty.NPCUnkShort29); - public int HP { get; set; } + public short ElementWeak => Get(PubRecordProperty.NPCElementWeak); + public short ElementWeakPower => Get(PubRecordProperty.NPCElementWeakPower); - public short MinDam { get; set; } - public short MaxDam { get; set; } + public byte UnkByte35 => Get(PubRecordProperty.NPCUnkByte35); - public short Accuracy { get; set; } - public short Evade { get; set; } - public short Armor { get; set; } + public int Exp => Get(PubRecordProperty.NPCExp); - public byte UnkByte26 { get; set; } - public short UnkShort27 { get; set; } - public short UnkShort29 { get; set; } - - public short ElementWeak { get; set; } - public short ElementWeakPower { get; set; } - - public byte UnkByte35 { get; set; } - - public int Exp { get; set; } - - public TValue Get(PubRecordProperty type) + public ENFRecord() + : this(0, string.Empty) { - var name = Enum.GetName(type.GetType(), type) ?? ""; - if (!name.StartsWith("Global") && !name.StartsWith("NPC")) - throw new ArgumentOutOfRangeException(nameof(type), "Unsupported property requested for ENFRecord"); - - if (name.StartsWith("Global")) - name = name.Substring(6); - else if (name.StartsWith("NPC")) - name = name.Substring(3); - - var propertyInfo = GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public); - var boxedValue = propertyInfo.GetValue(this); - - return (TValue)boxedValue; } - public byte[] SerializeToByteArray(INumberEncoderService numberEncoderService) + public ENFRecord(int id, string name) + : base(id, name, PubRecordProperty.NPC) { - var ret = Enumerable.Repeat(254, DATA_SIZE + 1 + Name.Length).ToArray(); - - using (var mem = new MemoryStream(ret)) - { - mem.WriteByte(numberEncoderService.EncodeNumber(Name.Length, 1)[0]); - var name = Encoding.ASCII.GetBytes(Name); - mem.Write(name, 0, name.Length); - - mem.Write(numberEncoderService.EncodeNumber(Graphic, 2), 0, 2); - - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte2, 1)[0]); - - mem.Write(numberEncoderService.EncodeNumber(Boss, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Child, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber((short)Type, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(VendorID, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(HP, 3), 0, 3); - - mem.Write(numberEncoderService.EncodeNumber(UnkShort14, 2), 0, 2); - - mem.Write(numberEncoderService.EncodeNumber(MinDam, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(MaxDam, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Accuracy, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Evade, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Armor, 2), 0, 2); - - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte26, 1)[0]); - mem.Write(numberEncoderService.EncodeNumber(UnkShort27, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(UnkShort29, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(ElementWeak, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(ElementWeakPower, 2), 0, 2); - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte35, 1)[0]); - - mem.Write(numberEncoderService.EncodeNumber(Exp, 3), 0, 3); - } - - return ret; } - public void DeserializeFromByteArray(byte[] recordBytes, INumberEncoderService numberEncoderService) + private ENFRecord(int id, List names, Dictionary propertyBag) + : base(id, names, propertyBag) { - if (recordBytes.Length != DATA_SIZE) - throw new ArgumentOutOfRangeException(nameof(recordBytes), "Data is not properly sized for correct deserialization"); - - Graphic = numberEncoderService.DecodeNumber(recordBytes[0], recordBytes[1]); - - UnkByte2 = (byte)numberEncoderService.DecodeNumber(recordBytes[2]); - - Boss = (short)numberEncoderService.DecodeNumber(recordBytes[3], recordBytes[4]); - Child = (short)numberEncoderService.DecodeNumber(recordBytes[5], recordBytes[6]); - Type = (NPCType)numberEncoderService.DecodeNumber(recordBytes[7], recordBytes[8]); - VendorID = (short)numberEncoderService.DecodeNumber(recordBytes[9], recordBytes[10]); - HP = numberEncoderService.DecodeNumber(recordBytes[11], recordBytes[12], recordBytes[13]); - - UnkShort14 = (short)numberEncoderService.DecodeNumber(recordBytes[14], recordBytes[15]); - - MinDam = (short)numberEncoderService.DecodeNumber(recordBytes[16], recordBytes[17]); - MaxDam = (short)numberEncoderService.DecodeNumber(recordBytes[18], recordBytes[19]); - - Accuracy = (short)numberEncoderService.DecodeNumber(recordBytes[20], recordBytes[21]); - Evade = (short)numberEncoderService.DecodeNumber(recordBytes[22], recordBytes[23]); - Armor = (short)numberEncoderService.DecodeNumber(recordBytes[24], recordBytes[25]); - - UnkByte26 = (byte)numberEncoderService.DecodeNumber(recordBytes[26]); - UnkShort27 = (short)numberEncoderService.DecodeNumber(recordBytes[27], recordBytes[28]); - UnkShort29 = (short)numberEncoderService.DecodeNumber(recordBytes[29], recordBytes[30]); - ElementWeak = (short)numberEncoderService.DecodeNumber(recordBytes[31], recordBytes[32]); - ElementWeakPower = (short)numberEncoderService.DecodeNumber(recordBytes[33], recordBytes[34]); - UnkByte35 = (byte)numberEncoderService.DecodeNumber(recordBytes[35]); + } - Exp = numberEncoderService.DecodeNumber(recordBytes[36], recordBytes[37], recordBytes[38]); + protected override PubRecord MakeCopy(List names, Dictionary propertyBag) + { + return new ENFRecord(ID, new List(names), new Dictionary(propertyBag)); } } } \ No newline at end of file diff --git a/EOLib.IO/Pub/ESFFile.cs b/EOLib.IO/Pub/ESFFile.cs index 5eeacc372..c0f00aea3 100644 --- a/EOLib.IO/Pub/ESFFile.cs +++ b/EOLib.IO/Pub/ESFFile.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Text; -using EOLib.IO.Services; +using System.Collections.Generic; namespace EOLib.IO.Pub { @@ -8,50 +6,18 @@ public class ESFFile : BasePubFile { public override string FileType => "ESF"; - public override void DeserializeFromByteArray(byte[] bytes, INumberEncoderService numberEncoderService) + public ESFFile() { - using (var mem = new MemoryStream(bytes)) - ReadFromStream(mem, numberEncoderService); } - private void ReadFromStream(Stream mem, INumberEncoderService numberEncoderService) + public ESFFile(int checksum, List data) + : base(checksum, data) { - mem.Seek(3, SeekOrigin.Begin); - - var checksum = new byte[4]; - mem.Read(checksum, 0, 4); - CheckSum = numberEncoderService.DecodeNumber(checksum); - - var lenBytes = new byte[2]; - mem.Read(lenBytes, 0, 2); - var recordsInFile = (short)numberEncoderService.DecodeNumber(lenBytes); - - mem.Seek(1, SeekOrigin.Current); - - var rawData = new byte[ESFRecord.DATA_SIZE]; - for (int i = 1; i <= recordsInFile && mem.Position < mem.Length; ++i) - { - var nameLength = numberEncoderService.DecodeNumber((byte)mem.ReadByte()); - var shoutLength = numberEncoderService.DecodeNumber((byte)mem.ReadByte()); - var rawName = new byte[nameLength]; - var rawShout = new byte[shoutLength]; - mem.Read(rawName, 0, nameLength); - mem.Read(rawShout, 0, shoutLength); - mem.Read(rawData, 0, ESFRecord.DATA_SIZE); - - var record = new ESFRecord - { - ID = i, - Name = Encoding.ASCII.GetString(rawName), - Shout = Encoding.ASCII.GetString(rawShout) - }; - record.DeserializeFromByteArray(rawData, numberEncoderService); - - _data.Add(record); - } + } - if (recordsInFile != Length) - throw new IOException("Mismatch between expected length and actual length!"); + protected override BasePubFile MakeCopy() + { + return new ESFFile(CheckSum, new List(this)); } } } diff --git a/EOLib.IO/Pub/ESFRecord.cs b/EOLib.IO/Pub/ESFRecord.cs index 9cbca28f9..84231a477 100644 --- a/EOLib.IO/Pub/ESFRecord.cs +++ b/EOLib.IO/Pub/ESFRecord.cs @@ -1,183 +1,73 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using EOLib.IO.Services; +using System.Collections.Generic; namespace EOLib.IO.Pub { - public class ESFRecord : IPubRecord + public class ESFRecord : PubRecord { - public const int DATA_SIZE = 51; + public override int NumberOfNames => 2; - public int RecordSize => DATA_SIZE; + public string Shout => Names[1]; - public int ID { get; set; } + public short Icon => Get(PubRecordProperty.SpellIcon); + public short Graphic => Get(PubRecordProperty.SpellGraphic); - public string Name { get; set; } - public string Shout { get; set; } + public short TP => Get(PubRecordProperty.SpellTP); + public short SP => Get(PubRecordProperty.SpellSP); - public short Icon { get; set; } - public short Graphic { get; set; } + public byte CastTime => Get(PubRecordProperty.SpellCastTime); - public short TP { get; set; } - public short SP { get; set; } + public byte UnkByte9 => Get(PubRecordProperty.SpellUnkByte9); + public byte UnkByte10 => Get(PubRecordProperty.SpellUnkByte10); - public byte CastTime { get; set; } + public SpellType Type => Get(PubRecordProperty.SpellType); - public byte UnkByte9 { get; set; } - public byte UnkByte10 { get; set; } + public byte UnkByte14 => Get(PubRecordProperty.SpellUnkByte14); + public short UnkShort15 => Get(PubRecordProperty.SpellUnkShort15); - public SpellType Type { get; set; } + public SpellTargetRestrict TargetRestrict => Get(PubRecordProperty.SpellTargetRestrict); + public SpellTarget Target => Get(PubRecordProperty.SpellTarget); - public byte UnkByte14 { get; set; } - public short UnkShort15 { get; set; } + public byte UnkByte19 => Get(PubRecordProperty.SpellUnkByte19); + public byte UnkByte20 => Get(PubRecordProperty.SpellUnkByte20); + public short UnkShort21 => Get(PubRecordProperty.SpellUnkShort21); - public SpellTargetRestrict TargetRestrict { get; set; } - public SpellTarget Target { get; set; } + public short MinDam => Get(PubRecordProperty.SpellMinDam); + public short MaxDam => Get(PubRecordProperty.SpellMaxDam); + public short Accuracy => Get(PubRecordProperty.SpellAccuracy); - public byte UnkByte19 { get; set; } - public byte UnkByte20 { get; set; } - public short UnkShort21 { get; set; } + public short UnkShort29 => Get(PubRecordProperty.SpellUnkShort29); + public short UnkShort31 => Get(PubRecordProperty.SpellUnkShort31); + public byte UnkByte33 => Get(PubRecordProperty.SpellUnkByte33); - public short MinDam { get; set; } - public short MaxDam { get; set; } - public short Accuracy { get; set; } + public short HP => Get(PubRecordProperty.SpellHP); - public short UnkShort29 { get; set; } - public short UnkShort31 { get; set; } - public byte UnkByte33 { get; set; } + public short UnkShort36 => Get(PubRecordProperty.SpellUnkShort36); + public byte UnkByte38 => Get(PubRecordProperty.SpellUnkByte38); + public short UnkShort39 => Get(PubRecordProperty.SpellUnkShort39); + public short UnkShort41 => Get(PubRecordProperty.SpellUnkShort41); + public short UnkShort43 => Get(PubRecordProperty.SpellUnkShort43); + public short UnkShort45 => Get(PubRecordProperty.SpellUnkShort45); + public short UnkShort47 => Get(PubRecordProperty.SpellUnkShort47); + public short UnkShort49 => Get(PubRecordProperty.SpellUnkShort49); - public short HP { get; set; } - - public short UnkShort36 { get; set; } - public byte UnkByte38 { get; set; } - public short UnkShort39 { get; set; } - public short UnkShort41 { get; set; } - public short UnkShort43 { get; set; } - public short UnkShort45 { get; set; } - public short UnkShort47 { get; set; } - public short UnkShort49 { get; set; } - - public TValue Get(PubRecordProperty type) + public ESFRecord() + : this(0, string.Empty) { - var name = Enum.GetName(type.GetType(), type) ?? ""; - if (!name.StartsWith("Global") && !name.StartsWith("Spell")) - throw new ArgumentOutOfRangeException(nameof(type), "Unsupported property requested for ESFRecord"); - - if (name.StartsWith("Global")) - name = name.Substring(6); - else if (name.StartsWith("Spell")) - name = name.Substring(5); - - var propertyInfo = GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.Public); - var boxedValue = propertyInfo.GetValue(this); - - return (TValue)boxedValue; } - public byte[] SerializeToByteArray(INumberEncoderService numberEncoderService) + public ESFRecord(int id, string name) + : base(id, name, PubRecordProperty.Spell) { - var ret = Enumerable.Repeat(254, DATA_SIZE + 2 + Name.Length + Shout.Length).ToArray(); - - using (var mem = new MemoryStream(ret)) - { - mem.WriteByte(numberEncoderService.EncodeNumber(Name.Length, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(Shout.Length, 1)[0]); - var name = Encoding.ASCII.GetBytes(Name); - var shout = Encoding.ASCII.GetBytes(Shout); //shout, shout, let it all out! - mem.Write(name, 0, name.Length); - mem.Write(shout, 0, shout.Length); - - mem.Write(numberEncoderService.EncodeNumber(Icon, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Graphic, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(TP, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(SP, 2), 0, 2); - mem.WriteByte(numberEncoderService.EncodeNumber(CastTime, 1)[0]); - - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte9, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte10, 1)[0]); - - mem.Write(numberEncoderService.EncodeNumber((int)Type, 3), 0, 3); //This is documented as a 3 byte int. - - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte14, 1)[0]); - mem.Write(numberEncoderService.EncodeNumber(UnkShort15, 2), 0, 2); - - mem.WriteByte(numberEncoderService.EncodeNumber((byte)TargetRestrict, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber((byte)Target, 1)[0]); - - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte19, 1)[0]); - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte20, 1)[0]); - mem.Write(numberEncoderService.EncodeNumber(UnkShort21, 2), 0, 2); - - mem.Write(numberEncoderService.EncodeNumber(MinDam, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(MaxDam, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(Accuracy, 2), 0, 2); - - mem.Write(numberEncoderService.EncodeNumber(UnkShort29, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(UnkShort31, 2), 0, 2); - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte33, 1)[0]); - - mem.Write(numberEncoderService.EncodeNumber(HP, 2), 0, 2); - - mem.Write(numberEncoderService.EncodeNumber(UnkShort36, 2), 0, 2); - mem.WriteByte(numberEncoderService.EncodeNumber(UnkByte38, 1)[0]); - mem.Write(numberEncoderService.EncodeNumber(UnkShort39, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(UnkShort41, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(UnkShort43, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(UnkShort45, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(UnkShort47, 2), 0, 2); - mem.Write(numberEncoderService.EncodeNumber(UnkShort49, 2), 0, 2); - } - - return ret; } - public void DeserializeFromByteArray(byte[] recordBytes, INumberEncoderService numberEncoderService) + private ESFRecord(int id, List names, Dictionary propertyBag) + : base(id, names, propertyBag) { - if (recordBytes.Length != DATA_SIZE) - throw new ArgumentOutOfRangeException(nameof(recordBytes), "Data is not properly sized for correct deserialization"); - - Icon = (short)numberEncoderService.DecodeNumber(recordBytes[0], recordBytes[1]); - Graphic = (short)numberEncoderService.DecodeNumber(recordBytes[2], recordBytes[3]); - TP = (short)numberEncoderService.DecodeNumber(recordBytes[4], recordBytes[5]); - SP = (short)numberEncoderService.DecodeNumber(recordBytes[6], recordBytes[7]); - CastTime = (byte)numberEncoderService.DecodeNumber(recordBytes[8]); - - UnkByte9 = (byte)numberEncoderService.DecodeNumber(recordBytes[9]); - UnkByte10 = (byte)numberEncoderService.DecodeNumber(recordBytes[10]); - - Type = (SpellType)numberEncoderService.DecodeNumber(recordBytes[11], recordBytes[12], recordBytes[13]); - - UnkByte14 = (byte)numberEncoderService.DecodeNumber(recordBytes[14]); - UnkShort15 = (short)numberEncoderService.DecodeNumber(recordBytes[15], recordBytes[16]); - - TargetRestrict = (SpellTargetRestrict)numberEncoderService.DecodeNumber(recordBytes[17]); - Target = (SpellTarget)numberEncoderService.DecodeNumber(recordBytes[18]); - - UnkByte19 = (byte)numberEncoderService.DecodeNumber(recordBytes[19]); - UnkByte20 = (byte)numberEncoderService.DecodeNumber(recordBytes[20]); - UnkShort21 = (short)numberEncoderService.DecodeNumber(recordBytes[21], recordBytes[22]); - - MinDam = (short)numberEncoderService.DecodeNumber(recordBytes[23], recordBytes[24]); - MaxDam = (short)numberEncoderService.DecodeNumber(recordBytes[25], recordBytes[26]); - Accuracy = (short)numberEncoderService.DecodeNumber(recordBytes[27], recordBytes[28]); - - UnkShort29 = (short)numberEncoderService.DecodeNumber(recordBytes[29], recordBytes[30]); - UnkShort31 = (short)numberEncoderService.DecodeNumber(recordBytes[31], recordBytes[32]); - UnkByte33 = (byte)numberEncoderService.DecodeNumber(recordBytes[33]); - - HP = (short)numberEncoderService.DecodeNumber(recordBytes[34], recordBytes[35]); + } - UnkShort36 = (short)numberEncoderService.DecodeNumber(recordBytes[36], recordBytes[37]); - UnkByte38 = (byte)numberEncoderService.DecodeNumber(recordBytes[38]); - UnkShort39 = (short)numberEncoderService.DecodeNumber(recordBytes[39], recordBytes[40]); - UnkShort41 = (short)numberEncoderService.DecodeNumber(recordBytes[41], recordBytes[42]); - UnkShort43 = (short)numberEncoderService.DecodeNumber(recordBytes[43], recordBytes[44]); - UnkShort45 = (short)numberEncoderService.DecodeNumber(recordBytes[45], recordBytes[46]); - UnkShort47 = (short)numberEncoderService.DecodeNumber(recordBytes[47], recordBytes[48]); - UnkShort49 = (short)numberEncoderService.DecodeNumber(recordBytes[49], recordBytes[50]); + protected override PubRecord MakeCopy(List names, Dictionary propertyBag) + { + return new ESFRecord(ID, new List(names), new Dictionary(propertyBag)); } } } \ No newline at end of file diff --git a/EOLib.IO/Pub/IPubFile.cs b/EOLib.IO/Pub/IPubFile.cs index 23d4604e8..b5c99d28c 100644 --- a/EOLib.IO/Pub/IPubFile.cs +++ b/EOLib.IO/Pub/IPubFile.cs @@ -1,26 +1,27 @@ using System.Collections.Generic; -using EOLib.IO.Services; namespace EOLib.IO.Pub { - public interface IPubFile : IPubFile + public interface IPubFile : IPubFile, IEnumerable where TRecord : class, IPubRecord, new() { TRecord this[int id] { get; } - IReadOnlyList Data { get; } + IPubFile WithAddedRecord(TRecord record); + + IPubFile WithUpdatedRecord(TRecord record); + + IPubFile WithRemovedRecord(TRecord record); } public interface IPubFile { string FileType { get; } - int CheckSum { get; set; } + int CheckSum { get; } int Length { get; } - byte[] SerializeToByteArray(INumberEncoderService numberEncoderService, bool rewriteChecksum = true); - - void DeserializeFromByteArray(byte[] bytes, INumberEncoderService numberEncoderService); + IPubFile WithCheckSum(int checkSum); } } diff --git a/EOLib.IO/Pub/IPubRecord.cs b/EOLib.IO/Pub/IPubRecord.cs index f8d100cee..dde8f975b 100644 --- a/EOLib.IO/Pub/IPubRecord.cs +++ b/EOLib.IO/Pub/IPubRecord.cs @@ -1,19 +1,39 @@ -using EOLib.IO.Services; +using System.Collections.Generic; namespace EOLib.IO.Pub { public interface IPubRecord { - int RecordSize { get; } + int ID { get; } - int ID { get; set; } + /// + /// The first name in the list of names for the record + /// + string Name { get; } - string Name { get; set; } + /// + /// Collection of all names in the record + /// + IReadOnlyList Names { get; } - TValue Get(PubRecordProperty type); + /// + /// Expected number of names per record in a data file (ESF files have 'name' and 'shout' variable strings) + /// + int NumberOfNames { get; } - byte[] SerializeToByteArray(INumberEncoderService numberEncoderService); + /// + /// Constant size of a data record + /// + int DataSize { get; } - void DeserializeFromByteArray(byte[] recordBytes, INumberEncoderService numberEncoderService); + IReadOnlyDictionary Bag { get; } + + T Get(PubRecordProperty property); + + IPubRecord WithID(int id); + + IPubRecord WithNames(IReadOnlyList name); + + IPubRecord WithProperty(PubRecordProperty type, int value); } } \ No newline at end of file diff --git a/EOLib.IO/Pub/PubRecord.cs b/EOLib.IO/Pub/PubRecord.cs new file mode 100644 index 000000000..397808481 --- /dev/null +++ b/EOLib.IO/Pub/PubRecord.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace EOLib.IO.Pub +{ + public class PubRecord : IPubRecord + { + private readonly Lazy _dataSize; + + private readonly List _names; + private readonly Dictionary _propertyBag; + + public int ID { get; private set; } + + public string Name => _names.FirstOrDefault() ?? string.Empty; + + public IReadOnlyList Names => _names; + + public virtual int NumberOfNames => 1; + + public int DataSize => _dataSize.Value; + + public IReadOnlyDictionary Bag => _propertyBag; + + public PubRecord() + : this(0, new List { string.Empty }, new Dictionary()) + { + } + + public PubRecord(int id, string name, PubRecordProperty flag) + : this(id, new List { name }, flag) + { + } + + public PubRecord(int id, List names, PubRecordProperty flag) + : this(id, names, GetPropertiesWithFlag(flag)) + { + } + + public PubRecord(int id, List names, Dictionary propertyBag) + { + ID = id; + _names = names; + _propertyBag = propertyBag; + + _dataSize = new Lazy(() => _propertyBag.Values + .GroupBy(x => x.Offset) + .Select(x => x.First().Length) + .Aggregate((a, b) => a + b)); + } + + public IPubRecord WithID(int id) + { + var copy = MakeCopy(_names, _propertyBag); + copy.ID = id; + return copy; + } + + public IPubRecord WithNames(IReadOnlyList names) + { + var copy = MakeCopy(_names, _propertyBag); + copy._names.Clear(); + copy._names.AddRange(names); + return copy; + } + + public IPubRecord WithProperty(PubRecordProperty type, int value) + { + var copy = MakeCopy(_names, _propertyBag); + var existing = copy._propertyBag[type]; + copy._propertyBag[type] = new RecordData(existing.Offset, existing.Length, value); + return copy; + } + + public T Get(PubRecordProperty property) => CastTo.From(Bag[property].Value); + + protected virtual PubRecord MakeCopy(List names, Dictionary propertyBag) + { + return new PubRecord(ID, new List(names), new Dictionary(propertyBag)); + } + + private static Dictionary GetPropertiesWithFlag(PubRecordProperty flag) + { + var enumType = typeof(PubRecordProperty); + var enumValues = ((PubRecordProperty[])enumType.GetEnumValues()) + .Where(x => x.HasFlag(flag)) + .ToList(); + + return enumValues + .Select(x => new { Key = x, Value = enumType.GetMember(x.ToString()) }) + .Select(x => new { x.Key, Value = x.Value.FirstOrDefault(m => m.DeclaringType == enumType) }) + .Select(x => new { x.Key, Value = x.Value.GetCustomAttributes(false).SingleOrDefault() }) + .Where(x => x.Value != null) + .ToDictionary(k => k.Key, v => new RecordData(v.Value.Offset, v.Value.Length, 0)); + } + } +} diff --git a/EOLib.IO/Pub/PubRecordProperty.cs b/EOLib.IO/Pub/PubRecordProperty.cs index 50ae2ca04..47dfaaaa8 100644 --- a/EOLib.IO/Pub/PubRecordProperty.cs +++ b/EOLib.IO/Pub/PubRecordProperty.cs @@ -1,175 +1,266 @@ +using System; + namespace EOLib.IO.Pub { /// /// Enum representing the different properties that exist within the pub records /// - /// - /// IPubRecord::Get uses reflection to resolve the correct property. Property value resolution is based on the naming of the values in this enum. - /// These values should always match the names of the actual properties in the implementations of the IPubRecord files (plus the type prefix). - /// To be safe, just don't change the names here unless you know what you're doing. It's a crappy design, but it checks out. - /// - public enum PubRecordProperty + [Flags] + public enum PubRecordProperty : uint { - //Applicable to all records - GlobalID, - GlobalName, - - #region Item Specific + Item = 0x100, + [RecordData(0, 2)] ItemGraphic, + [RecordData(2, 1)] ItemType, + [RecordData(3, 1)] ItemSubType, + [RecordData(4, 1)] ItemSpecial, + [RecordData(5, 2)] ItemHP, + [RecordData(7, 2)] ItemTP, + [RecordData(9, 2)] ItemMinDam, + [RecordData(11, 2)] ItemMaxDam, + [RecordData(13, 2)] ItemAccuracy, + [RecordData(15, 2)] ItemEvade, + [RecordData(17, 2)] ItemArmor, + [RecordData(19, 1)] ItemUnkByte19, + [RecordData(20, 1)] ItemStr, + [RecordData(21, 1)] ItemInt, + [RecordData(22, 1)] ItemWis, + [RecordData(23, 1)] ItemAgi, + [RecordData(24, 1)] ItemCon, + [RecordData(25, 1)] ItemCha, + [RecordData(26, 1)] ItemLight, + [RecordData(27, 1)] ItemDark, + [RecordData(28, 1)] ItemEarth, + [RecordData(29, 1)] ItemAir, + [RecordData(30, 1)] ItemWater, + [RecordData(31, 1)] ItemFire, + [RecordData(32, 3)] ItemScrollMap, + [RecordData(32, 3)] ItemDollGraphic, + [RecordData(32, 3)] ItemExpReward, + [RecordData(32, 3)] ItemHairColor, + [RecordData(32, 3)] ItemEffect, + [RecordData(32, 3)] ItemKey, + [RecordData(35, 1)] ItemGender, + [RecordData(35, 1)] ItemScrollX, + [RecordData(36, 1)] ItemScrollY, + [RecordData(36, 1)] ItemDualWieldDollGraphic, + [RecordData(37, 2)] ItemLevelReq, + [RecordData(39, 2)] ItemClassReq, + [RecordData(41, 2)] ItemStrReq, + [RecordData(43, 2)] ItemIntReq, + [RecordData(45, 2)] ItemWisReq, + [RecordData(47, 2)] ItemAgiReq, + [RecordData(49, 2)] ItemConReq, + [RecordData(51, 2)] ItemChaReq, + [RecordData(53, 1)] ItemElement, + [RecordData(54, 1)] ItemElementPower, + [RecordData(55, 1)] ItemWeight, + [RecordData(56, 1)] ItemUnkByte56, + [RecordData(57, 1)] ItemSize, - #endregion - - #region NPC Specific + NPC = 0x200, + [RecordData(0, 2)] NPCGraphic, + [RecordData(2, 1)] NPCUnkByte2, + [RecordData(3, 2)] NPCBoss, + [RecordData(5, 2)] NPCChild, + [RecordData(7, 2)] NPCType, - - NPCUnkShort14, - + [RecordData(9, 2)] NPCVendorID, + [RecordData(11, 3)] NPCHP, - NPCExp, + + [RecordData(14, 2)] + NPCUnkShort14, + + [RecordData(16, 2)] NPCMinDam, + [RecordData(18, 2)] NPCMaxDam, + [RecordData(20, 2)] NPCAccuracy, + [RecordData(22, 2)] NPCEvade, + [RecordData(24, 2)] NPCArmor, + [RecordData(26, 1)] NPCUnkByte26, + [RecordData(27, 2)] NPCUnkShort27, + [RecordData(29, 2)] NPCUnkShort29, + + [RecordData(31, 2)] NPCElementWeak, + [RecordData(33, 2)] NPCElementWeakPower, + [RecordData(35, 1)] NPCUnkByte35, - #endregion - - #region Spell Specific + [RecordData(36, 3)] + NPCExp, - SpellShout, + Spell = 0x400, + [RecordData(0, 2)] SpellIcon, + [RecordData(2, 2)] SpellGraphic, + [RecordData(4, 2)] SpellTP, + [RecordData(6, 2)] SpellSP, + [RecordData(8, 1)] SpellCastTime, + [RecordData(9, 1)] SpellUnkByte9, + [RecordData(10, 1)] SpellUnkByte10, + [RecordData(11, 3)] SpellType, + [RecordData(14, 1)] SpellUnkByte14, + [RecordData(15, 2)] SpellUnkShort15, + [RecordData(17, 1)] SpellTargetRestrict, + [RecordData(18, 1)] SpellTarget, + [RecordData(19, 1)] SpellUnkByte19, + [RecordData(20, 1)] SpellUnkByte20, + [RecordData(21, 2)] SpellUnkShort21, + [RecordData(23, 2)] SpellMinDam, + [RecordData(25, 2)] SpellMaxDam, + [RecordData(27, 2)] SpellAccuracy, + [RecordData(29, 2)] SpellUnkShort29, + [RecordData(31, 2)] SpellUnkShort31, + [RecordData(33, 1)] SpellUnkByte33, + [RecordData(34, 2)] SpellHP, + [RecordData(36, 2)] SpellUnkShort36, + [RecordData(38, 1)] SpellUnkByte38, + [RecordData(39, 2)] SpellUnkShort39, + [RecordData(41, 2)] SpellUnkShort41, + [RecordData(43, 2)] SpellUnkShort43, + [RecordData(45, 2)] SpellUnkShort45, + [RecordData(47, 2)] SpellUnkShort47, + [RecordData(49, 2)] SpellUnkShort49, - #endregion - - #region Class Specific + Class = 0x800, + [RecordData(0, 1)] ClassBase, + [RecordData(1, 1)] ClassType, + [RecordData(2, 2)] ClassStr, + [RecordData(4, 2)] ClassInt, + [RecordData(6, 2)] ClassWis, + [RecordData(8, 2)] ClassAgi, + [RecordData(10, 2)] ClassCon, + [RecordData(12, 2)] ClassCha - - #endregion } } diff --git a/EOLib.IO/Pub/RecordData.cs b/EOLib.IO/Pub/RecordData.cs new file mode 100644 index 000000000..e07b4f5c4 --- /dev/null +++ b/EOLib.IO/Pub/RecordData.cs @@ -0,0 +1,46 @@ +namespace EOLib.IO.Pub +{ + public struct RecordData + { + public static RecordData Default { get; } = new RecordData(0, 0, 0); + + public int Offset { get; } + + public int Length { get; } + + public int Value { get; } + + public RecordData(int offset, int length, int value) + : this() + { + Offset = offset; + Length = length; + Value = value; + } + + public override bool Equals(object obj) + { + if (obj == null || obj.GetType() != typeof(RecordData)) + return false; + + RecordData other = (RecordData)obj; + + return Offset == other.Offset && + Length == other.Length && + Value == other.Value; + } + + public override int GetHashCode() + { + var hash = 397 ^ Offset.GetHashCode(); + hash = (hash * 397) ^ Length.GetHashCode(); + hash = (hash * 397) ^ Value.GetHashCode(); + return hash; + } + + public override string ToString() + { + return $"{Offset} {Length} {Value}"; + } + } +} diff --git a/EOLib.IO/Pub/RecordDataAttribute.cs b/EOLib.IO/Pub/RecordDataAttribute.cs new file mode 100644 index 000000000..664604e95 --- /dev/null +++ b/EOLib.IO/Pub/RecordDataAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace EOLib.IO.Pub +{ + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public class RecordDataAttribute : Attribute + { + public int Offset { get; } + + public int Length { get; } + + public RecordDataAttribute(int offset, int length) + { + Offset = offset; + Length = length; + } + } +} diff --git a/EOLib.IO/Services/ClassFileLoadService.cs b/EOLib.IO/Services/ClassFileLoadService.cs index 62e77a17f..952d7489f 100644 --- a/EOLib.IO/Services/ClassFileLoadService.cs +++ b/EOLib.IO/Services/ClassFileLoadService.cs @@ -1,17 +1,18 @@ using System.IO; using AutomaticTypeMapper; using EOLib.IO.Pub; +using EOLib.IO.Services.Serializers; namespace EOLib.IO.Services { [MappedType(BaseType = typeof(IPubLoadService))] public class ClassFileLoadService : IPubLoadService { - private readonly INumberEncoderService _numberEncoderService; + private readonly IPubFileDeserializer _pubFileDeserializer; - public ClassFileLoadService(INumberEncoderService numberEncoderService) + public ClassFileLoadService(IPubFileDeserializer pubFileDeserializer) { - _numberEncoderService = numberEncoderService; + _pubFileDeserializer = pubFileDeserializer; } public IPubFile LoadPubFromDefaultFile() @@ -22,11 +23,7 @@ public IPubFile LoadPubFromDefaultFile() public IPubFile LoadPubFromExplicitFile(string fileName) { var fileBytes = File.ReadAllBytes(fileName); - - var pub = new ECFFile(); - pub.DeserializeFromByteArray(fileBytes, _numberEncoderService); - - return pub; + return _pubFileDeserializer.DeserializeFromByteArray(fileBytes, () => new ECFFile()); } } } diff --git a/EOLib.IO/Services/IPubFileSaveService.cs b/EOLib.IO/Services/IPubFileSaveService.cs index 60afe4897..f0add58e8 100644 --- a/EOLib.IO/Services/IPubFileSaveService.cs +++ b/EOLib.IO/Services/IPubFileSaveService.cs @@ -4,6 +4,7 @@ namespace EOLib.IO.Services { public interface IPubFileSaveService { - void SaveFile(string path, IPubFile pubFile, bool rewriteChecksum = true); + void SaveFile(string path, IPubFile pubFile, bool rewriteChecksum = true) + where TRecord : class, IPubRecord, new(); } } diff --git a/EOLib.IO/Services/IPubLoadService.cs b/EOLib.IO/Services/IPubLoadService.cs index 9839aaf59..774e8b55d 100644 --- a/EOLib.IO/Services/IPubLoadService.cs +++ b/EOLib.IO/Services/IPubLoadService.cs @@ -2,7 +2,7 @@ namespace EOLib.IO.Services { - public interface IPubLoadService + public interface IPubLoadService where T : class, IPubRecord, new() { IPubFile LoadPubFromDefaultFile(); diff --git a/EOLib.IO/Services/ItemFileLoadService.cs b/EOLib.IO/Services/ItemFileLoadService.cs index d6f8653a3..fb21c5385 100644 --- a/EOLib.IO/Services/ItemFileLoadService.cs +++ b/EOLib.IO/Services/ItemFileLoadService.cs @@ -1,17 +1,18 @@ using System.IO; using AutomaticTypeMapper; using EOLib.IO.Pub; +using EOLib.IO.Services.Serializers; namespace EOLib.IO.Services { [MappedType(BaseType = typeof(IPubLoadService))] public class ItemFileLoadService : IPubLoadService { - private readonly INumberEncoderService _numberEncoderService; + private readonly IPubFileDeserializer _pubFileDeserializer; - public ItemFileLoadService(INumberEncoderService numberEncoderService) + public ItemFileLoadService(IPubFileDeserializer pubFileDeserializer) { - _numberEncoderService = numberEncoderService; + _pubFileDeserializer = pubFileDeserializer; } public IPubFile LoadPubFromDefaultFile() @@ -22,11 +23,7 @@ public IPubFile LoadPubFromDefaultFile() public IPubFile LoadPubFromExplicitFile(string fileName) { var fileBytes = File.ReadAllBytes(fileName); - - var pub = new EIFFile(); - pub.DeserializeFromByteArray(fileBytes, _numberEncoderService); - - return pub; + return _pubFileDeserializer.DeserializeFromByteArray(fileBytes, () => new EIFFile()); } } } diff --git a/EOLib.IO/Services/NPCFileLoadService.cs b/EOLib.IO/Services/NPCFileLoadService.cs index deda1aff1..eafdd1303 100644 --- a/EOLib.IO/Services/NPCFileLoadService.cs +++ b/EOLib.IO/Services/NPCFileLoadService.cs @@ -1,17 +1,18 @@ using System.IO; using AutomaticTypeMapper; using EOLib.IO.Pub; +using EOLib.IO.Services.Serializers; namespace EOLib.IO.Services { [MappedType(BaseType = typeof(IPubLoadService))] public class NPCFileLoadService : IPubLoadService { - private readonly INumberEncoderService _numberEncoderService; + private readonly IPubFileDeserializer _pubFileDeserializer; - public NPCFileLoadService(INumberEncoderService numberEncoderService) + public NPCFileLoadService(IPubFileDeserializer pubFileDeserializer) { - _numberEncoderService = numberEncoderService; + _pubFileDeserializer = pubFileDeserializer; } public IPubFile LoadPubFromDefaultFile() @@ -22,11 +23,7 @@ public IPubFile LoadPubFromDefaultFile() public IPubFile LoadPubFromExplicitFile(string fileName) { var fileBytes = File.ReadAllBytes(fileName); - - var pub = new ENFFile(); - pub.DeserializeFromByteArray(fileBytes, _numberEncoderService); - - return pub; + return _pubFileDeserializer.DeserializeFromByteArray(fileBytes, () => new ENFFile()); } } } diff --git a/EOLib.IO/Services/PubFileSaveService.cs b/EOLib.IO/Services/PubFileSaveService.cs index 54590d762..8c5094c37 100644 --- a/EOLib.IO/Services/PubFileSaveService.cs +++ b/EOLib.IO/Services/PubFileSaveService.cs @@ -1,26 +1,28 @@ using System.IO; using AutomaticTypeMapper; using EOLib.IO.Pub; +using EOLib.IO.Services.Serializers; namespace EOLib.IO.Services { [MappedType(BaseType = typeof(IPubFileSaveService))] public class PubFileSaveService : IPubFileSaveService { - private readonly INumberEncoderService _numberEncoderService; + private readonly IPubFileSerializer _pubFileSerializer; - public PubFileSaveService(INumberEncoderService numberEncoderService) + public PubFileSaveService(IPubFileSerializer pubFileSerializer) { - _numberEncoderService = numberEncoderService; + _pubFileSerializer = pubFileSerializer; } - public void SaveFile(string path, IPubFile pubFile, bool rewriteChecksum = true) + public void SaveFile(string path, IPubFile pubFile, bool rewriteChecksum = true) + where TRecord : class, IPubRecord, new() { var directoryName = Path.GetDirectoryName(path) ?? ""; if (!Directory.Exists(directoryName)) Directory.CreateDirectory(directoryName); - var pubFileBytes = pubFile.SerializeToByteArray(_numberEncoderService, rewriteChecksum); + var pubFileBytes = _pubFileSerializer.SerializeToByteArray(pubFile, rewriteChecksum); File.WriteAllBytes(path, pubFileBytes); } } diff --git a/EOLib.IO/Services/Serializers/IPubFileDeserializer.cs b/EOLib.IO/Services/Serializers/IPubFileDeserializer.cs new file mode 100644 index 000000000..bfff0b677 --- /dev/null +++ b/EOLib.IO/Services/Serializers/IPubFileDeserializer.cs @@ -0,0 +1,17 @@ +using EOLib.IO.Pub; +using System; + +namespace EOLib.IO.Services.Serializers +{ + public interface IPubFileDeserializer + { + IPubFile DeserializeFromByteArray(byte[] data, Func> fileFactory) + where TRecord : class, IPubRecord, new(); + } + + public interface IPubFileSerializer : IPubFileDeserializer + { + byte[] SerializeToByteArray(IPubFile file, bool rewriteChecksum = true) + where TRecord : class, IPubRecord, new(); + } +} diff --git a/EOLib.IO/Services/Serializers/IPubRecordSerializer.cs b/EOLib.IO/Services/Serializers/IPubRecordSerializer.cs new file mode 100644 index 000000000..5643e64c9 --- /dev/null +++ b/EOLib.IO/Services/Serializers/IPubRecordSerializer.cs @@ -0,0 +1,12 @@ +using EOLib.IO.Pub; +using System; + +namespace EOLib.IO.Services.Serializers +{ + public interface IPubRecordSerializer + { + IPubRecord DeserializeFromByteArray(byte[] data, Func recordFactory); + + byte[] SerializeToByteArray(IPubRecord record); + } +} diff --git a/EOLib.IO/Services/Serializers/PubFileSerializer.cs b/EOLib.IO/Services/Serializers/PubFileSerializer.cs new file mode 100644 index 000000000..9890097a2 --- /dev/null +++ b/EOLib.IO/Services/Serializers/PubFileSerializer.cs @@ -0,0 +1,99 @@ +using AutomaticTypeMapper; +using EOLib.IO.Pub; +using System; +using System.IO; +using System.Text; + +namespace EOLib.IO.Services.Serializers +{ + [AutoMappedType] + public class PubFileSerializer : IPubFileSerializer + { + private readonly INumberEncoderService _numberEncoderService; + private readonly IPubRecordSerializer _pubRecordSerializer; + + public PubFileSerializer(INumberEncoderService numberEncoderService, + IPubRecordSerializer pubRecordSerializer) + { + _numberEncoderService = numberEncoderService; + _pubRecordSerializer = pubRecordSerializer; + } + + public IPubFile DeserializeFromByteArray(byte[] data, Func> fileFactory) + where TRecord : class, IPubRecord, new() + { + using (var mem = new MemoryStream(data)) + { + mem.Seek(3, SeekOrigin.Begin); + + var checksumBytes = new byte[4]; + mem.Read(checksumBytes, 0, 4); + var checksum = _numberEncoderService.DecodeNumber(checksumBytes); + + var file = (IPubFile)fileFactory().WithCheckSum(checksum); + + var lenBytes = new byte[2]; + mem.Read(lenBytes, 0, 2); + var recordsInFile = (short)_numberEncoderService.DecodeNumber(lenBytes); + + mem.Seek(1, SeekOrigin.Current); + + var dummyRecord = new TRecord(); + var dataSize = dummyRecord.DataSize; + var numberOfNames = dummyRecord.NumberOfNames; + for (int i = 1; i <= recordsInFile && mem.Position < mem.Length; ++i) + { + int nameLength = 0; + for (int nameNdx = 0; nameNdx < numberOfNames; nameNdx++) + nameLength += _numberEncoderService.DecodeNumber((byte)mem.ReadByte()); + mem.Seek(-numberOfNames, SeekOrigin.Current); + + var rawData = new byte[nameLength + numberOfNames + dataSize]; + mem.Read(rawData, 0, rawData.Length); + + var record = _pubRecordSerializer.DeserializeFromByteArray(rawData, () => new TRecord().WithID(i)); + file = file.WithAddedRecord((TRecord)record); + } + + if (recordsInFile != file.Length) + throw new IOException("Mismatch between expected length and actual length!"); + + return file; + } + } + + public byte[] SerializeToByteArray(IPubFile file, bool rewriteChecksum = true) + where TRecord : class, IPubRecord, new() + { + byte[] fileBytes; + + using (var mem = new MemoryStream()) //write to memory so we can get a CRC for the new RID value + { + mem.Write(Encoding.ASCII.GetBytes(file.FileType), 0, 3); + mem.Write(_numberEncoderService.EncodeNumber(0, 2), 0, 2); + mem.Write(_numberEncoderService.EncodeNumber(0, 2), 0, 2); + mem.Write(_numberEncoderService.EncodeNumber(file.Length, 2), 0, 2); + + mem.WriteByte(_numberEncoderService.EncodeNumber(1, 1)[0]); + + foreach (var dataRecord in file) + { + var toWrite = _pubRecordSerializer.SerializeToByteArray(dataRecord); + mem.Write(toWrite, 0, toWrite.Length); + } + + fileBytes = mem.ToArray(); + } + + var checksumBytes = _numberEncoderService.EncodeNumber(file.CheckSum, 4); + if (rewriteChecksum) + { + var checksum = CRC32.Check(fileBytes); + checksumBytes = _numberEncoderService.EncodeNumber((int)checksum, 4); + } + + Array.Copy(checksumBytes, 0, fileBytes, 3, 4); + return fileBytes; + } + } +} diff --git a/EOLib.IO/Services/Serializers/PubRecordSerializer.cs b/EOLib.IO/Services/Serializers/PubRecordSerializer.cs new file mode 100644 index 000000000..c631fd068 --- /dev/null +++ b/EOLib.IO/Services/Serializers/PubRecordSerializer.cs @@ -0,0 +1,78 @@ +using AutomaticTypeMapper; +using EOLib.IO.Pub; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EOLib.IO.Services.Serializers +{ + [AutoMappedType] + public class PubRecordSerializer : IPubRecordSerializer + { + private readonly INumberEncoderService _numberEncoderService; + + public PubRecordSerializer(INumberEncoderService numberEncoderService) + { + _numberEncoderService = numberEncoderService; + } + + public IPubRecord DeserializeFromByteArray(byte[] data, Func recordFactory) + { + var record = recordFactory(); + + var nameLengths = new List(); + for (int i = 0; i < record.NumberOfNames; i++) + nameLengths.Add((byte)_numberEncoderService.DecodeNumber(data[i])); + + int offset = nameLengths.Count; + var names = new List(nameLengths.Count); + foreach (var nameLength in nameLengths) + { + names.Add(Encoding.ASCII.GetString(data, offset, nameLength)); + offset += names.Last().Length; + } + + record = record.WithNames(names); + foreach (var propertyKvp in record.Bag) + { + var property = propertyKvp.Value; + var propertyRawBytes = data.Skip(offset + property.Offset).Take(property.Length).ToArray(); + record = record.WithProperty(propertyKvp.Key, _numberEncoderService.DecodeNumber(propertyRawBytes)); + } + + return record; + } + + public byte[] SerializeToByteArray(IPubRecord record) + { + var retList = new List(); + + foreach (var name in record.Names) + { + var nameLength = _numberEncoderService.EncodeNumber(name.Length, 1)[0]; + retList.Add(nameLength); + } + + foreach (var name in record.Names) + { + var nameBytes = Encoding.ASCII.GetBytes(name); + retList.AddRange(nameBytes); + } + + var distinctOffsets = record.Bag + .GroupBy(x => x.Value.Offset) + .Select(x => new KeyValuePair(x.First().Key, x.First().Value)); + + foreach (var propertyKvp in distinctOffsets) + { + var property = propertyKvp.Value; + var propertyRawBytes = _numberEncoderService.EncodeNumber(property.Value, property.Length); + Array.Resize(ref propertyRawBytes, property.Length); + retList.AddRange(propertyRawBytes); + } + + return retList.ToArray(); + } + } +} diff --git a/EOLib.IO/Services/SpellFileLoadService.cs b/EOLib.IO/Services/SpellFileLoadService.cs index ed071c69e..387f17317 100644 --- a/EOLib.IO/Services/SpellFileLoadService.cs +++ b/EOLib.IO/Services/SpellFileLoadService.cs @@ -1,17 +1,18 @@ using System.IO; using AutomaticTypeMapper; using EOLib.IO.Pub; +using EOLib.IO.Services.Serializers; namespace EOLib.IO.Services { [MappedType(BaseType = typeof(IPubLoadService))] public class SpellFileLoadService : IPubLoadService { - private readonly INumberEncoderService _numberEncoderService; + private readonly IPubFileDeserializer _pubFileDeserializer; - public SpellFileLoadService(INumberEncoderService numberEncoderService) + public SpellFileLoadService(IPubFileDeserializer pubFileDeserializer) { - _numberEncoderService = numberEncoderService; + _pubFileDeserializer = pubFileDeserializer; } public IPubFile LoadPubFromDefaultFile() @@ -22,11 +23,7 @@ public IPubFile LoadPubFromDefaultFile() public IPubFile LoadPubFromExplicitFile(string fileName) { var fileBytes = File.ReadAllBytes(fileName); - - var pub = new ESFFile(); - pub.DeserializeFromByteArray(fileBytes, _numberEncoderService); - - return pub; + return _pubFileDeserializer.DeserializeFromByteArray(fileBytes, () => new ESFFile()); } } }