diff --git a/Source/Game/DataStorage/CliDB.cs b/Source/Game/DataStorage/CliDB.cs index 4ca0a1eca..a83fa22cb 100644 --- a/Source/Game/DataStorage/CliDB.cs +++ b/Source/Game/DataStorage/CliDB.cs @@ -285,7 +285,7 @@ namespace Game.DataStorage SpellVisualKitStorage = ReadDB2("SpellVisualKit.db2", HotfixStatements.SEL_SPELL_VISUAL_KIT); SpellXSpellVisualStorage = ReadDB2("SpellXSpellVisual.db2", HotfixStatements.SEL_SPELL_X_SPELL_VISUAL); SummonPropertiesStorage = ReadDB2("SummonProperties.db2", HotfixStatements.SEL_SUMMON_PROPERTIES); - //TactKeyStorage = ReadDB2("TactKey.db2", HotfixStatements.SEL_TACT_KEY); + TactKeyStorage = ReadDB2("TactKey.db2", HotfixStatements.SEL_TACT_KEY); TalentStorage = ReadDB2("Talent.db2", HotfixStatements.SEL_TALENT, HotfixStatements.SEL_TALENT_LOCALE); TaxiNodesStorage = ReadDB2("TaxiNodes.db2", HotfixStatements.SEL_TAXI_NODES, HotfixStatements.SEL_TAXI_NODES_LOCALE); TaxiPathStorage = ReadDB2("TaxiPath.db2", HotfixStatements.SEL_TAXI_PATH); @@ -641,7 +641,7 @@ namespace Game.DataStorage public static DB6Storage SpellVisualKitStorage; public static DB6Storage SpellXSpellVisualStorage; public static DB6Storage SummonPropertiesStorage; - //public static DB6Storage TactKeyStorage; + public static DB6Storage TactKeyStorage; public static DB6Storage TalentStorage; public static DB6Storage TaxiNodesStorage; public static DB6Storage TaxiPathStorage; diff --git a/Source/Game/DataStorage/ClientReader/DB6Storage.cs b/Source/Game/DataStorage/ClientReader/DB6Storage.cs index 907f5d053..b64fd871d 100644 --- a/Source/Game/DataStorage/ClientReader/DB6Storage.cs +++ b/Source/Game/DataStorage/ClientReader/DB6Storage.cs @@ -39,8 +39,12 @@ namespace Game.DataStorage [Serializable] public class DB6Storage : Dictionary, IDB2Storage where T : new() { - public void LoadData(int indexField, BitSet availableDb2Locales, HotfixStatements preparedStatement, HotfixStatements preparedStatementLocale) + uint _tableHash; + + public void LoadData(WDCHeader header, BitSet availableDb2Locales, HotfixStatements preparedStatement, HotfixStatements preparedStatementLocale) { + _tableHash = header.TableHash; + SQLResult result = DB.Hotfix.Query(DB.Hotfix.GetPreparedStatement(preparedStatement)); if (!result.IsEmpty()) { @@ -183,7 +187,7 @@ namespace Game.DataStorage } } - var id = (uint)fields[indexField == -1 ? 0 : indexField].GetValue(obj); + var id = (uint)fields[header.IdIndex == -1 ? 0 : header.IdIndex].GetValue(obj); base[id] = obj; } while (result.NextRow()); @@ -358,5 +362,7 @@ namespace Game.DataStorage { Remove(id); } + + public uint GetTableHash() { return _tableHash; } } } diff --git a/Source/Game/DataStorage/ClientReader/DBReader.cs b/Source/Game/DataStorage/ClientReader/DBReader.cs index 2a191a863..69e2af6fd 100644 --- a/Source/Game/DataStorage/ClientReader/DBReader.cs +++ b/Source/Game/DataStorage/ClientReader/DBReader.cs @@ -58,7 +58,7 @@ namespace Game.DataStorage foreach (var b in reader._records) storage.Add((uint)b.Key, b.Value.As()); - storage.LoadData(reader.Header.IdIndex, availableDb2Locales, preparedStatement, preparedStatementLocale); + storage.LoadData(reader.Header, availableDb2Locales, preparedStatement, preparedStatementLocale); Global.DB2Mgr.AddDB2(reader.Header.TableHash, storage); loadedFileCount++; diff --git a/Source/Game/DataStorage/DB2Manager.cs b/Source/Game/DataStorage/DB2Manager.cs index 1f3357101..63336da3f 100644 --- a/Source/Game/DataStorage/DB2Manager.cs +++ b/Source/Game/DataStorage/DB2Manager.cs @@ -43,7 +43,10 @@ namespace Game.DataStorage _nameValidators[i] = new List(); for (var i = 0; i < (int)Locale.Total; ++i) - _hotfixBlob[i] = new Dictionary, byte[]>(); + { + _hotfixBlob[i] = new Dictionary<(uint tableHas, int recordId), byte[]>(); + _hotfixOptionalData[i] = new MultiMap<(uint tableHas, int recordId), HotfixOptionalData>(); + } } public void LoadStores() @@ -748,7 +751,7 @@ namespace Game.DataStorage var storeItr = _storage.LookupByKey(tableHash); if (storeItr != null) { - Log.outError(LogFilter.ServerLoading, $"Table hash 0x{tableHash:X} points to a loaded DB2 store {nameof(storeItr)}, fill related table instead of hotfix_blob"); + Log.outError(LogFilter.Sql, $"Table hash 0x{tableHash:X} points to a loaded DB2 store {nameof(storeItr)}, fill related table instead of hotfix_blob"); continue; } @@ -758,20 +761,96 @@ namespace Game.DataStorage Locale locale = localeName.ToEnum(); if (!SharedConst.IsValidLocale(locale)) { - Log.outError(LogFilter.ServerLoading, $"`hotfix_blob` contains invalid locale: {localeName} at TableHash: 0x{tableHash:X} and RecordID: {recordId}"); + Log.outError(LogFilter.Sql, $"`hotfix_blob` contains invalid locale: {localeName} at TableHash: 0x{tableHash:X} and RecordID: {recordId}"); continue; } if (!availableDb2Locales[(int)locale]) continue; - _hotfixBlob[(int)locale][Tuple.Create(tableHash, recordId)] = result.Read(3); + _hotfixBlob[(int)locale][(tableHash, recordId)] = result.Read(3); hotfixBlobCount++; } while (result.NextRow()); Log.outInfo(LogFilter.ServerLoading, $"Loaded {hotfixBlobCount} hotfix blob records in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); } + public void LoadHotfixOptionalData(BitSet availableDb2Locales) + { + // Register allowed optional data keys + _allowedHotfixOptionalData.Add(CliDB.BroadcastTextStorage.GetTableHash(), Tuple.Create(CliDB.TactKeyStorage.GetTableHash(), (AllowedHotfixOptionalData)ValidateBroadcastTextTactKeyOptionalData)); + + uint oldMSTime = Time.GetMSTime(); + + SQLResult result = DB.Hotfix.Query("SELECT TableHash, RecordId, locale, `Key`, `Data` FROM hotfix_optional_data ORDER BY TableHash"); + if (result.IsEmpty()) + { + Log.outInfo(LogFilter.ServerLoading, "Loaded 0 hotfix optional data records."); + return; + } + + uint hotfixOptionalDataCount = 0; + do + { + uint tableHash = result.Read(0); + var allowedHotfixes = _allowedHotfixOptionalData.LookupByKey(tableHash); + if (allowedHotfixes.Empty()) + { + Log.outError(LogFilter.Sql, $"Table `hotfix_optional_data` references DB2 store by hash 0x{tableHash:X} that is not allowed to have optional data"); + continue; + } + + uint recordId = result.Read(1); + var db2storage = _storage.LookupByKey(tableHash); + if (db2storage == null) + { + Log.outError(LogFilter.Sql, $"Table `hotfix_optional_data` references unknown DB2 store by hash 0x{tableHash:X} with RecordID: {recordId}"); + continue; + } + + string localeName = result.Read(2); + Locale locale = localeName.ToEnum(); + + if (!SharedConst.IsValidLocale(locale)) + { + Log.outError(LogFilter.Sql, $"`hotfix_optional_data` contains invalid locale: {localeName} at TableHash: 0x{tableHash:X} and RecordID: {recordId}"); + continue; + } + + if (!availableDb2Locales[(int)locale]) + continue; + + HotfixOptionalData optionalData = new HotfixOptionalData(); + optionalData.Key = result.Read(3); + var allowedHotfixItr = allowedHotfixes.Find(v => + { + return v.Item1 == optionalData.Key; + }); + if (allowedHotfixItr == null) + { + Log.outError(LogFilter.Sql, $"Table `hotfix_optional_data` references non-allowed optional data key 0x{optionalData.Key:X} for DB2 store by hash 0x{tableHash:X} and RecordID: {recordId}"); + continue; + } + + optionalData.Data = result.Read(4); + if (!allowedHotfixItr.Item2(optionalData.Data)) + { + Log.outError(LogFilter.Sql, $"Table `hotfix_optional_data` contains invalid data for DB2 store 0x{tableHash:X}, RecordID: {recordId} and Key: 0x{optionalData.Key:X}"); + continue; + } + + _hotfixOptionalData[(int)locale].Add((tableHash, (int)recordId), optionalData); + hotfixOptionalDataCount++; + } while (result.NextRow()); + + Log.outInfo(LogFilter.ServerLoading, $"Loaded {hotfixOptionalDataCount} hotfix optional data records in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); + } + + public bool ValidateBroadcastTextTactKeyOptionalData(byte[] data) + { + return data.Length == 8 + 16; + } + public uint GetHotfixCount() { return (uint)_hotfixData.Count; } public List GetHotfixData() { return _hotfixData; } @@ -783,6 +862,13 @@ namespace Game.DataStorage return _hotfixBlob[(int)locale].LookupByKey(Tuple.Create(tableHash, recordId)); } + public List GetHotfixOptionalData(uint tableHash, uint recordId, Locale locale) + { + Cypher.Assert(SharedConst.IsValidLocale(locale), $"Locale {locale} is invalid locale"); + + return _hotfixOptionalData[(int)locale].LookupByKey((tableHash, (int)recordId)); + } + public uint GetEmptyAnimStateID() { return (uint)CliDB.AnimationDataStorage.Count; @@ -2154,9 +2240,13 @@ namespace Game.DataStorage _storage[tableHash] = store; } + delegate bool AllowedHotfixOptionalData(byte[] data); + Dictionary _storage = new Dictionary(); List _hotfixData = new List(); - Dictionary, byte[]>[] _hotfixBlob = new Dictionary, byte[]>[(int)Locale.Total]; + Dictionary<(uint tableHas, int recordId), byte[]>[] _hotfixBlob = new Dictionary<(uint tableHas, int recordId), byte[]>[(int)Locale.Total]; + MultiMap> _allowedHotfixOptionalData = new MultiMap>(); + MultiMap<(uint tableHas, int recordId), HotfixOptionalData>[]_hotfixOptionalData = new MultiMap<(uint tableHas, int recordId), HotfixOptionalData>[(int)Locale.Total]; MultiMap _areaGroupMembers = new MultiMap(); MultiMap _artifactPowers = new MultiMap(); @@ -2408,7 +2498,13 @@ namespace Game.DataStorage HotfixID = data.ReadInt32(); } } - + + public class HotfixOptionalData + { + public uint Key; + public byte[] Data; + } + class ChrClassesXPowerTypesRecordComparer : IComparer { public int Compare(ChrClassesXPowerTypesRecord left, ChrClassesXPowerTypesRecord right) diff --git a/Source/Game/Handlers/HotfixHandler.cs b/Source/Game/Handlers/HotfixHandler.cs index d7efba5d8..ce3a89a6d 100644 --- a/Source/Game/Handlers/HotfixHandler.cs +++ b/Source/Game/Handlers/HotfixHandler.cs @@ -29,11 +29,6 @@ namespace Game void HandleDBQueryBulk(DBQueryBulk dbQuery) { IDB2Storage store = Global.DB2Mgr.GetStorage(dbQuery.TableHash); - if (store == null) - { - Log.outError(LogFilter.Network, "CMSG_DB_QUERY_BULK: {0} requested unsupported unknown hotfix type: {1}", GetPlayerInfo(), dbQuery.TableHash); - return; - } foreach (DBQueryBulk.DBQueryRecord record in dbQuery.Queries) { @@ -41,11 +36,18 @@ namespace Game dbReply.TableHash = dbQuery.TableHash; dbReply.RecordID = record.RecordID; - if (store.HasRecord(record.RecordID)) + if (store != null && store.HasRecord(record.RecordID)) { dbReply.Status = 1; dbReply.Timestamp = (uint)GameTime.GetGameTime(); store.WriteRecord(record.RecordID, GetSessionDbcLocale(), dbReply.Data); + + var optionalDataEntries = Global.DB2Mgr.GetHotfixOptionalData(dbQuery.TableHash, record.RecordID, GetSessionDbcLocale()); + foreach (HotfixOptionalData optionalData in optionalDataEntries) + { + dbReply.Data.WriteUInt32(optionalData.Key); + dbReply.Data.WriteBytes(optionalData.Data); + } } else { @@ -80,6 +82,14 @@ namespace Game { uint pos = hotfixQueryResponse.HotfixContent.GetSize(); storage.WriteRecord((uint)hotfixRecord.RecordID, GetSessionDbcLocale(), hotfixQueryResponse.HotfixContent); + + var optionalDataEntries = Global.DB2Mgr.GetHotfixOptionalData(hotfixRecord.TableHash, (uint)hotfixRecord.RecordID, GetSessionDbcLocale()); + foreach (HotfixOptionalData optionalData in optionalDataEntries) + { + hotfixQueryResponse.HotfixContent.WriteUInt32(optionalData.Key); + hotfixQueryResponse.HotfixContent.WriteBytes(optionalData.Data); + } + hotfixData.Size.Set(hotfixQueryResponse.HotfixContent.GetSize() - pos); } else diff --git a/Source/Game/Server/WorldManager.cs b/Source/Game/Server/WorldManager.cs index 31ac4fe1c..2346c0094 100644 --- a/Source/Game/Server/WorldManager.cs +++ b/Source/Game/Server/WorldManager.cs @@ -410,6 +410,9 @@ namespace Game Log.outInfo(LogFilter.ServerLoading, "Loading hotfix info..."); Global.DB2Mgr.LoadHotfixData(); + Log.outInfo(LogFilter.ServerLoading, "Loading hotfix optional data..."); + Global.DB2Mgr.LoadHotfixOptionalData(m_availableDbcLocaleMask); + //- Load M2 fly by cameras M2Storage.LoadM2Cameras(_dataPath); diff --git a/sql/updates/hotfixes/master/2020_12_20_00_hotfixes.sql b/sql/updates/hotfixes/master/2020_12_20_00_hotfixes.sql new file mode 100644 index 000000000..dc939580d --- /dev/null +++ b/sql/updates/hotfixes/master/2020_12_20_00_hotfixes.sql @@ -0,0 +1,15 @@ +-- +-- Table structure for table `hotfix_optional_data` +-- +DROP TABLE IF EXISTS `hotfix_optional_data`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `hotfix_optional_data` ( + `TableHash` int(10) unsigned NOT NULL, + `RecordId` int(10) unsigned NOT NULL, + `locale` varchar(4) NOT NULL, + `Key` int(10) unsigned NOT NULL, + `Data` blob NOT NULL, + `VerifiedBuild` int(11) NOT NULL DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */;