Core/DataStores: Implement optional data in hotfix packets
Port From (https://github.com/TrinityCore/TrinityCore/commit/fbbf40981cc14523fa181cf6125336b254e8ae4d)
This commit is contained in:
@@ -285,7 +285,7 @@ namespace Game.DataStorage
|
|||||||
SpellVisualKitStorage = ReadDB2<SpellVisualKitRecord>("SpellVisualKit.db2", HotfixStatements.SEL_SPELL_VISUAL_KIT);
|
SpellVisualKitStorage = ReadDB2<SpellVisualKitRecord>("SpellVisualKit.db2", HotfixStatements.SEL_SPELL_VISUAL_KIT);
|
||||||
SpellXSpellVisualStorage = ReadDB2<SpellXSpellVisualRecord>("SpellXSpellVisual.db2", HotfixStatements.SEL_SPELL_X_SPELL_VISUAL);
|
SpellXSpellVisualStorage = ReadDB2<SpellXSpellVisualRecord>("SpellXSpellVisual.db2", HotfixStatements.SEL_SPELL_X_SPELL_VISUAL);
|
||||||
SummonPropertiesStorage = ReadDB2<SummonPropertiesRecord>("SummonProperties.db2", HotfixStatements.SEL_SUMMON_PROPERTIES);
|
SummonPropertiesStorage = ReadDB2<SummonPropertiesRecord>("SummonProperties.db2", HotfixStatements.SEL_SUMMON_PROPERTIES);
|
||||||
//TactKeyStorage = ReadDB2<TactKeyRecord>("TactKey.db2", HotfixStatements.SEL_TACT_KEY);
|
TactKeyStorage = ReadDB2<TactKeyRecord>("TactKey.db2", HotfixStatements.SEL_TACT_KEY);
|
||||||
TalentStorage = ReadDB2<TalentRecord>("Talent.db2", HotfixStatements.SEL_TALENT, HotfixStatements.SEL_TALENT_LOCALE);
|
TalentStorage = ReadDB2<TalentRecord>("Talent.db2", HotfixStatements.SEL_TALENT, HotfixStatements.SEL_TALENT_LOCALE);
|
||||||
TaxiNodesStorage = ReadDB2<TaxiNodesRecord>("TaxiNodes.db2", HotfixStatements.SEL_TAXI_NODES, HotfixStatements.SEL_TAXI_NODES_LOCALE);
|
TaxiNodesStorage = ReadDB2<TaxiNodesRecord>("TaxiNodes.db2", HotfixStatements.SEL_TAXI_NODES, HotfixStatements.SEL_TAXI_NODES_LOCALE);
|
||||||
TaxiPathStorage = ReadDB2<TaxiPathRecord>("TaxiPath.db2", HotfixStatements.SEL_TAXI_PATH);
|
TaxiPathStorage = ReadDB2<TaxiPathRecord>("TaxiPath.db2", HotfixStatements.SEL_TAXI_PATH);
|
||||||
@@ -641,7 +641,7 @@ namespace Game.DataStorage
|
|||||||
public static DB6Storage<SpellVisualKitRecord> SpellVisualKitStorage;
|
public static DB6Storage<SpellVisualKitRecord> SpellVisualKitStorage;
|
||||||
public static DB6Storage<SpellXSpellVisualRecord> SpellXSpellVisualStorage;
|
public static DB6Storage<SpellXSpellVisualRecord> SpellXSpellVisualStorage;
|
||||||
public static DB6Storage<SummonPropertiesRecord> SummonPropertiesStorage;
|
public static DB6Storage<SummonPropertiesRecord> SummonPropertiesStorage;
|
||||||
//public static DB6Storage<TactKeyRecord> TactKeyStorage;
|
public static DB6Storage<TactKeyRecord> TactKeyStorage;
|
||||||
public static DB6Storage<TalentRecord> TalentStorage;
|
public static DB6Storage<TalentRecord> TalentStorage;
|
||||||
public static DB6Storage<TaxiNodesRecord> TaxiNodesStorage;
|
public static DB6Storage<TaxiNodesRecord> TaxiNodesStorage;
|
||||||
public static DB6Storage<TaxiPathRecord> TaxiPathStorage;
|
public static DB6Storage<TaxiPathRecord> TaxiPathStorage;
|
||||||
|
|||||||
@@ -39,8 +39,12 @@ namespace Game.DataStorage
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class DB6Storage<T> : Dictionary<uint, T>, IDB2Storage where T : new()
|
public class DB6Storage<T> : Dictionary<uint, T>, 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));
|
SQLResult result = DB.Hotfix.Query(DB.Hotfix.GetPreparedStatement(preparedStatement));
|
||||||
if (!result.IsEmpty())
|
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;
|
base[id] = obj;
|
||||||
}
|
}
|
||||||
while (result.NextRow());
|
while (result.NextRow());
|
||||||
@@ -358,5 +362,7 @@ namespace Game.DataStorage
|
|||||||
{
|
{
|
||||||
Remove(id);
|
Remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public uint GetTableHash() { return _tableHash; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ namespace Game.DataStorage
|
|||||||
foreach (var b in reader._records)
|
foreach (var b in reader._records)
|
||||||
storage.Add((uint)b.Key, b.Value.As<T>());
|
storage.Add((uint)b.Key, b.Value.As<T>());
|
||||||
|
|
||||||
storage.LoadData(reader.Header.IdIndex, availableDb2Locales, preparedStatement, preparedStatementLocale);
|
storage.LoadData(reader.Header, availableDb2Locales, preparedStatement, preparedStatementLocale);
|
||||||
|
|
||||||
Global.DB2Mgr.AddDB2(reader.Header.TableHash, storage);
|
Global.DB2Mgr.AddDB2(reader.Header.TableHash, storage);
|
||||||
loadedFileCount++;
|
loadedFileCount++;
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ namespace Game.DataStorage
|
|||||||
_nameValidators[i] = new List<string>();
|
_nameValidators[i] = new List<string>();
|
||||||
|
|
||||||
for (var i = 0; i < (int)Locale.Total; ++i)
|
for (var i = 0; i < (int)Locale.Total; ++i)
|
||||||
_hotfixBlob[i] = new Dictionary<Tuple<uint, int>, byte[]>();
|
{
|
||||||
|
_hotfixBlob[i] = new Dictionary<(uint tableHas, int recordId), byte[]>();
|
||||||
|
_hotfixOptionalData[i] = new MultiMap<(uint tableHas, int recordId), HotfixOptionalData>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadStores()
|
public void LoadStores()
|
||||||
@@ -748,7 +751,7 @@ namespace Game.DataStorage
|
|||||||
var storeItr = _storage.LookupByKey(tableHash);
|
var storeItr = _storage.LookupByKey(tableHash);
|
||||||
if (storeItr != null)
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -758,20 +761,96 @@ namespace Game.DataStorage
|
|||||||
Locale locale = localeName.ToEnum<Locale>();
|
Locale locale = localeName.ToEnum<Locale>();
|
||||||
if (!SharedConst.IsValidLocale(locale))
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!availableDb2Locales[(int)locale])
|
if (!availableDb2Locales[(int)locale])
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
_hotfixBlob[(int)locale][Tuple.Create(tableHash, recordId)] = result.Read<byte[]>(3);
|
_hotfixBlob[(int)locale][(tableHash, recordId)] = result.Read<byte[]>(3);
|
||||||
hotfixBlobCount++;
|
hotfixBlobCount++;
|
||||||
} while (result.NextRow());
|
} while (result.NextRow());
|
||||||
|
|
||||||
Log.outInfo(LogFilter.ServerLoading, $"Loaded {hotfixBlobCount} hotfix blob records in {Time.GetMSTimeDiffToNow(oldMSTime)} ms");
|
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<uint>(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<uint>(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<string>(2);
|
||||||
|
Locale locale = localeName.ToEnum<Locale>();
|
||||||
|
|
||||||
|
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<uint>(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<byte[]>(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 uint GetHotfixCount() { return (uint)_hotfixData.Count; }
|
||||||
|
|
||||||
public List<HotfixRecord> GetHotfixData() { return _hotfixData; }
|
public List<HotfixRecord> GetHotfixData() { return _hotfixData; }
|
||||||
@@ -783,6 +862,13 @@ namespace Game.DataStorage
|
|||||||
return _hotfixBlob[(int)locale].LookupByKey(Tuple.Create(tableHash, recordId));
|
return _hotfixBlob[(int)locale].LookupByKey(Tuple.Create(tableHash, recordId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<HotfixOptionalData> 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()
|
public uint GetEmptyAnimStateID()
|
||||||
{
|
{
|
||||||
return (uint)CliDB.AnimationDataStorage.Count;
|
return (uint)CliDB.AnimationDataStorage.Count;
|
||||||
@@ -2154,9 +2240,13 @@ namespace Game.DataStorage
|
|||||||
_storage[tableHash] = store;
|
_storage[tableHash] = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delegate bool AllowedHotfixOptionalData(byte[] data);
|
||||||
|
|
||||||
Dictionary<uint, IDB2Storage> _storage = new Dictionary<uint, IDB2Storage>();
|
Dictionary<uint, IDB2Storage> _storage = new Dictionary<uint, IDB2Storage>();
|
||||||
List<HotfixRecord> _hotfixData = new List<HotfixRecord>();
|
List<HotfixRecord> _hotfixData = new List<HotfixRecord>();
|
||||||
Dictionary<Tuple<uint, int>, byte[]>[] _hotfixBlob = new Dictionary<Tuple<uint, int>, byte[]>[(int)Locale.Total];
|
Dictionary<(uint tableHas, int recordId), byte[]>[] _hotfixBlob = new Dictionary<(uint tableHas, int recordId), byte[]>[(int)Locale.Total];
|
||||||
|
MultiMap<uint, Tuple<uint, AllowedHotfixOptionalData>> _allowedHotfixOptionalData = new MultiMap<uint, Tuple<uint, AllowedHotfixOptionalData>>();
|
||||||
|
MultiMap<(uint tableHas, int recordId), HotfixOptionalData>[]_hotfixOptionalData = new MultiMap<(uint tableHas, int recordId), HotfixOptionalData>[(int)Locale.Total];
|
||||||
|
|
||||||
MultiMap<uint, uint> _areaGroupMembers = new MultiMap<uint, uint>();
|
MultiMap<uint, uint> _areaGroupMembers = new MultiMap<uint, uint>();
|
||||||
MultiMap<uint, ArtifactPowerRecord> _artifactPowers = new MultiMap<uint, ArtifactPowerRecord>();
|
MultiMap<uint, ArtifactPowerRecord> _artifactPowers = new MultiMap<uint, ArtifactPowerRecord>();
|
||||||
@@ -2409,6 +2499,12 @@ namespace Game.DataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class HotfixOptionalData
|
||||||
|
{
|
||||||
|
public uint Key;
|
||||||
|
public byte[] Data;
|
||||||
|
}
|
||||||
|
|
||||||
class ChrClassesXPowerTypesRecordComparer : IComparer<ChrClassesXPowerTypesRecord>
|
class ChrClassesXPowerTypesRecordComparer : IComparer<ChrClassesXPowerTypesRecord>
|
||||||
{
|
{
|
||||||
public int Compare(ChrClassesXPowerTypesRecord left, ChrClassesXPowerTypesRecord right)
|
public int Compare(ChrClassesXPowerTypesRecord left, ChrClassesXPowerTypesRecord right)
|
||||||
|
|||||||
@@ -29,11 +29,6 @@ namespace Game
|
|||||||
void HandleDBQueryBulk(DBQueryBulk dbQuery)
|
void HandleDBQueryBulk(DBQueryBulk dbQuery)
|
||||||
{
|
{
|
||||||
IDB2Storage store = Global.DB2Mgr.GetStorage(dbQuery.TableHash);
|
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)
|
foreach (DBQueryBulk.DBQueryRecord record in dbQuery.Queries)
|
||||||
{
|
{
|
||||||
@@ -41,11 +36,18 @@ namespace Game
|
|||||||
dbReply.TableHash = dbQuery.TableHash;
|
dbReply.TableHash = dbQuery.TableHash;
|
||||||
dbReply.RecordID = record.RecordID;
|
dbReply.RecordID = record.RecordID;
|
||||||
|
|
||||||
if (store.HasRecord(record.RecordID))
|
if (store != null && store.HasRecord(record.RecordID))
|
||||||
{
|
{
|
||||||
dbReply.Status = 1;
|
dbReply.Status = 1;
|
||||||
dbReply.Timestamp = (uint)GameTime.GetGameTime();
|
dbReply.Timestamp = (uint)GameTime.GetGameTime();
|
||||||
store.WriteRecord(record.RecordID, GetSessionDbcLocale(), dbReply.Data);
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -80,6 +82,14 @@ namespace Game
|
|||||||
{
|
{
|
||||||
uint pos = hotfixQueryResponse.HotfixContent.GetSize();
|
uint pos = hotfixQueryResponse.HotfixContent.GetSize();
|
||||||
storage.WriteRecord((uint)hotfixRecord.RecordID, GetSessionDbcLocale(), hotfixQueryResponse.HotfixContent);
|
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);
|
hotfixData.Size.Set(hotfixQueryResponse.HotfixContent.GetSize() - pos);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -410,6 +410,9 @@ namespace Game
|
|||||||
Log.outInfo(LogFilter.ServerLoading, "Loading hotfix info...");
|
Log.outInfo(LogFilter.ServerLoading, "Loading hotfix info...");
|
||||||
Global.DB2Mgr.LoadHotfixData();
|
Global.DB2Mgr.LoadHotfixData();
|
||||||
|
|
||||||
|
Log.outInfo(LogFilter.ServerLoading, "Loading hotfix optional data...");
|
||||||
|
Global.DB2Mgr.LoadHotfixOptionalData(m_availableDbcLocaleMask);
|
||||||
|
|
||||||
//- Load M2 fly by cameras
|
//- Load M2 fly by cameras
|
||||||
M2Storage.LoadM2Cameras(_dataPath);
|
M2Storage.LoadM2Cameras(_dataPath);
|
||||||
|
|
||||||
|
|||||||
@@ -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 */;
|
||||||
Reference in New Issue
Block a user