diff --git a/.gitignore b/.gitignore index 2b67f85..2c557a1 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,6 @@ MigrationBackup/ # Private Providers DBCD/Providers/MirrorDBCProvider.cs + +# Rider +.idea/ diff --git a/DBCD.IO/Attributes/EnumAttribute.cs b/DBCD.IO/Attributes/EnumAttribute.cs new file mode 100644 index 0000000..849996d --- /dev/null +++ b/DBCD.IO/Attributes/EnumAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace DBCD.IO.Attributes +{ + [AttributeUsage(AttributeTargets.Field)] + public class EnumAttribute(string enumName, bool isFlags) : Attribute + { + public readonly string EnumName = enumName; + public readonly bool IsFlags = isFlags; + } +} diff --git a/DBCD.Tests/ReadingTest.cs b/DBCD.Tests/ReadingTest.cs index 92ac8a7..de90604 100644 --- a/DBCD.Tests/ReadingTest.cs +++ b/DBCD.Tests/ReadingTest.cs @@ -1,6 +1,7 @@ using DBCD.Providers; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Collections.Generic; using System.IO; namespace DBCD.Tests @@ -208,7 +209,6 @@ public void TestReadingAllDB2s() // System.Console.WriteLine($"B: {countBefore} => A: {countAfter}"); //} - //[TestMethod] //public void TestFilesystemDBDProvider() //{ @@ -217,5 +217,30 @@ public void TestReadingAllDB2s() // // Spell is present in Classic Era -> Retail: https://www.wowhead.com/spell=17/ // Assert.AreEqual("Power Word: Shield", storage[17]["Name_lang"]); //} + + [TestMethod] + public void TestEnumReadingSingle() + { + var dbcd = new DBCD(wagoDBCProvider, githubDBDProvider, new GithubEnumProvider(useCache: true)); + + var storage = dbcd.Load("SpellEffect", "12.0.1.66220"); + var spellEffectRow = storage[1177101]; + Assert.AreEqual(spellEffectRow.IsEnumMember("Effect", "SET_PLAYER_DATA_ELEMENT_ACCOUNT"), true); + } + + [TestMethod] + public void TestEnumReadingArray() + { + var dbcd = new DBCD(wagoDBCProvider, githubDBDProvider, new GithubEnumProvider(useCache: true)); + + var storage = dbcd.Load("SpellMisc", "12.0.1.66220"); + var spellMiscRow = storage[66253]; + + Assert.AreEqual(spellMiscRow.HasFlag("Attributes", 0, "ON_NEXT_SWING"), false); + Assert.AreEqual(spellMiscRow.HasFlag("Attributes", 0, "HIDDEN_CLIENTSIDE"), true); + + // Throws an exception because the Enum Member is not in the Enum + Assert.ThrowsException(() => spellMiscRow.HasFlag("Attributes", 1, "HIDDEN_CLIENTSIDE")); + } } } diff --git a/DBCD/DBCD.cs b/DBCD/DBCD.cs index 118c326..f7a9d61 100644 --- a/DBCD/DBCD.cs +++ b/DBCD/DBCD.cs @@ -4,27 +4,30 @@ using System; using System.Collections.Generic; using System.IO; +using DBDefsLib.Structs; namespace DBCD { - public class DBCD { private readonly IDBCProvider dbcProvider; private readonly IDBDProvider dbdProvider; + private readonly IEnumProvider enumProvider; private readonly bool useBDBD; - private readonly Dictionary BDBDCache; + private readonly Dictionary BDBDTableCache; /// /// Creates a DBCD instance that uses the given DBC and DBD providers. /// /// The IDBCProvider for DBC files. /// The IDBDProvider for DBD files. - public DBCD(IDBCProvider dbcProvider, IDBDProvider dbdProvider) + /// The optional IEnumProvider for enum/flag metadata. + public DBCD(IDBCProvider dbcProvider, IDBDProvider dbdProvider, IEnumProvider enumProvider = null) { this.dbcProvider = dbcProvider; this.dbdProvider = dbdProvider; + this.enumProvider = enumProvider; this.useBDBD = false; } @@ -33,12 +36,16 @@ public DBCD(IDBCProvider dbcProvider, IDBDProvider dbdProvider) /// /// The IDBCProvider for DBC files. /// The stream for a BDBD (Binary DBD) file to load all definitions from. + /// The optional IEnumProvider for enum/flag metadata. /// WARNING: The usage of a BDBD file for supplying definitions is still experimental and currently has little to no advantages. - public DBCD(IDBCProvider dbcProvider, Stream bdbdStream) + public DBCD(IDBCProvider dbcProvider, Stream bdbdStream, IEnumProvider enumProvider = null) { this.dbcProvider = dbcProvider; + this.enumProvider = enumProvider; this.useBDBD = true; - this.BDBDCache = BDBDReader.Read(bdbdStream); + + var bdbd = BDBDReader.Read(bdbdStream); + BDBDTableCache = bdbd.tableDefinitions; } /// @@ -52,7 +59,7 @@ public IDBCDStorage Load(string tableName, string build = null, Locale locale = { var dbcStream = this.dbcProvider.StreamForTableName(tableName, build); - Structs.DBDefinition databaseDefinition; + DBDefinition databaseDefinition; if (!useBDBD) { @@ -62,23 +69,19 @@ public IDBCDStorage Load(string tableName, string build = null, Locale locale = } else { - if (!BDBDCache.TryGetValue(tableName, out var tableInfo)) + if (!BDBDTableCache.TryGetValue(tableName, out var tableInfo)) throw new FileNotFoundException($"Table {tableName} not found in BDBD."); databaseDefinition = tableInfo.dbd; } - var builder = new DBCDBuilder(locale); + var builder = new DBCDBuilder(locale, enumProvider); var dbReader = new DBParser(dbcStream); var definition = builder.Build(dbReader, databaseDefinition, tableName, build); - var type = typeof(DBCDStorage<>).MakeGenericType(definition.Item1); - - return (IDBCDStorage)Activator.CreateInstance(type, new object[2] { - dbReader, - definition.Item2 - }); + var type = typeof(DBCDStorage<>).MakeGenericType(definition.Type); + return (IDBCDStorage)Activator.CreateInstance(type, dbReader, definition.Info); } } @@ -102,4 +105,4 @@ public enum Locale PtBR = PtPT, ItIT = 11, } -} \ No newline at end of file +} diff --git a/DBCD/DBCD.csproj b/DBCD/DBCD.csproj index 8628525..0172015 100644 --- a/DBCD/DBCD.csproj +++ b/DBCD/DBCD.csproj @@ -9,7 +9,7 @@ - + diff --git a/DBCD/DBCDBuilder.cs b/DBCD/DBCDBuilder.cs index 64f7f8e..c6b8efc 100644 --- a/DBCD/DBCDBuilder.cs +++ b/DBCD/DBCDBuilder.cs @@ -1,6 +1,9 @@ using DBDefsLib; +using DBDefsLib.Constants; +using DBDefsLib.Structs; using DBCD.IO; using DBCD.IO.Attributes; +using DBCD.Providers; using System; using System.Collections.Generic; using System.IO; @@ -10,43 +13,43 @@ namespace DBCD { - public struct DBCDInfo { internal string tableName; - internal string[] availableColumns; + internal IReadOnlyDictionary enumTypes; } internal class DBCDBuilder { - private ModuleBuilder moduleBuilder; - private int locStringSize; + private readonly ModuleBuilder moduleBuilder; private readonly Locale locale; + private readonly IEnumProvider enumProvider; + private int locStringSize; - internal DBCDBuilder(Locale locale = Locale.None) + internal DBCDBuilder(Locale locale = Locale.None, IEnumProvider enumProvider = null) { var assemblyName = new AssemblyName("DBCDDefinitions"); var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); - var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name); + moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name); - this.moduleBuilder = moduleBuilder; this.locStringSize = 1; this.locale = locale; + this.enumProvider = enumProvider; } - internal Tuple Build(DBParser dbcReader, Structs.DBDefinition databaseDefinition, string name, string build) + internal (Type Type, DBCDInfo Info) Build(DBParser dbcReader, DBDefinition databaseDefinition, string name, string build) { - if (name == null) - name = Guid.NewGuid().ToString(); + name ??= Guid.NewGuid().ToString(); - Structs.VersionDefinitions? versionDefinition = null; + VersionDefinitions? versionDefinition = null; + Build currentBuild = null; if (!string.IsNullOrWhiteSpace(build)) { - var dbBuild = new Build(build); - locStringSize = GetLocStringSize(dbBuild); - Utils.GetVersionDefinitionByBuild(databaseDefinition, dbBuild, out versionDefinition); + currentBuild = new Build(build); + locStringSize = GetLocStringSize(currentBuild); + Utils.GetVersionDefinitionByBuild(databaseDefinition, currentBuild, out versionDefinition); } if (versionDefinition == null && dbcReader.LayoutHash != 0) @@ -69,17 +72,17 @@ internal Tuple Build(DBParser dbcReader, Structs.DBDefinition da var fields = versionDefinition.Value.definitions; var columns = new List(fields.Length); - bool localiseStrings = locale != Locale.None; + var localiseStrings = locale != Locale.None; + var enumTypes = new Dictionary(); var metadataIndex = 0; foreach (var fieldDefinition in fields) { var columnDefinition = databaseDefinition.columnDefinitions[fieldDefinition.name]; - bool isLocalisedString = columnDefinition.type == "locstring" && locStringSize > 1; - + var isLocalisedString = columnDefinition.type == "locstring" && locStringSize > 1; Type fieldType; - if (fieldDefinition.isRelation && fieldDefinition.isNonInline) + if (fieldDefinition is { isRelation: true, isNonInline: true }) { fieldType = fieldDefinition.arrLength == 0 ? typeof(int) : typeof(int[]); } @@ -95,8 +98,8 @@ internal Tuple Build(DBParser dbcReader, Structs.DBDefinition da if (fieldDefinition.isID) { AddAttribute(field, fieldDefinition.isNonInline); - } - + } + if (!fieldDefinition.isNonInline) { if (dbcReader.ColumnMeta != null && metadataIndex < dbcReader.ColumnMeta.Length) @@ -136,6 +139,49 @@ internal Tuple Build(DBParser dbcReader, Structs.DBDefinition da columns.Add(fieldDefinition.name + "_mask"); } } + + // Enum/flags annotation — non-relation fields with a known mapping only + if (enumProvider != null && !fieldDefinition.isRelation && enumProvider.HasEnumDefinition(name, fieldDefinition.name)) + { + if (fieldDefinition.arrLength == 0) + { + // Non-array: single definition applies to the whole field + var enumDef = enumProvider.GetEnumDefinition(name, fieldDefinition.name); + if (enumDef.HasValue) + { + var isFlags = enumDef.Value.metaType == MetaType.FLAGS; + var enumTypeName = $"{name}_{fieldDefinition.name}"; + var enumType = BuildEnumType(enumTypeName, enumDef.Value, currentBuild); + enumTypes[fieldDefinition.name] = enumType; + AddEnumAttribute(field, enumTypeName, isFlags); + } + } + else + { + // Array: query each index individually; specific-index mappings take priority + // over "applies to all" mappings (handled inside GetEnumDefinition). + EnumDefinition? attributeHint = null; + + for (var i = 0; i < fieldDefinition.arrLength; i++) + { + var enumDef = enumProvider.GetEnumDefinition(name, fieldDefinition.name, i); + if (!enumDef.HasValue) + continue; + + var enumTypeName = $"{name}_{fieldDefinition.name}_{i}"; + var enumType = BuildEnumType(enumTypeName, enumDef.Value, currentBuild); + enumTypes[$"{fieldDefinition.name}[{i}]"] = enumType; + attributeHint ??= enumDef; + } + + // Tag the field with EnumAttribute so consumers know to check EnumTypes + if (attributeHint.HasValue) + { + var hintTypeName = $"{name}_{fieldDefinition.name}"; + AddEnumAttribute(field, hintTypeName, attributeHint.Value.metaType == MetaType.FLAGS); + } + } + } } var type = typeBuilder.CreateTypeInfo(); @@ -143,10 +189,55 @@ internal Tuple Build(DBParser dbcReader, Structs.DBDefinition da var info = new DBCDInfo { availableColumns = columns.ToArray(), - tableName = name + tableName = name, + enumTypes = enumTypes }; - return new Tuple(type, info); + return (type, info); + } + + /// + /// Dynamically generates an enum type from an , + /// optionally filtered to entries that match . + /// + private Type BuildEnumType(string typeName, EnumDefinition enumDef, Build currentBuild) + { + var enumBuilder = moduleBuilder.DefineEnum(typeName, TypeAttributes.Public, typeof(long)); + + var isFlags = enumDef.metaType == MetaType.FLAGS; + if (isFlags) + { + var flagsCtor = typeof(FlagsAttribute).GetConstructor(Type.EmptyTypes); + enumBuilder.SetCustomAttribute(new CustomAttributeBuilder(flagsCtor, [])); + } + + foreach (var entry in enumDef.entries) + { + if (string.IsNullOrEmpty(entry.name)) + continue; + + if (!EntryMatchesBuild(entry, currentBuild)) + continue; + + enumBuilder.DefineLiteral(entry.name, (long)entry.value); + } + + return enumBuilder.CreateTypeInfo(); + } + + /// + /// Returns true if the entry applies to the given build, or if the entry has no build + /// restrictions (applies to all builds), or if no build is specified. + /// + private static bool EntryMatchesBuild(EnumEntry entry, Build currentBuild) + { + if (currentBuild == null || (entry.builds.Length == 0 && entry.buildRanges.Length == 0)) + return true; + + if (entry.builds.Any(b => b.Equals(currentBuild))) + return true; + + return entry.buildRanges.Any(r => r.Contains(currentBuild)); } private int GetLocStringSize(Build build) @@ -171,44 +262,46 @@ private void AddAttribute(FieldBuilder field, params object[] parameters) whe field.SetCustomAttribute(attributeBuilder); } - private Type FieldDefinitionToType(Structs.Definition field, Structs.ColumnDefinition column, bool localiseStrings) + /// + /// Applies to a field. Uses a dedicated method because the + /// generic helper derives parameter types via + /// GetType(), which doesn't work correctly for bool literals. + /// + private static void AddEnumAttribute(FieldBuilder field, string enumName, bool isFlags) + { + var ctor = typeof(EnumAttribute).GetConstructor([typeof(string), typeof(bool)]); + var attributeBuilder = new CustomAttributeBuilder(ctor, [enumName, isFlags]); + field.SetCustomAttribute(attributeBuilder); + } + + private Type FieldDefinitionToType(Definition field, ColumnDefinition column, bool localiseStrings) { var isArray = field.arrLength != 0; switch (column.type) { case "int": + { + var signed = field.isSigned; + var type = field.size switch { - Type type = null; - var signed = field.isSigned; - - switch (field.size) - { - case 8: - type = signed ? typeof(sbyte) : typeof(byte); - break; - case 16: - type = signed ? typeof(short) : typeof(ushort); - break; - case 32: - type = signed ? typeof(int) : typeof(uint); - break; - case 64: - type = signed ? typeof(long) : typeof(ulong); - break; - } - - return isArray ? type.MakeArrayType() : type; - } + 8 => signed ? typeof(sbyte) : typeof(byte), + 16 => signed ? typeof(short) : typeof(ushort), + 32 => signed ? typeof(int) : typeof(uint), + 64 => signed ? typeof(long) : typeof(ulong), + _ => null + }; + return isArray ? type.MakeArrayType() : type; + } case "string": return isArray ? typeof(string[]) : typeof(string); case "locstring": - { - if (isArray && locStringSize > 1) - throw new NotSupportedException("Localised string arrays are not supported"); + { + if (isArray && locStringSize > 1) + throw new NotSupportedException("Localised string arrays are not supported"); - return (!localiseStrings && locStringSize > 1) || isArray ? typeof(string[]) : typeof(string); - } + return (!localiseStrings && locStringSize > 1) || isArray ? typeof(string[]) : typeof(string); + } case "float": return isArray ? typeof(float[]) : typeof(float); default: diff --git a/DBCD/DBCDStorage.cs b/DBCD/DBCDStorage.cs index 444409c..dafa3fa 100644 --- a/DBCD/DBCDStorage.cs +++ b/DBCD/DBCDStorage.cs @@ -1,11 +1,8 @@ using DBCD.Helpers; - using DBCD.IO; using DBCD.IO.Attributes; using System; -using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Dynamic; using System.IO; using System.Linq; @@ -18,12 +15,14 @@ public class DBCDRow : DynamicObject public int ID; private readonly dynamic raw; - private FieldAccessor fieldAccessor; + private readonly FieldAccessor fieldAccessor; + private readonly IReadOnlyDictionary enumTypes; - internal DBCDRow(int ID, dynamic raw, FieldAccessor fieldAccessor) + internal DBCDRow(int ID, dynamic raw, FieldAccessor fieldAccessor, IReadOnlyDictionary enumTypes = null) { this.raw = raw; this.fieldAccessor = fieldAccessor; + this.enumTypes = enumTypes; this.ID = ID; } @@ -39,14 +38,32 @@ public override bool TrySetMember(SetMemberBinder binder, object value) public object this[string fieldName] { - get => fieldAccessor[this.raw, fieldName]; + get + { + var value = fieldAccessor[this.raw, fieldName]; + if (enumTypes != null && enumTypes.TryGetValue(fieldName, out var enumType)) + return Enum.ToObject(enumType, value); + return value; + } set => fieldAccessor[this.raw, fieldName] = value; } public object this[string fieldName, int index] { - get => ((Array)this[fieldName]).GetValue(index); - set => ((Array)this[fieldName]).SetValue(value, index); + get + { + var element = ((Array)fieldAccessor[this.raw, fieldName]).GetValue(index); + if (enumTypes != null) + { + var key = enumTypes.ContainsKey($"{fieldName}[{index}]") + ? $"{fieldName}[{index}]" + : fieldName; + if (enumTypes.TryGetValue(key, out var enumType)) + return Enum.ToObject(enumType, element); + } + return element; + } + set => ((Array)fieldAccessor[this.raw, fieldName]).SetValue(value, index); } public T Field(string fieldName) @@ -59,6 +76,80 @@ public T FieldAs(string fieldName) return fieldAccessor.GetMemberAs(this.raw, fieldName); } + /// + /// Returns true if the named flag is set in the flags field. + /// + public bool HasFlag(string fieldName, string flagName) + { + if (enumTypes == null || !enumTypes.TryGetValue(fieldName, out var enumType)) + return false; + + if (!Enum.IsDefined(enumType, flagName)) + throw new KeyNotFoundException($"{flagName} not in {enumType}"); + + var raw = Convert.ToUInt64(fieldAccessor[this.raw, fieldName]); + var flag = Convert.ToUInt64(Enum.Parse(enumType, flagName)); + return (raw & flag) == flag; + } + + /// + /// Returns true if the named flag is set in the flags field at the given array index. + /// Checks a per-index mapping first ("FieldName[n]"), then falls back to a whole-field mapping. + /// + public bool HasFlag(string fieldName, int index, string flagName) + { + if (enumTypes == null) + return false; + + var key = enumTypes.ContainsKey($"{fieldName}[{index}]") ? $"{fieldName}[{index}]" : fieldName; + if (!enumTypes.TryGetValue(key, out var enumType)) + return false; + + if (!Enum.IsDefined(enumType, flagName)) + throw new KeyNotFoundException($"{flagName} not in {enumType}"); + + var element = ((Array)fieldAccessor[this.raw, fieldName]).GetValue(index); + var raw = Convert.ToUInt64(element); + var flag = Convert.ToUInt64(Enum.Parse(enumType, flagName)); + return (raw & flag) == flag; + } + + /// + /// Returns true if the enum field's value matches the named enum member. + /// + public bool IsEnumMember(string fieldName, string memberName) + { + if (enumTypes == null || !enumTypes.TryGetValue(fieldName, out var enumType)) + return false; + + if (!Enum.IsDefined(enumType, memberName)) + throw new KeyNotFoundException($"{memberName} not in {enumType}"); + + var raw = Enum.ToObject(enumType, fieldAccessor[this.raw, fieldName]); + return raw.Equals(Enum.Parse(enumType, memberName)); + } + + /// + /// Returns true if the enum field at the given array index matches the named enum member. + /// Checks a per-index mapping first ("FieldName[n]"), then falls back to a whole-field mapping. + /// + public bool IsEnumMember(string fieldName, int index, string memberName) + { + if (enumTypes == null) + return false; + + var key = enumTypes.ContainsKey($"{fieldName}[{index}]") ? $"{fieldName}[{index}]" : fieldName; + if (!enumTypes.TryGetValue(key, out var enumType)) + return false; + + if (!Enum.IsDefined(enumType, memberName)) + throw new KeyNotFoundException($"{memberName} not in {enumType}"); + + var element = ((Array)fieldAccessor[this.raw, fieldName]).GetValue(index); + var raw = Enum.ToObject(enumType, element); + return raw.Equals(Enum.Parse(enumType, memberName)); + } + public override IEnumerable GetDynamicMemberNames() { return fieldAccessor.FieldNames; @@ -81,25 +172,16 @@ internal DynamicKeyValuePair(T key, dynamic value) } } - public class RowConstructor + public class RowConstructor(IDBCDStorage storage) { - private readonly IDBCDStorage storage; - public RowConstructor(IDBCDStorage storage) - { - this.storage = storage; - } - public bool Create(int index, Action f) { var constructedRow = storage.ConstructRow(index); if (storage.ContainsKey(index)) return false; - else - { - f(constructedRow); - storage.Add(index, constructedRow); - } + f(constructedRow); + storage.Add(index, constructedRow); return true; } } @@ -109,6 +191,14 @@ public interface IDBCDStorage : IEnumerable>, IDictiona string[] AvailableColumns { get; } uint LayoutHash { get; } + IReadOnlyDictionary EnumTypes { get; } + + /// + /// Returns true if the field name exists in the enum types dictionary. + /// For array fields, pass the key with index included (e.g. "Attributes[0]"). + /// + bool IsEnumOrFlagField(string fieldName); + DBCDRow ConstructRow(int index); Dictionary GetEncryptedSections(); @@ -129,10 +219,16 @@ public interface IDBCDStorage : IEnumerable>, IDictiona private readonly DBCDInfo info; private readonly DBParser parser; + private readonly (FieldInfo Field, Type ElementType, int Count, bool IsString)[] _arrayFieldCache; + private readonly FieldInfo[] _stringFieldCache; + string[] IDBCDStorage.AvailableColumns => this.info.availableColumns; public uint LayoutHash => this.storage.LayoutHash; + public IReadOnlyDictionary EnumTypes => this.info.enumTypes; public override string ToString() => $"{this.info.tableName}"; + public bool IsEnumOrFlagField(string fieldName) => info.enumTypes?.ContainsKey(fieldName) ?? false; + public DBCDStorage(Stream stream, DBCDInfo info) : this(new DBParser(stream), info) { } public DBCDStorage(DBParser dbParser, DBCDInfo info) : this(dbParser, dbParser.GetRecords(), info) { } @@ -144,8 +240,20 @@ public DBCDStorage(DBParser parser, Storage storage, DBCDInfo info) : base(ne this.parser = parser; this.storage = storage; + var fields = typeof(T).GetFields(); + _arrayFieldCache = fields + .Where(f => f.FieldType.IsArray) + .Select(f => + { + var elementType = f.FieldType.GetElementType(); + var count = f.GetCustomAttribute().Count; + return (f, elementType, count, elementType == typeof(string)); + }) + .ToArray(); + _stringFieldCache = fields.Where(f => f.FieldType == typeof(string)).ToArray(); + foreach (var record in storage) - base.Add(record.Key, new DBCDRow(record.Key, record.Value, fieldAccessor)); + base.Add(record.Key, new DBCDRow(record.Key, record.Value, fieldAccessor, info.enumTypes)); } @@ -162,10 +270,10 @@ public void ApplyingHotfixes(HotfixReader hotfixReader, HotfixReader.RowProcesso #if NETSTANDARD2_0 foreach (var record in mutableStorage) - base[record.Key] = new DBCDRow(record.Key, record.Value, fieldAccessor); + base[record.Key] = new DBCDRow(record.Key, record.Value, fieldAccessor, info.EnumTypes); #else foreach (var (id, row) in mutableStorage) - base[id] = new DBCDRow(id, row, fieldAccessor); + base[id] = new DBCDRow(id, row, fieldAccessor, info.enumTypes); #endif foreach (var key in base.Keys.Except(mutableStorage.Keys)) base.Remove(key); @@ -198,36 +306,22 @@ public void Save(string filename) public DBCDRow ConstructRow(int index) { T raw = new(); - var fields = typeof(T).GetFields(); - // Array Fields need to be initialized to fill their length - var arrayFields = fields.Where(x => x.FieldType.IsArray); - foreach (var arrayField in arrayFields) - { - var count = arrayField.GetCustomAttribute().Count; - var elementType = arrayField.FieldType.GetElementType(); - var isStringField = elementType == typeof(string); - Array rowRecords = Array.CreateInstance(elementType, count); - for (var i = 0; i < count; i++) - { - if (isStringField) - { - rowRecords.SetValue(string.Empty, i); - } else - { - rowRecords.SetValue(Activator.CreateInstance(elementType), i); - } - } - arrayField.SetValue(raw, rowRecords); + // Array fields: value type arrays are already zero-initialized by Array.CreateInstance; + // only string arrays need explicit filling. + foreach (var (field, elementType, count, isString) in _arrayFieldCache) + { + var arr = Array.CreateInstance(elementType, count); + if (isString) + Array.Fill((string[])arr, string.Empty); + field.SetValue(raw, arr); } - // String Fields need to be initialized to empty string rather than null; - var stringFields = fields.Where(x => x.FieldType == typeof(string)); - foreach (var stringField in stringFields) - { + // String fields must be initialized to empty string rather than null. + foreach (var stringField in _stringFieldCache) stringField.SetValue(raw, string.Empty); - } - return new DBCDRow(index, raw, fieldAccessor); + + return new DBCDRow(index, raw, fieldAccessor, info.enumTypes); } public Dictionary ToDictionary() @@ -235,4 +329,4 @@ public Dictionary ToDictionary() return this; } } -} \ No newline at end of file +} diff --git a/DBCD/Providers/FilesystemEnumProvider.cs b/DBCD/Providers/FilesystemEnumProvider.cs new file mode 100644 index 0000000..5316704 --- /dev/null +++ b/DBCD/Providers/FilesystemEnumProvider.cs @@ -0,0 +1,104 @@ +using System; +using DBDefsLib; +using DBDefsLib.Constants; +using DBDefsLib.Structs; +using System.Collections.Generic; +using System.IO; + +namespace DBCD.Providers +{ + /// + /// Resolves enum/flag definitions from a local WoWDBDefs meta directory, + /// using a .dbdm file to map table columns to their enum/flag files. + /// + public class FilesystemEnumProvider : IEnumProvider + { + private readonly string metaDirectory; + private readonly Dictionary cache = new(); + + public List Mappings { get; } + + /// Absolute path to the .dbdm mapping file (e.g. WoWDBDefs/meta/Meta.dbdm). + public FilesystemEnumProvider(string dbdmFile) + { + metaDirectory = Path.GetDirectoryName(dbdmFile)!; + Mappings = new DBDMReader().Read(dbdmFile); + PopulateCache(); + } + + public EnumDefinition? GetEnumDefinition(string tableName, string columnName, int? arrayIndex = null, + string conditionalTable = null, string conditionalColumn = null, string conditionalValue = null) + { + // Build base key (with or without array index) + var baseKey = arrayIndex.HasValue + ? $"{tableName.ToLowerInvariant()}::{columnName.ToLowerInvariant()}[{arrayIndex}]" + : $"{tableName.ToLowerInvariant()}::{columnName.ToLowerInvariant()}"; + + // If conditional context supplied, try conditional key first + if (!string.IsNullOrEmpty(conditionalTable)) + { + var conditionalKey = $"{baseKey}@{conditionalTable.ToLowerInvariant()}.{conditionalColumn!.ToLowerInvariant()}={conditionalValue}"; + if (cache.TryGetValue(conditionalKey, out var conditional)) + return conditional; + } + + // Fall back to unconditional (handles arrayIndex fallback too) + if (arrayIndex.HasValue) + { + if (cache.TryGetValue(baseKey, out var specific)) + return specific; + var fallbackKey = $"{tableName.ToLowerInvariant()}::{columnName.ToLowerInvariant()}"; + return cache.TryGetValue(fallbackKey, out var fallback) ? fallback : null; + } + + return cache.TryGetValue(baseKey, out var cached) ? cached : null; + } + + private void PopulateCache() + { + // Deduplicate file reads: multiple mappings may point to the same enum/flag file. + var fileCache = new Dictionary(); + + foreach (var mapping in Mappings) + { + if (mapping.meta is MetaType.COLOR) + continue; + + var cacheKey = BuildCacheKey(mapping); + var fileKey = $"{mapping.meta}::{mapping.metaValue}"; + if (!fileCache.TryGetValue(fileKey, out var enumDef)) + { + enumDef = TryReadEnumFile(mapping); + fileCache[fileKey] = enumDef; + } + + if (enumDef.HasValue) + cache[cacheKey] = enumDef.Value; + } + } + + private EnumDefinition? TryReadEnumFile(MappingDefinition mapping) + { + var dir = mapping.meta == MetaType.ENUM ? "enums" : "flags"; + var ext = mapping.meta == MetaType.ENUM ? ".dbde" : ".dbdf"; + var path = Path.Combine(metaDirectory, dir, $"{mapping.metaValue}{ext}"); + + if (!File.Exists(path)) + return null; + + return new DBDEnumReader().Read(path, mapping.meta); + } + + private static string BuildCacheKey(MappingDefinition m) + { + var key = m.arrIndex.HasValue + ? $"{m.tableName.ToLowerInvariant()}::{m.columnName.ToLowerInvariant()}[{m.arrIndex}]" + : $"{m.tableName.ToLowerInvariant()}::{m.columnName.ToLowerInvariant()}"; + + if (!string.IsNullOrEmpty(m.conditionalTable)) + key += $"@{m.conditionalTable.ToLowerInvariant()}.{m.conditionalColumn.ToLowerInvariant()}={m.conditionalValue}"; + + return key; + } + } +} diff --git a/DBCD/Providers/GithubEnumProvider.cs b/DBCD/Providers/GithubEnumProvider.cs new file mode 100644 index 0000000..51c367a --- /dev/null +++ b/DBCD/Providers/GithubEnumProvider.cs @@ -0,0 +1,147 @@ +using DBDefsLib; +using DBDefsLib.Constants; +using DBDefsLib.Structs; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; + +namespace DBCD.Providers +{ + /// + /// Resolves enum/flag definitions from the WoWDBDefs GitHub repository, + /// downloading the mapping file and individual enum/flag files on demand. + /// + public class GithubEnumProvider : IEnumProvider + { + private static readonly Uri BaseURI = new Uri("https://raw.githubusercontent.com/wowdev/WoWDBDefs/master/meta/"); + private readonly HttpClient client = new HttpClient(); + + private static bool UseCache = false; + private static string CachePath { get; } = "EnumCache/"; + private static readonly TimeSpan CacheExpiryTime = new TimeSpan(1, 0, 0, 0); + + private readonly Dictionary cache = new(); + + public List Mappings { get; } + + public GithubEnumProvider(bool useCache = false) + { + UseCache = useCache; + client.BaseAddress = BaseURI; + + if (useCache && !Directory.Exists(CachePath)) + Directory.CreateDirectory(CachePath); + + var mappingStream = FetchFile("mapping.dbdm"); + Mappings = new DBDMReader().Read(mappingStream); + PopulateCache(); + } + + public EnumDefinition? GetEnumDefinition(string tableName, string columnName, int? arrayIndex = null, + string conditionalTable = null, string conditionalColumn = null, string conditionalValue = null) + { + // Build base key (with or without array index) + var baseKey = arrayIndex.HasValue + ? $"{tableName.ToLowerInvariant()}::{columnName.ToLowerInvariant()}[{arrayIndex}]" + : $"{tableName.ToLowerInvariant()}::{columnName.ToLowerInvariant()}"; + + // If conditional context supplied, try conditional key first + if (!string.IsNullOrEmpty(conditionalTable)) + { + var conditionalKey = $"{baseKey}@{conditionalTable.ToLowerInvariant()}.{conditionalColumn!.ToLowerInvariant()}={conditionalValue}"; + if (cache.TryGetValue(conditionalKey, out var conditional)) + return conditional; + } + + // Fall back to unconditional (handles arrayIndex fallback too) + if (arrayIndex.HasValue) + { + if (cache.TryGetValue(baseKey, out var specific)) + return specific; + + var fallbackKey = $"{tableName.ToLowerInvariant()}::{columnName.ToLowerInvariant()}"; + return cache.TryGetValue(fallbackKey, out var fallback) ? fallback : null; + } + + return cache.TryGetValue(baseKey, out var cached) ? cached : null; + } + + private void PopulateCache() + { + // Deduplicate file fetches: multiple mappings may point to the same enum/flag file. + var fileCache = new Dictionary(); + + foreach (var mapping in Mappings) + { + if (mapping.meta is MetaType.COLOR) + continue; + + var cacheKey = BuildCacheKey(mapping); + var fileKey = $"{mapping.meta}::{mapping.metaValue}"; + if (!fileCache.TryGetValue(fileKey, out var enumDef)) + { + enumDef = TryReadEnumFile(mapping); + fileCache[fileKey] = enumDef; + } + + if (enumDef.HasValue) + cache[cacheKey] = enumDef.Value; + } + } + + private EnumDefinition? TryReadEnumFile(MappingDefinition mapping) + { + var dir = mapping.meta == MetaType.ENUM ? "enums" : "flags"; + var ext = mapping.meta == MetaType.ENUM ? ".dbde" : ".dbdf"; + var query = $"{dir}/{mapping.metaValue}{ext}"; + + try + { + var stream = FetchFile(query); + return new DBDEnumReader().Read(stream, mapping.meta); + } + catch + { + return null; + } + } + + private Stream FetchFile(string query) + { + if (UseCache) + { + var cacheFile = Path.Combine(CachePath, query.Replace('/', Path.DirectorySeparatorChar)); + if (File.Exists(cacheFile)) + { + var lastWrite = File.GetLastWriteTime(cacheFile); + if (DateTime.Now - lastWrite < CacheExpiryTime) + return new MemoryStream(File.ReadAllBytes(cacheFile)); + } + } + + var bytes = client.GetByteArrayAsync(query).Result; + + if (UseCache) + { + var cacheFile = Path.Combine(CachePath, query.Replace('/', Path.DirectorySeparatorChar)); + Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)!); + File.WriteAllBytes(cacheFile, bytes); + } + + return new MemoryStream(bytes); + } + + private static string BuildCacheKey(MappingDefinition m) + { + var key = m.arrIndex.HasValue + ? $"{m.tableName.ToLowerInvariant()}::{m.columnName.ToLowerInvariant()}[{m.arrIndex}]" + : $"{m.tableName.ToLowerInvariant()}::{m.columnName.ToLowerInvariant()}"; + + if (!string.IsNullOrEmpty(m.conditionalTable)) + key += $"@{m.conditionalTable.ToLowerInvariant()}.{m.conditionalColumn.ToLowerInvariant()}={m.conditionalValue}"; + + return key; + } + } +} diff --git a/DBCD/Providers/IEnumProvider.cs b/DBCD/Providers/IEnumProvider.cs new file mode 100644 index 0000000..61f17c6 --- /dev/null +++ b/DBCD/Providers/IEnumProvider.cs @@ -0,0 +1,35 @@ +using DBDefsLib.Constants; +using DBDefsLib.Structs; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DBCD.Providers +{ + public interface IEnumProvider + { + /// + /// The list of instances, this contains the actual mapping of field -> meta type -> meta value etc. + /// + public List Mappings { get; } + + /// + /// Returns true if any enum or flag mapping exists for the given field, regardless of array index. + /// Use this as a fast pre-check before calling . + /// + public bool HasEnumDefinition(string tableName, string columnName) => + Mappings.Any(m => + m.meta != MetaType.COLOR && + m.tableName.Equals(tableName, StringComparison.OrdinalIgnoreCase) && + m.columnName.Equals(columnName, StringComparison.OrdinalIgnoreCase)); + + /// + /// Returns the for a table column, or null if none is mapped. + /// For non-array fields, omit (or pass null). + /// For array fields, pass the element index. A specific-index mapping takes priority over + /// an "applies to all elements" mapping (indicated by a null arrIndex in the source data). + /// + public EnumDefinition? GetEnumDefinition(string tableName, string columnName, int? arrayIndex = null, + string conditionalTable = null, string conditionalColumn = null, string conditionalValue = null); + } +}