From 567d733c6b80c431057598421806599b23880e3c Mon Sep 17 00:00:00 2001 From: Jarl Gullberg Date: Wed, 6 Sep 2017 22:01:56 +0200 Subject: [PATCH] Implement reflection-based DBC deserialization, as well as some initial tests. --- libwarcraft.Tests/Content/.gitignore | 3 + libwarcraft.Tests/Content/README.md | 4 + .../Integration/DBC/DBCTestHelper.cs | 68 +++++++++ .../DBC/IO/RecordDeserializationTests.cs | 105 ++++++++++++++ .../DBC/Vanilla/RecordLoadingTests.cs | 132 ++++++++++++++++++ .../Reflection/DBCReflectionTests.cs | 132 ++++++++++++++++++ .../Reflection/InvalidTestDBCRecord.cs | 27 ++++ libwarcraft.Tests/Reflection/TestDBCRecord.cs | 37 +++++ libwarcraft.Tests/libwarcraft.Tests.csproj | 12 +- .../libwarcraft.Tests.csproj.DotSettings | 2 + libwarcraft.sln.DotSettings | 1 + libwarcraft/Core/DatabaseRecordAttribute.cs | 20 +++ libwarcraft/Core/Extensions/ExtendedIO.cs | 56 +++++++- libwarcraft/Core/ForeignKeyInfoAttribute.cs | 18 +++ ...ieldVersion.cs => RecordFieldAttribute.cs} | 23 ++- .../Core/Reflection/DBC/DBCReflection.cs | 125 +++++++++++++++++ .../DBC/Definitions/AnimationDataRecord.cs | 3 - libwarcraft/DBC/Definitions/DBCRecord.cs | 1 + libwarcraft/DBC/Definitions/MapRecord.cs | 74 ++++++++-- libwarcraft/MDX/Animation/MDXTrack.cs | 7 + 20 files changed, 824 insertions(+), 26 deletions(-) create mode 100644 libwarcraft.Tests/Content/.gitignore create mode 100644 libwarcraft.Tests/Content/README.md create mode 100644 libwarcraft.Tests/Integration/DBC/DBCTestHelper.cs create mode 100644 libwarcraft.Tests/Integration/DBC/IO/RecordDeserializationTests.cs create mode 100644 libwarcraft.Tests/Integration/DBC/Vanilla/RecordLoadingTests.cs create mode 100644 libwarcraft.Tests/Reflection/DBCReflectionTests.cs create mode 100644 libwarcraft.Tests/Reflection/InvalidTestDBCRecord.cs create mode 100644 libwarcraft.Tests/Reflection/TestDBCRecord.cs create mode 100644 libwarcraft.Tests/libwarcraft.Tests.csproj.DotSettings create mode 100644 libwarcraft/Core/DatabaseRecordAttribute.cs create mode 100644 libwarcraft/Core/ForeignKeyInfoAttribute.cs rename libwarcraft/Core/{FieldVersion.cs => RecordFieldAttribute.cs} (59%) create mode 100644 libwarcraft/Core/Reflection/DBC/DBCReflection.cs diff --git a/libwarcraft.Tests/Content/.gitignore b/libwarcraft.Tests/Content/.gitignore new file mode 100644 index 0000000..09feb50 --- /dev/null +++ b/libwarcraft.Tests/Content/.gitignore @@ -0,0 +1,3 @@ +*.* +!*.md +!.gitignore \ No newline at end of file diff --git a/libwarcraft.Tests/Content/README.md b/libwarcraft.Tests/Content/README.md new file mode 100644 index 0000000..294f615 --- /dev/null +++ b/libwarcraft.Tests/Content/README.md @@ -0,0 +1,4 @@ +This directory shall contain actual data files that are used to test the compliance of the library. +No files are included, and must be extracted from a legitimately owned and licensed copy of World of Warcraft. + +Each directory in this directory shall map to a member of the `WarcraftVersion` enum. \ No newline at end of file diff --git a/libwarcraft.Tests/Integration/DBC/DBCTestHelper.cs b/libwarcraft.Tests/Integration/DBC/DBCTestHelper.cs new file mode 100644 index 0000000..e3fb52c --- /dev/null +++ b/libwarcraft.Tests/Integration/DBC/DBCTestHelper.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using NUnit.Framework; +using Warcraft.Core; +using Warcraft.DBC; +using Warcraft.DBC.Definitions; + +namespace libwarcraft.Tests.Integration.DBC +{ + public static class DBCTestHelper + { + public static DBC LoadDatabase(WarcraftVersion version, DatabaseName databaseName) where T : DBCRecord, new() + { + return new DBC(version, GetDatabaseBytes(version, databaseName)); + } + + public static bool HasDatabaseFile(WarcraftVersion version, DatabaseName databaseName) + { + return File.Exists(GetDatabaseFilePath(version, databaseName)); + } + + public static byte[] GetDatabaseBytes(WarcraftVersion version, DatabaseName databaseName) + { + return File.ReadAllBytes(GetDatabaseFilePath(version, databaseName)); + } + + private static string GetDatabaseFilePath(WarcraftVersion version, DatabaseName databaseName) + { + var path = Path.Combine + ( + TestContext.CurrentContext.WorkDirectory, + "Content", + version.ToString(), + "DBFilesClient", + $"{databaseName}.dbc" + ); + + return path; + } + + /// + /// Converts a database name into a qualified type. + /// + /// The enumerated name of the database, + /// The type mapping to the database name. + public static Type GetRecordTypeFromDatabaseName(DatabaseName databaseName) + { + return Type.GetType($"Warcraft.DBC.Definitions.{databaseName}Record, libwarcraft"); + } + + /// + /// Gets the database name from a type. + /// + /// The type of the record. + /// The enumerated database name. + /// Thrown if the given type can't be resolved to a database name. + public static DatabaseName GetDatabaseNameFromRecordType(Type recordType) + { + string recordName = recordType.Name.Replace("Record", string.Empty); + if (Enum.TryParse(recordName, true, out DatabaseName databaseName)) + { + return databaseName; + } + + throw new ArgumentException("The given type could not be resolved to a database name.", nameof(recordType)); + } + } +} diff --git a/libwarcraft.Tests/Integration/DBC/IO/RecordDeserializationTests.cs b/libwarcraft.Tests/Integration/DBC/IO/RecordDeserializationTests.cs new file mode 100644 index 0000000..b6d31c6 --- /dev/null +++ b/libwarcraft.Tests/Integration/DBC/IO/RecordDeserializationTests.cs @@ -0,0 +1,105 @@ +using System.IO; +using libwarcraft.Tests.Reflection; +using NUnit.Framework; +using Warcraft.Core; +using Warcraft.Core.Extensions; +using Warcraft.Core.Reflection.DBC; + +namespace libwarcraft.Tests.Integration.DBC.IO +{ + [TestFixture] + public class RecordDeserializationTests + { + private readonly byte[] TestDBCRecordBytesClassic = + { + 1, 0, 0, 0, // ID + 1, 0, 0, 0, // TestSimpleField + 2, 0, 0, 0, // TestAddedAndRemovedField + 6, 0, 0, 0, // TestForeignKeyField + }; + + private readonly byte[] TestDBCRecordBytesWrath = + { + 1, 0, 0, 0, // ID + 1, 0, 0, 0, // TestSimpleField + 2, 0, 0, 0, // TestAddedAndRemovedField + 6, 0, 0, 0, // TestForeignKeyField + 4, 0, 0, 0, // TestNewFieldInWrath + }; + + private readonly byte[] TestDBCRecordBytesCata = + { + 1, 0, 0, 0, // ID + 1, 0, 0, 0, // TestSimpleField + 6, 0, 0, 0, // TestForeignKeyField + 4, 0, 0, 0, // TestNewFieldInWrath + }; + + [Test] + public void DeserializingTestDBCRecordSetsThePropertiesToTheCorrectValues() + { + var testVersion = WarcraftVersion.Classic; + + TestDBCRecord record = new TestDBCRecord(); + record.Version = testVersion; + + using (var ms = new MemoryStream(this.TestDBCRecordBytesClassic)) + { + using (var br = new BinaryReader(ms)) + { + DBCReflection.DeserializeRecord(br, record, testVersion); + } + } + + Assert.AreEqual(1, record.ID); + Assert.AreEqual(1, record.TestSimpleField); + Assert.AreEqual(2, record.TestAddedAndRemovedField); + Assert.AreEqual(6, record.TestForeignKeyField.Key); + } + + [Test] + public void DeserializingTestDBCRecordSetsThePropertiesToTheCorrectValuesForAddedFields() + { + var testVersion = WarcraftVersion.Wrath; + + TestDBCRecord record = new TestDBCRecord(); + record.Version = testVersion; + + using (var ms = new MemoryStream(this.TestDBCRecordBytesWrath)) + { + using (var br = new BinaryReader(ms)) + { + DBCReflection.DeserializeRecord(br, record, testVersion); + } + } + + Assert.AreEqual(1, record.ID); + Assert.AreEqual(1, record.TestSimpleField); + Assert.AreEqual(2, record.TestAddedAndRemovedField); + Assert.AreEqual(6, record.TestForeignKeyField.Key); + Assert.AreEqual(4, record.TestNewFieldInWrath); + } + + [Test] + public void DeserializingTestDBCRecordSetsThePropertiesToTheCorrectValuesForRemovedFields() + { + var testVersion = WarcraftVersion.Cataclysm; + + TestDBCRecord record = new TestDBCRecord(); + record.Version = testVersion; + + using (var ms = new MemoryStream(this.TestDBCRecordBytesCata)) + { + using (var br = new BinaryReader(ms)) + { + DBCReflection.DeserializeRecord(br, record, testVersion); + } + } + + Assert.AreEqual(1, record.ID); + Assert.AreEqual(1, record.TestSimpleField); + Assert.AreEqual(6, record.TestForeignKeyField.Key); + Assert.AreEqual(4, record.TestNewFieldInWrath); + } + } +} diff --git a/libwarcraft.Tests/Integration/DBC/Vanilla/RecordLoadingTests.cs b/libwarcraft.Tests/Integration/DBC/Vanilla/RecordLoadingTests.cs new file mode 100644 index 0000000..bcf3b62 --- /dev/null +++ b/libwarcraft.Tests/Integration/DBC/Vanilla/RecordLoadingTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Linq; +using NUnit.Framework; +using Warcraft.Core; +using Warcraft.DBC; +using Warcraft.DBC.Definitions; + +namespace libwarcraft.Tests.Integration.DBC.Vanilla +{ + [TestFixture] + public class RecordLoadingTests + { + private const WarcraftVersion Version = WarcraftVersion.Classic; + + private static void TestLoadRecord(WarcraftVersion version) where T : DBCRecord, new() + { + var databaseName = DBCTestHelper.GetDatabaseNameFromRecordType(typeof(T)); + if (!DBCTestHelper.HasDatabaseFile(version, databaseName)) + { + Assert.Ignore("Database file not present. Skipping."); + } + + var database = DBCTestHelper.LoadDatabase(version, databaseName); + + try + { + database.First(); + } + catch (ArgumentException e) + { + Assert.Fail($"Failed to read {databaseName}: {e}"); + } + } + + [Test] + public void TestLoadAnimationDataRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadCharHairGeosetsRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadSoundAmbienceRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadCharSectionsRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadSoundEntriesRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadCreatureDisplayInfoExtraRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadSoundProviderPreferences() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadCreatureDisplayInfoRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadSpellRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadCreatureModelDataRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadWMOAreaTableRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadLiquidObjectRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadZoneIntroMusicTableRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadLiquidTypeRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadZoneMusicRecord() + { + TestLoadRecord(Version); + } + + [Test] + public void TestLoadMapRecord() + { + TestLoadRecord(Version); + } + + } +} diff --git a/libwarcraft.Tests/Reflection/DBCReflectionTests.cs b/libwarcraft.Tests/Reflection/DBCReflectionTests.cs new file mode 100644 index 0000000..91d8f68 --- /dev/null +++ b/libwarcraft.Tests/Reflection/DBCReflectionTests.cs @@ -0,0 +1,132 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using NUnit.Framework; +using Warcraft.Core; +using Warcraft.Core.Reflection.DBC; +using Warcraft.DBC; +using Warcraft.DBC.Definitions; + +namespace libwarcraft.Tests.Reflection +{ + [TestFixture] + public class DBCReflectionTests + { + private readonly string[] TestRecordPropertyNames = + { + nameof(DBCRecord.ID), + nameof(TestDBCRecord.TestSimpleField), + nameof(TestDBCRecord.TestAddedAndRemovedField), + nameof(TestDBCRecord.TestForeignKeyField), + nameof(TestDBCRecord.TestNewFieldInWrath), + }; + + private readonly string[] TestRecordClassicPropertyNames = + { + nameof(DBCRecord.ID), + nameof(TestDBCRecord.TestSimpleField), + nameof(TestDBCRecord.TestAddedAndRemovedField), + nameof(TestDBCRecord.TestForeignKeyField), + }; + + private readonly string[] TestRecordCataPropertyNames = + { + nameof(DBCRecord.ID), + nameof(TestDBCRecord.TestSimpleField), + nameof(TestDBCRecord.TestForeignKeyField), + nameof(TestDBCRecord.TestNewFieldInWrath), + }; + + [Test] + public void GetRecordPropertiesGetsAValidPropertySet() + { + var recordProperties = DBCReflection.GetRecordProperties(); + + var recordPropertyNames = recordProperties.Select(p => p.Name); + + Assert.That(recordPropertyNames, Is.EquivalentTo(this.TestRecordPropertyNames)); + } + + [Test] + public void GetVersionRelevantPropertiesGetsAValidVersionedPropertySetForAddedProperties() + { + var recordProperties = DBCReflection.GetVersionRelevantProperties(WarcraftVersion.Classic); + + var recordPropertyNames = recordProperties.Select(p => p.Name); + + Assert.That(recordPropertyNames, Is.EquivalentTo(this.TestRecordClassicPropertyNames)); + } + + [Test] + public void GetVersionRelevantPropertiesGetsAValidVersionedPropertySetForRemovedProperties() + { + var recordProperties = DBCReflection.GetVersionRelevantProperties(WarcraftVersion.Cataclysm); + + var recordPropertyNames = recordProperties.Select(p => p.Name); + + Assert.That(recordPropertyNames, Is.EquivalentTo(this.TestRecordCataPropertyNames)); + } + + [Test] + public void IsPropertyForeignKeyReturnsTrueForForeignKeyPropertiesAndViceVersa() + { + var foreignKeyProperty = typeof(TestDBCRecord).GetProperties() + .First(p => p.Name == nameof(TestDBCRecord.TestForeignKeyField)); + + var otherProperty = typeof(TestDBCRecord).GetProperties() + .First(p => p.Name == nameof(TestDBCRecord.TestSimpleField)); + + Assert.True(DBCReflection.IsPropertyForeignKey(foreignKeyProperty)); + Assert.False(DBCReflection.IsPropertyForeignKey(otherProperty)); + } + + [Test] + public void GetForeignKeyInfoOnAValidForeignKeyPropertyReturnsValidData() + { + var foreignKeyProperty = typeof(TestDBCRecord).GetProperties() + .First(p => p.Name == nameof(TestDBCRecord.TestForeignKeyField)); + + var foreignKeyInfo = DBCReflection.GetForeignKeyInfo(foreignKeyProperty); + + Assert.AreEqual(DatabaseName.AnimationData, foreignKeyInfo.Database); + Assert.AreEqual(nameof(AnimationDataRecord.ID), foreignKeyInfo.Field); + } + + [Test] + public void GetForeignKeyInfoOnAPropertyThatIsNotAForeignKeyThrows() + { + var otherProperty = typeof(TestDBCRecord).GetProperties() + .First(p => p.Name == nameof(TestDBCRecord.TestSimpleField)); + + Assert.Throws(() => DBCReflection.GetForeignKeyInfo(otherProperty)); + } + + [Test] + public void GetForeignKeyInfoOnAPropertyWithoutTheForeignKeyInfoAttributeThrows() + { + var invalidForeignKeyProperty = typeof(InvalidTestDBCRecord).GetProperties() + .First(p => p.Name == nameof(InvalidTestDBCRecord.TestForeignKeyFieldMissingInfo)); + + Assert.Throws(() => DBCReflection.GetForeignKeyInfo(invalidForeignKeyProperty)); + } + + [Test] + public void GetForeignKeyTypeOnAPropertyThatIsNotAForeignKeyThrows() + { + var otherProperty = typeof(TestDBCRecord).GetProperties() + .First(p => p.Name == nameof(TestDBCRecord.TestSimpleField)); + + Assert.Throws(() => DBCReflection.GetForeignKeyType(otherProperty)); + } + + [Test] + public void GetForeignKeyTypeOnAForeignKeyReturnsCorrectType() + { + var foreignKeyProperty = typeof(TestDBCRecord).GetProperties() + .First(p => p.Name == nameof(TestDBCRecord.TestForeignKeyField)); + + Assert.AreEqual(typeof(uint), DBCReflection.GetForeignKeyType(foreignKeyProperty)); + } + } +} diff --git a/libwarcraft.Tests/Reflection/InvalidTestDBCRecord.cs b/libwarcraft.Tests/Reflection/InvalidTestDBCRecord.cs new file mode 100644 index 0000000..8320f5a --- /dev/null +++ b/libwarcraft.Tests/Reflection/InvalidTestDBCRecord.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Warcraft.Core; +using Warcraft.DBC.Definitions; +using Warcraft.DBC.SpecialFields; + +namespace libwarcraft.Tests.Reflection +{ + [DatabaseRecord] + public class InvalidTestDBCRecord : DBCRecord + { + [RecordField(WarcraftVersion.Classic, 1)] + public ForeignKey TestForeignKeyFieldMissingInfo { get; set; } + + [RecordField(WarcraftVersion.Classic, 2)] + public uint TestFieldWithoutSetter { get; } + + public override void PostLoad(byte[] data) + { + throw new System.NotImplementedException(); + } + + public override IEnumerable GetStringReferences() + { + throw new System.NotImplementedException(); + } + } +} diff --git a/libwarcraft.Tests/Reflection/TestDBCRecord.cs b/libwarcraft.Tests/Reflection/TestDBCRecord.cs new file mode 100644 index 0000000..99f56a8 --- /dev/null +++ b/libwarcraft.Tests/Reflection/TestDBCRecord.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Warcraft.Core; +using Warcraft.DBC; +using Warcraft.DBC.Definitions; +using Warcraft.DBC.SpecialFields; + +namespace libwarcraft.Tests.Reflection +{ + [DatabaseRecord] + public class TestDBCRecord : DBCRecord + { + public uint TestNotRecordField { get; } + + [RecordField(WarcraftVersion.Classic, 1)] + public uint TestSimpleField { get; set; } + + [RecordField(WarcraftVersion.Classic, WarcraftVersion.Cataclysm, 2)] + public uint TestAddedAndRemovedField { get; set; } + + [RecordField(WarcraftVersion.Classic, 3)] + [ForeignKeyInfo(DatabaseName.AnimationData, nameof(AnimationDataRecord.ID))] + public ForeignKey TestForeignKeyField { get; set; } + + [RecordField(WarcraftVersion.Wrath, 4)] + public uint TestNewFieldInWrath { get; set; } + + public override void PostLoad(byte[] data) + { + throw new System.NotImplementedException(); + } + + public override IEnumerable GetStringReferences() + { + throw new System.NotImplementedException(); + } + } +} diff --git a/libwarcraft.Tests/libwarcraft.Tests.csproj b/libwarcraft.Tests/libwarcraft.Tests.csproj index ca110f7..33d92a9 100644 --- a/libwarcraft.Tests/libwarcraft.Tests.csproj +++ b/libwarcraft.Tests/libwarcraft.Tests.csproj @@ -1,14 +1,22 @@  - netstandard2.0 + netcoreapp2.0 + Exe false - + + + + + + PreserveNewest + + \ No newline at end of file diff --git a/libwarcraft.Tests/libwarcraft.Tests.csproj.DotSettings b/libwarcraft.Tests/libwarcraft.Tests.csproj.DotSettings new file mode 100644 index 0000000..b0096de --- /dev/null +++ b/libwarcraft.Tests/libwarcraft.Tests.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/libwarcraft.sln.DotSettings b/libwarcraft.sln.DotSettings index 7bb3495..b0476bd 100644 --- a/libwarcraft.sln.DotSettings +++ b/libwarcraft.sln.DotSettings @@ -56,6 +56,7 @@ WMO <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True True True True \ No newline at end of file diff --git a/libwarcraft/Core/DatabaseRecordAttribute.cs b/libwarcraft/Core/DatabaseRecordAttribute.cs new file mode 100644 index 0000000..1b96e2d --- /dev/null +++ b/libwarcraft/Core/DatabaseRecordAttribute.cs @@ -0,0 +1,20 @@ +using System; +using Warcraft.DBC; + +namespace Warcraft.Core +{ + public class DatabaseRecordAttribute : Attribute + { + public DatabaseName Database { get; } + + public DatabaseRecordAttribute() + { + + } + + public DatabaseRecordAttribute(DatabaseName database) + { + this.Database = database; + } + } +} diff --git a/libwarcraft/Core/Extensions/ExtendedIO.cs b/libwarcraft/Core/Extensions/ExtendedIO.cs index b6920da..96a9bb2 100644 --- a/libwarcraft/Core/Extensions/ExtendedIO.cs +++ b/libwarcraft/Core/Extensions/ExtendedIO.cs @@ -23,9 +23,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Numerics; +using System.Reflection; using System.Text; using Warcraft.Core.Interfaces; +using Warcraft.Core.Reflection.DBC; using Warcraft.Core.Shading.Blending; using Warcraft.Core.Structures; using Warcraft.DBC; @@ -201,7 +204,27 @@ public static T Read(this BinaryReader br) "with it.", typeof(T).Name); } - return TypeReaderMap[typeof(T)](br); + return Read(br, typeof(T)); + } + + /// + /// Reads a value of type from the data stream. The generic type must be + /// explicitly implemented in . Note that strings are read as C-style null-terminated + /// strings, and not C#-style length-prefixed strings. + /// + /// + /// + /// + /// + public static dynamic Read(this BinaryReader br, Type type) + { + if (!TypeReaderMap.ContainsKey(type)) + { + throw new ArgumentException("The given type has no supported reading function associated " + + "with it.", type.Name); + } + + return TypeReaderMap[type](br); } /// @@ -220,7 +243,27 @@ public static T Read(this BinaryReader br, WarcraftVersion version) "with it.", typeof(T).Name); } - return VersionedTypeReaderMap[typeof(T)](br, version); + return Read(br, typeof(T), version); + } + + /// + /// Reads a versioned value of type from the data stream. The generic type must be + /// explicitly implemented in . Note that strings are read as C-style null-terminated + /// strings, and not C#-style length-prefixed strings. + /// + /// + /// + /// + /// + public static dynamic Read(this BinaryReader br, Type type, WarcraftVersion version) + { + if (!TypeReaderMap.ContainsKey(type)) + { + throw new ArgumentException("The given type has no supported reading function associated " + + "with it.", type.Name); + } + + return VersionedTypeReaderMap[type](br, version); } /* @@ -463,7 +506,14 @@ public static T ReadRecord(this BinaryReader reader, int fieldCount, int reco } } - record.DeserializeSelf(reader); + if (typeof(T).GetCustomAttributes().Any(a => a is DatabaseRecordAttribute)) + { + DBCReflection.DeserializeRecord(reader, record, version); + } + else + { + record.DeserializeSelf(reader); + } return record; } diff --git a/libwarcraft/Core/ForeignKeyInfoAttribute.cs b/libwarcraft/Core/ForeignKeyInfoAttribute.cs new file mode 100644 index 0000000..ce13d0c --- /dev/null +++ b/libwarcraft/Core/ForeignKeyInfoAttribute.cs @@ -0,0 +1,18 @@ +using System; +using Warcraft.DBC; + +namespace Warcraft.Core +{ + [AttributeUsage(AttributeTargets.Property)] + public class ForeignKeyInfoAttribute : Attribute + { + public DatabaseName Database { get; } + public string Field { get; } + + public ForeignKeyInfoAttribute(DatabaseName databaseName, string field) + { + this.Database = databaseName; + this.Field = field; + } + } +} diff --git a/libwarcraft/Core/FieldVersion.cs b/libwarcraft/Core/RecordFieldAttribute.cs similarity index 59% rename from libwarcraft/Core/FieldVersion.cs rename to libwarcraft/Core/RecordFieldAttribute.cs index c40d22d..5b1c792 100644 --- a/libwarcraft/Core/FieldVersion.cs +++ b/libwarcraft/Core/RecordFieldAttribute.cs @@ -1,5 +1,5 @@ // -// FieldVersionAttribute.cs +// RecordField.cs // // Author: // Jarl Gullberg @@ -23,17 +23,26 @@ namespace Warcraft.Core { - [AttributeUsage(AttributeTargets.Field)] - public class FieldVersion : Attribute + [AttributeUsage(AttributeTargets.Property)] + public class RecordFieldAttribute : Attribute { - public WarcraftVersion Version + public WarcraftVersion IntroducedIn { get; } + + public WarcraftVersion? RemovedIn { get; } + + public uint Order { get; } + + public RecordFieldAttribute(WarcraftVersion introducedIn, WarcraftVersion removedIn, uint fieldOrder) { - get; + this.IntroducedIn = introducedIn; + this.RemovedIn = removedIn; + this.Order = fieldOrder; } - public FieldVersion(WarcraftVersion inVersion) + public RecordFieldAttribute(WarcraftVersion inIntroducedIn, uint fieldOrder) { - this.Version = inVersion; + this.IntroducedIn = inIntroducedIn; + this.Order = fieldOrder; } } } diff --git a/libwarcraft/Core/Reflection/DBC/DBCReflection.cs b/libwarcraft/Core/Reflection/DBC/DBCReflection.cs new file mode 100644 index 0000000..ebdc556 --- /dev/null +++ b/libwarcraft/Core/Reflection/DBC/DBCReflection.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Warcraft.Core.Extensions; +using Warcraft.DBC.SpecialFields; + +namespace Warcraft.Core.Reflection.DBC +{ + public static class DBCReflection + { + public static void DeserializeRecord(BinaryReader reader, T record, WarcraftVersion version) + { + var databaseProperties = GetVersionRelevantProperties(version); + foreach (var databaseProperty in databaseProperties) + { + if (!databaseProperty.CanWrite) + { + throw new ArgumentException("Property setter not found. Record properties must have a setter."); + } + + object fieldValue; + var fieldType = databaseProperty.PropertyType; + if (IsPropertyForeignKey(databaseProperty)) + { + // Get the foreign key information + var foreignKeyAttribute = GetForeignKeyInfo(databaseProperty); + + // Get the inner type + var keyType = GetForeignKeyType(databaseProperty); + var keyValue = reader.Read(keyType); + + // Create the specific ForeignKey type + var genericKeyFieldType = typeof(ForeignKey<>); + var specificKeyFieldType = genericKeyFieldType.MakeGenericType(keyType); + + fieldValue = Activator.CreateInstance + ( + specificKeyFieldType, + foreignKeyAttribute.Database, + foreignKeyAttribute.Field, + keyValue + ); + } + else + { + fieldValue = reader.Read(fieldType); + } + + databaseProperty.SetValue(record, fieldValue); + } + } + + public static IEnumerable GetRecordProperties() + { + return typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy).Where(p => p.IsDefined(typeof(RecordFieldAttribute))); + } + + public static IEnumerable GetVersionRelevantProperties(WarcraftVersion version) + { + // Order the properties by their field order. + var orderedProperties = GetRecordProperties().OrderBy( + p => (p.GetCustomAttributes().First(a => a is RecordFieldAttribute) as RecordFieldAttribute)?.Order); + + foreach (var recordProperty in orderedProperties) + { + var versionAttribute = recordProperty.GetCustomAttributes().First(a => a is RecordFieldAttribute) as RecordFieldAttribute; + + if (versionAttribute == null) + { + throw new InvalidDataException("Somehow, a property had a version attribute defined but did not actually have one. Call a priest."); + } + + // Field is not present in the version we're reading, skip it + if (versionAttribute.IntroducedIn > version) + { + continue; + } + + // Field has been removed in the version we're reading, skip it + if (versionAttribute.RemovedIn.HasValue && versionAttribute.RemovedIn.Value <= version) + { + continue; + } + + yield return recordProperty; + } + } + + public static bool IsPropertyForeignKey(PropertyInfo propertyInfo) + { + return + propertyInfo.PropertyType.IsGenericType && + propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ForeignKey<>); + } + + public static ForeignKeyInfoAttribute GetForeignKeyInfo(PropertyInfo foreignKeyInfo) + { + if (!IsPropertyForeignKey(foreignKeyInfo)) + { + throw new ArgumentException("The given property was not a foreign key.", nameof(foreignKeyInfo)); + } + + var foreignKeyAttribute = foreignKeyInfo.GetCustomAttributes().FirstOrDefault(a => a is ForeignKeyInfoAttribute) as ForeignKeyInfoAttribute; + + if (foreignKeyAttribute == null) + { + throw new InvalidDataException("ForeignKey properties must be decorated with the ForeignKeyInfo attribute."); + } + + return foreignKeyAttribute; + } + + public static Type GetForeignKeyType(PropertyInfo foreignKeyInfo) + { + if (!IsPropertyForeignKey(foreignKeyInfo)) + { + throw new ArgumentException("The given property was not a foreign key.", nameof(foreignKeyInfo)); + } + + return foreignKeyInfo.PropertyType.GetGenericArguments().First(); + } + } +} diff --git a/libwarcraft/DBC/Definitions/AnimationDataRecord.cs b/libwarcraft/DBC/Definitions/AnimationDataRecord.cs index c071f9e..b9adb63 100644 --- a/libwarcraft/DBC/Definitions/AnimationDataRecord.cs +++ b/libwarcraft/DBC/Definitions/AnimationDataRecord.cs @@ -44,13 +44,11 @@ public class AnimationDataRecord : DBCRecord /// /// The weapon flags. This affects how the model's weapons are held during the animation. /// - [FieldVersion(WarcraftVersion.Warlords)] public WeaponAnimationFlags WeaponFlags; /// /// The body flags. /// - [FieldVersion(WarcraftVersion.Warlords)] public uint BodyFlags; /// @@ -72,7 +70,6 @@ public class AnimationDataRecord : DBCRecord /// The behaviour tier of the animation. In most cases, this indicates whether or not the animation /// is used for flying characters. /// - [FieldVersion(WarcraftVersion.Wrath)] public uint BehaviourTier; /// diff --git a/libwarcraft/DBC/Definitions/DBCRecord.cs b/libwarcraft/DBC/Definitions/DBCRecord.cs index e7276ad..4a8dca0 100644 --- a/libwarcraft/DBC/Definitions/DBCRecord.cs +++ b/libwarcraft/DBC/Definitions/DBCRecord.cs @@ -37,6 +37,7 @@ public abstract class DBCRecord : IPostLoad, IDBCRecord, IDeferredDeseri /// /// The record ID. This is the equivalent of a primary key in an SQL database, and is unique to the record. /// + [RecordField(WarcraftVersion.Classic, 0)] public uint ID { get; diff --git a/libwarcraft/DBC/Definitions/MapRecord.cs b/libwarcraft/DBC/Definitions/MapRecord.cs index be18b1d..63642bd 100644 --- a/libwarcraft/DBC/Definitions/MapRecord.cs +++ b/libwarcraft/DBC/Definitions/MapRecord.cs @@ -23,6 +23,8 @@ using System; using System.Collections.Generic; using System.IO; +using Warcraft.Core; +using Warcraft.Core.Extensions; using Warcraft.DBC.SpecialFields; namespace Warcraft.DBC.Definitions @@ -121,10 +123,7 @@ public class MapRecord : DBCRecord /// public uint Unknown5; - /// - /// Loads and parses the provided data. - /// - /// ExtendedData. + /// public override void PostLoad(byte[] data) { using (MemoryStream ms = new MemoryStream(data)) @@ -136,18 +135,33 @@ public override void PostLoad(byte[] data) } } - /// - /// Deserializes the data of the object using the provided . - /// - /// + /// public override void DeserializeSelf(BinaryReader reader) { base.DeserializeSelf(reader); - throw new NotImplementedException(); + this.Directory = reader.ReadStringReference(); + this.InstanceType = reader.ReadUInt32(); + this.PvP = reader.ReadUInt32(); + this.MapName = reader.ReadLocalizedStringReference(this.Version); + this.MinLevel = reader.ReadUInt32(); + this.MaxLevel = reader.ReadUInt32(); + this.MaxPlayers = reader.ReadUInt32(); + this.Unknown1 = reader.ReadUInt32(); + this.Unknown2 = reader.ReadUInt32(); + this.Unknown3 = reader.ReadUInt32(); + this.AreaTableID = new ForeignKey(DatabaseName.AreaTable, nameof(DBCRecord.ID), reader.ReadUInt32()); + this.MapDescription1 = reader.ReadLocalizedStringReference(this.Version); + this.MapDescription2 = reader.ReadLocalizedStringReference(this.Version); + this.LoadingScreenID = new ForeignKey(DatabaseName.LoadingScreens, nameof(DBCRecord.ID), reader.ReadUInt32()); + this.RaidOffset = reader.ReadUInt32(); + this.Unknown4 = reader.ReadUInt32(); + this.Unknown5 = reader.ReadUInt32(); + this.HasLoadedRecordData = true; } + /// public override IEnumerable GetStringReferences() { yield return this.Directory; @@ -168,8 +182,46 @@ public override IEnumerable GetStringReferences() } } - public override int FieldCount => throw new System.NotImplementedException(); + /// + public override int FieldCount + { + get + { + switch (this.Version) + { + case WarcraftVersion.Classic: + { + var normalFieldCount = 15; + var localizedFieldCount = LocalizedStringReference.GetFieldCount(this.Version) * 3; + + return normalFieldCount + localizedFieldCount; + + } + default: + { + throw new NotImplementedException(); + } + } + } + } - public override int RecordSize => throw new System.NotImplementedException(); + /// + public override int RecordSize + { + get + { + switch (this.Version) + { + case WarcraftVersion.Classic: + { + return this.FieldCount * sizeof(uint); + } + default: + { + throw new NotImplementedException(); + } + } + } + } } } \ No newline at end of file diff --git a/libwarcraft/MDX/Animation/MDXTrack.cs b/libwarcraft/MDX/Animation/MDXTrack.cs index ec08c49..be0a7b4 100644 --- a/libwarcraft/MDX/Animation/MDXTrack.cs +++ b/libwarcraft/MDX/Animation/MDXTrack.cs @@ -33,6 +33,11 @@ namespace Warcraft.MDX.Animation { public class MDXTrack : IVersionedClass { + /// + /// Gets a value indicating whether the timelines are as one composite timeline, or as separate timelines. + /// + public bool IsComposite { get; } + public InterpolationType Interpolationtype; public ushort GlobalSequenceID; @@ -67,6 +72,7 @@ public MDXTrack(BinaryReader br, WarcraftVersion version, bool valueless = false if (version < WarcraftVersion.Wrath) { + this.IsComposite = true; this.CompositeTimelineInterpolationRanges = br.ReadMDXArray(); this.CompositeTimelineTimestamps = br.ReadMDXArray(); @@ -87,6 +93,7 @@ public MDXTrack(BinaryReader br, WarcraftVersion version, bool valueless = false } else { + this.IsComposite = false; this.Timestamps = br.ReadMDXArray>(); if (valueless)