diff --git a/README.md b/README.md index f88fef48..db04a5b6 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,14 @@ Works with all expansions! - Powerful search - Cheats - Items - - Extract relic/charm from items at no cost, keeping both - - Modify the relic/charm/artefact completion bonus - - Complete relic/charm from a single piece - - Craft an artifact from its recipe - - Create missing set pieces + - [Extract relic/charm from items at no cost, keeping both](documentation/AFFIXES.md#RelicRemoval) + - [Modify the relic/charm/artefact completion bonus](documentation/AFFIXES.md#RelicCompletion) + - [Complete relic/charm from a single piece](documentation/AFFIXES.md#RelicCompleteStack) + - [Craft an artifact from its recipe](documentation/AFFIXES.md#Formula) + - [Change item seed](documentation/AFFIXES.md#Seed) + - [Create missing set pieces](documentation/AFFIXES.md#MissingSetPiece) - [Craft custom items](/documentation/FORGE.md) + - [Change items affixes](documentation/AFFIXES.md) - Duplicate any item - Characters - Redisribute attribute points diff --git a/TQVaultAE.sln b/TQVaultAE.sln index 90d2a3e0..e2eb44c2 100644 --- a/TQVaultAE.sln +++ b/TQVaultAE.sln @@ -11,6 +11,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TQVaultAE.GUI", "src\TQVaul EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{20915EFE-C25B-499E-9CB6-5888F29BC32F}" ProjectSection(SolutionItems) = preProject + documentation\AFFIXES.md = documentation\AFFIXES.md CHANGELOG.md = CHANGELOG.md CONTRIBUTING.md = CONTRIBUTING.md documentation\FORGE.md = documentation\FORGE.md diff --git a/documentation/AFFIXES.md b/documentation/AFFIXES.md new file mode 100644 index 00000000..b12df3d3 --- /dev/null +++ b/documentation/AFFIXES.md @@ -0,0 +1,106 @@ +# Item editing features + +Beside the [Forge](FORGE.md) that can be overkill, TQvault let you tweek some items in a respectful manner. + +You can choose item affixes from regular loot table instead of farming duplicate and using the forge. + +### _Theory crafting made easy!!_ + +_**Note : "Item editing feature" must be enable in the settings.**_ + +--- + +## Table of contents +* [Prefix change](#Prefix) +* [Suffix change](#Suffix) +* [Broken items](#Broken) +* [Affixes removal](#Remove) +* [Affixes display mode](#DisplayMode) +* [Artefact creation](#Formula) +* [Artefact completion bonus change](#Artefact) +* [Relic and charm completion](#RelicCompleteStack) +* [Relic and charm completion bonus change](#RelicCompletion) +* [Relics removal](#RelicRemoval) +* [Socketed relic and charm completion bonus change](#SocketedRelicCompletion) +* [Item seed change](#Seed) +* [Create missing set pieces](#MissingSetPiece) + +--- + +### Prefix change + +![Prefix change](affixes/prefix.png) + +--- + +### Suffix change + +![Suffix change](affixes/suffix.png) + +--- + +### Broken affix when available + +![Broken affix when available](affixes/broken.png) + +--- + +### Affixes removal + +![Affixes removal](affixes/removeaffixes.png) + +--- + +### Affixes display mode + +![Affixes removal](affixes/displaymode.png) + +--- + +### Artefact creation + +![Artefact creation](affixes/artefactcreation.png) + +--- + +### Artefact completion bonus change + +![Artefact completion bonus change](affixes/artefactcompletion.png) + +--- + +### Relic and charm completion + +![Relic and charm completion](affixes/reliccompletestack.png) + +--- + +### Relic and charm completion bonus change + +![Relic and charm completion bonus change](affixes/reliccompletion.png) + +--- + +### Socketed relic and charm completion bonus change + +![Socketed relic and charm completion bonus change](affixes/socketedreliccompletion.png) + +--- + +### Relics removal + +![Relics removal](affixes/removerelic.png) + +--- + +### Item seed change + +| ![Open Seed form](affixes/seed1.png) | ![Change Seed](affixes/seed2.png) + +--- + +### Create missing set pieces + +![Create missing set pieces](affixes/missingset.png) + + diff --git a/documentation/affixes/artefactcompletion.png b/documentation/affixes/artefactcompletion.png new file mode 100644 index 00000000..658eb4c8 Binary files /dev/null and b/documentation/affixes/artefactcompletion.png differ diff --git a/documentation/affixes/artefactcreation.png b/documentation/affixes/artefactcreation.png new file mode 100644 index 00000000..b1cc4a7b Binary files /dev/null and b/documentation/affixes/artefactcreation.png differ diff --git a/documentation/affixes/broken.png b/documentation/affixes/broken.png new file mode 100644 index 00000000..09e32ad0 Binary files /dev/null and b/documentation/affixes/broken.png differ diff --git a/documentation/affixes/displaymode.png b/documentation/affixes/displaymode.png new file mode 100644 index 00000000..7db6dc07 Binary files /dev/null and b/documentation/affixes/displaymode.png differ diff --git a/documentation/affixes/missingset.png b/documentation/affixes/missingset.png new file mode 100644 index 00000000..96a7590a Binary files /dev/null and b/documentation/affixes/missingset.png differ diff --git a/documentation/affixes/prefix.png b/documentation/affixes/prefix.png new file mode 100644 index 00000000..ac7a50f7 Binary files /dev/null and b/documentation/affixes/prefix.png differ diff --git a/documentation/affixes/reliccompletestack.png b/documentation/affixes/reliccompletestack.png new file mode 100644 index 00000000..89201fec Binary files /dev/null and b/documentation/affixes/reliccompletestack.png differ diff --git a/documentation/affixes/reliccompletion.png b/documentation/affixes/reliccompletion.png new file mode 100644 index 00000000..3f4a7c94 Binary files /dev/null and b/documentation/affixes/reliccompletion.png differ diff --git a/documentation/affixes/removeaffixes.png b/documentation/affixes/removeaffixes.png new file mode 100644 index 00000000..52614250 Binary files /dev/null and b/documentation/affixes/removeaffixes.png differ diff --git a/documentation/affixes/removerelic.png b/documentation/affixes/removerelic.png new file mode 100644 index 00000000..6457c913 Binary files /dev/null and b/documentation/affixes/removerelic.png differ diff --git a/documentation/affixes/seed1.png b/documentation/affixes/seed1.png new file mode 100644 index 00000000..b8bad89c Binary files /dev/null and b/documentation/affixes/seed1.png differ diff --git a/documentation/affixes/seed2.png b/documentation/affixes/seed2.png new file mode 100644 index 00000000..5fefa8a9 Binary files /dev/null and b/documentation/affixes/seed2.png differ diff --git a/documentation/affixes/socketedreliccompletion.png b/documentation/affixes/socketedreliccompletion.png new file mode 100644 index 00000000..22031b9a Binary files /dev/null and b/documentation/affixes/socketedreliccompletion.png differ diff --git a/documentation/affixes/suffix.png b/documentation/affixes/suffix.png new file mode 100644 index 00000000..03c1c213 Binary files /dev/null and b/documentation/affixes/suffix.png differ diff --git a/src/TQVaultAE.Data/Database.cs b/src/TQVaultAE.Data/Database.cs index d6131739..62fa3b00 100644 --- a/src/TQVaultAE.Data/Database.cs +++ b/src/TQVaultAE.Data/Database.cs @@ -3,700 +3,856 @@ // Copyright (c) Brandon Wallace and Jesse Calhoun. All rights reserved. // //----------------------------------------------------------------------- -namespace TQVaultAE.Data +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using TQVaultAE.Config; +using TQVaultAE.Domain.Contracts.Providers; +using TQVaultAE.Domain.Contracts.Services; +using TQVaultAE.Domain.Entities; +using TQVaultAE.Domain.Helpers; +using TQVaultAE.Logs; + +namespace TQVaultAE.Data; + +/// +/// Reads a Titan Quest database file. +/// +public class Database : IDatabase { - using Microsoft.Extensions.Logging; - using System; - using System.Collections.Concurrent; - using System.Globalization; - using System.IO; - using System.Linq; - using System.Text; - using System.Text.RegularExpressions; - using TQVaultAE.Config; - using TQVaultAE.Domain.Contracts.Providers; - using TQVaultAE.Domain.Contracts.Services; - using TQVaultAE.Domain.Entities; - using TQVaultAE.Domain.Helpers; - using TQVaultAE.Logs; + private const StringComparison noCase = StringComparison.OrdinalIgnoreCase; + + #region Record Class Names + + private const string RCLASS_LOOTRANDOMIZER = "LootRandomizer"; + private const string RCLASS_LOOTITEMTABLE_FIXEDWEIGHT = "LootItemTable_FixedWeight"; + private const string RCLASS_LOOTITEMTABLE_DYNWEIGHT = "LootItemTable_DynWeight"; + + #endregion + + private readonly ILogger Log = null; + + #region Database Fields + + /// + /// Dictionary of all database info records + /// + private LazyConcurrentDictionary infoDB = new LazyConcurrentDictionary(); + + /// + /// Dictionary of all text database entries + /// + private ConcurrentDictionary textDB = new ConcurrentDictionary(); + + /// + /// Dictionary of all associated arc files in the database. + /// + private LazyConcurrentDictionary arcFiles = new LazyConcurrentDictionary(); + + /// + /// Dictionary of all records dataset loaded from the database. + /// + private LazyConcurrentDictionary resourcesData = new LazyConcurrentDictionary(); + + /// + /// Dictionary of all record collections loaded from the database. + /// + private LazyConcurrentDictionary dbRecordCollections = new LazyConcurrentDictionary(); /// - /// Reads a Titan Quest database file. + /// Game language to support setting language in UI /// - public class Database : IDatabase + private string gameLanguage; + + private readonly IArcFileProvider arcProv; + private readonly IArzFileProvider arzProv; + private readonly IItemAttributeProvider ItemAttributeProvider; + private readonly IGamePathService GamePathResolver; + private readonly ITQDataService TQData; + + #endregion Database Fields + + /// + /// Initializes a new instance of the Database class. + /// + public Database( + ILogger log + , IArcFileProvider arcFileProvider + , IArzFileProvider arzFileProvider + , IItemAttributeProvider itemAttributeProvider + , IGamePathService gamePathResolver + , ITQDataService tQData + ) { - private readonly ILogger Log = null; - - #region Database Fields - - /// - /// Dictionary of all database info records - /// - private LazyConcurrentDictionary infoDB = new LazyConcurrentDictionary(); - - /// - /// Dictionary of all text database entries - /// - private ConcurrentDictionary textDB = new ConcurrentDictionary(); - - /// - /// Dictionary of all associated arc files in the database. - /// - private LazyConcurrentDictionary arcFiles = new LazyConcurrentDictionary(); - - /// - /// Dictionary of all records dataset loaded from the database. - /// - private LazyConcurrentDictionary resourcesData = new LazyConcurrentDictionary(); - - /// - /// Dictionary of all record collections loaded from the database. - /// - private LazyConcurrentDictionary dbRecordCollections = new LazyConcurrentDictionary(); - - /// - /// Game language to support setting language in UI - /// - private string gameLanguage; - - private readonly IArcFileProvider arcProv; - private readonly IArzFileProvider arzProv; - private readonly IItemAttributeProvider ItemAttributeProvider; - private readonly IGamePathService GamePathResolver; - private readonly ITQDataService TQData; - - #endregion Database Fields - - /// - /// Initializes a new instance of the Database class. - /// - public Database( - ILogger log - , IArcFileProvider arcFileProvider - , IArzFileProvider arzFileProvider - , IItemAttributeProvider itemAttributeProvider - , IGamePathService gamePathResolver - , ITQDataService tQData - ) - { - this.Log = log; - this.AutoDetectLanguage = Config.Settings.Default.AutoDetectLanguage; - this.TQLanguage = Config.Settings.Default.TQLanguage; - this.arcProv = arcFileProvider; - this.arzProv = arzFileProvider; - this.ItemAttributeProvider = itemAttributeProvider; - this.GamePathResolver = gamePathResolver; - this.TQData = tQData; - this.LoadDBFile(); - } + this.Log = log; + this.AutoDetectLanguage = Config.Settings.Default.AutoDetectLanguage; + this.TQLanguage = Config.Settings.Default.TQLanguage; + this.arcProv = arcFileProvider; + this.arzProv = arzFileProvider; + this.ItemAttributeProvider = itemAttributeProvider; + this.GamePathResolver = gamePathResolver; + this.TQData = tQData; + this.LoadDBFile(); + } - #region Database Properties + #region Database Properties - /// - /// Gets or sets a value indicating whether the game language is being auto detected. - /// - public bool AutoDetectLanguage { get; set; } + /// + /// Mapping between ItemId and Affixes LootTable + /// + private static ReadOnlyDictionary> ItemAffixTableMap; - /// - /// Gets or sets the game language from the config file. - /// - public string TQLanguage { get; set; } + /// + /// Gets or sets a value indicating whether the game language is being auto detected. + /// + public bool AutoDetectLanguage { get; set; } + /// + /// Gets or sets the game language from the config file. + /// + public string TQLanguage { get; set; } - /// - /// Gets the instance of the Titan Quest Database ArzFile. - /// - public ArzFile ArzFile { get; private set; } - /// - /// Gets the instance of the Immortal Throne Database ArzFile. - /// - public ArzFile ArzFileIT { get; private set; } + /// + /// Gets the instance of the Titan Quest Database ArzFile. + /// + public ArzFile ArzFile { get; private set; } + + /// + /// Gets the instance of the Immortal Throne Database ArzFile. + /// + public ArzFile ArzFileIT { get; private set; } - /// - /// Gets the instance of a custom map Database ArzFile. - /// - public ArzFile ArzFileMod { get; private set; } + /// + /// Gets the instance of a custom map Database ArzFile. + /// + public ArzFile ArzFileMod { get; private set; } - /// - /// Gets the game language setting as a an English DisplayName. - /// - /// Changed to property by VillageIdiot to support changing of Language in UI - public string GameLanguage + /// + /// Gets the game language setting as a an English DisplayName. + /// + /// Changed to property by VillageIdiot to support changing of Language in UI + public string GameLanguage + { + get { - get + // Added by VillageIdiot + // Check if the user configured the language + if (this.gameLanguage == null) { - // Added by VillageIdiot - // Check if the user configured the language - if (this.gameLanguage == null) - { - if (!this.AutoDetectLanguage) - this.gameLanguage = this.TQLanguage; - } + if (!this.AutoDetectLanguage) + this.gameLanguage = this.TQLanguage; + } - // Try to read the language from the settings file - if (string.IsNullOrEmpty(this.gameLanguage)) + // Try to read the language from the settings file + if (string.IsNullOrEmpty(this.gameLanguage)) + { + try { - try + string optionsFile = GamePathResolver.TQSettingsFile; + if (!File.Exists(optionsFile)) { - string optionsFile = GamePathResolver.TQSettingsFile; - if (!File.Exists(optionsFile)) - { - // Try IT Folder if there is no settings file in TQ Folder - optionsFile = GamePathResolver.ITSettingsFile; - } - if (File.Exists(optionsFile)) + // Try IT Folder if there is no settings file in TQ Folder + optionsFile = GamePathResolver.ITSettingsFile; + } + if (File.Exists(optionsFile)) + { + var fileContent = File.ReadAllText(optionsFile); + var match = Regex.Match(fileContent, @"(?i)language\s*=\s*(""(?[^""]+)""|(?[^\r\n]*))[\r\n]"); + if (match.Success) { - var fileContent = File.ReadAllText(optionsFile); - var match = Regex.Match(fileContent, @"(?i)language\s*=\s*(""(?[^""]+)""|(?[^\r\n]*))[\r\n]"); - if (match.Success) - { - this.gameLanguage = match.Groups["Language"].Value.ToUpperInvariant(); - return this.gameLanguage; - } - - return null; + this.gameLanguage = match.Groups["Language"].Value.ToUpperInvariant(); + return this.gameLanguage; } return null; } - catch (IOException exception) - { - Log.ErrorException(exception); - return null; - } - } - // Added by VillageIdiot - // We have something so we need to return it - // This was added to support setting the language in the config file - return this.gameLanguage; + return null; + } + catch (IOException exception) + { + Log.ErrorException(exception); + return null; + } } + + // Added by VillageIdiot + // We have something so we need to return it + // This was added to support setting the language in the config file + return this.gameLanguage; } + } - #endregion Database Properties + #endregion Database Properties - #region Database Public Methods + #region Database Public Methods - #region Database Public Static Methods + #region Database Public Static Methods - /// - /// Used to Extract an ARC file into the destination directory. - /// The ARC file will not be added to the cache. - /// - /// Added by VillageIdiot - /// Name of the arc file - /// Destination path for extracted data - /// Returns true on success otherwise false - public bool ExtractArcFile(string arcFileName, string destination) + /// + /// Used to Extract an ARC file into the destination directory. + /// The ARC file will not be added to the cache. + /// + /// Added by VillageIdiot + /// Name of the arc file + /// Destination path for extracted data + /// Returns true on success otherwise false + public bool ExtractArcFile(string arcFileName, string destination) + { + bool result = false; + + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Database.ExtractARCFile('{0}', '{1}')", arcFileName, destination); + + try { - bool result = false; + ArcFile arcFile = new ArcFile(arcFileName); - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Database.ExtractARCFile('{0}', '{1}')", arcFileName, destination); + // Extract the files + result = arcProv.ExtractArcFile(arcFile, destination); + } + catch (IOException exception) + { + Log.LogError(exception, "Exception occurred"); + result = false; + } - try - { - ArcFile arcFile = new ArcFile(arcFileName); + if (TQDebug.DatabaseDebugLevel > 1) + Log.LogDebug("Extraction Result = {0}", result); - // Extract the files - result = arcProv.ExtractArcFile(arcFile, destination); - } - catch (IOException exception) - { - Log.LogError(exception, "Exception occurred"); - result = false; - } + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Exiting Database.ReadARCFile()"); - if (TQDebug.DatabaseDebugLevel > 1) - Log.LogDebug("Extraction Result = {0}", result); + return result; + } - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Exiting Database.ReadARCFile()"); + #endregion Database Public Static Methods - return result; - } + /// + /// Gets the Infor for a specific item id. + /// + /// Item ID which we are looking up. Will be normalized internally. + /// Returns Infor for item ID and NULL if not found. + public Info GetInfo(string itemId) + { + if (string.IsNullOrEmpty(itemId)) + return null; - #endregion Database Public Static Methods + itemId = TQData.NormalizeRecordPath(itemId); - /// - /// Gets the Infor for a specific item id. - /// - /// Item ID which we are looking up. Will be normalized internally. - /// Returns Infor for item ID and NULL if not found. - public Info GetInfo(string itemId) + return this.infoDB.GetOrAddAtomic(itemId, k => { - if (string.IsNullOrEmpty(itemId)) + DBRecordCollection record = GetRecordFromFile(k); + + if (record == null) return null; - itemId = TQData.NormalizeRecordPath(itemId); + return new Info(record); + }); + } - return this.infoDB.GetOrAddAtomic(itemId, k => - { - DBRecordCollection record = GetRecordFromFile(k); + /// + /// Uses the text database to convert the tag to a name in the localized language. + /// The tag is normalized to upper case internally. + /// + /// Tag to be looked up in the text database normalized to upper case. + /// Returns localized string, empty string if it cannot find a string or "?ErrorName?" in case of uncaught exception. + public string GetFriendlyName(string tagId) + => this.textDB.TryGetValue(tagId.ToUpperInvariant(), out var text) ? text : string.Empty; - if (record == null) - return null; + /// + /// Gets the formatted string for the variable attribute. + /// + /// variable for which we are making a nice string. + /// Formatted string in the format of: Attribute: value + public string VariableToStringNice(Variable variable) + => $"{this.GetItemAttributeFriendlyText(variable.Name)}: {variable.ToStringValue()}"; - return new Info(record); - }); + /// + /// Converts the item attribute to a name in the localized language + /// + /// Item attribure to be looked up. + /// Flag for whether the variable is added to the text string. + /// Returns localized item attribute + public string GetItemAttributeFriendlyText(string itemAttribute, bool addVariable = true) + { + ItemAttributesData data = ItemAttributeProvider.GetAttributeData(itemAttribute); + if (data == null) + { + this.Log.LogDebug($"Attribute unknown : {itemAttribute}"); + return string.Concat("?", itemAttribute, "?"); } - /// - /// Uses the text database to convert the tag to a name in the localized language. - /// The tag is normalized to upper case internally. - /// - /// Tag to be looked up in the text database normalized to upper case. - /// Returns localized string, empty string if it cannot find a string or "?ErrorName?" in case of uncaught exception. - public string GetFriendlyName(string tagId) - => this.textDB.TryGetValue(tagId.ToUpperInvariant(), out var text) ? text : string.Empty; - - /// - /// Gets the formatted string for the variable attribute. - /// - /// variable for which we are making a nice string. - /// Formatted string in the format of: Attribute: value - public string VariableToStringNice(Variable variable) - => $"{this.GetItemAttributeFriendlyText(variable.Name)}: {variable.ToStringValue()}"; - - /// - /// Converts the item attribute to a name in the localized language - /// - /// Item attribure to be looked up. - /// Flag for whether the variable is added to the text string. - /// Returns localized item attribute - public string GetItemAttributeFriendlyText(string itemAttribute, bool addVariable = true) - { - ItemAttributesData data = ItemAttributeProvider.GetAttributeData(itemAttribute); - if (data == null) - { - this.Log.LogDebug($"Attribute unknown : {itemAttribute}"); - return string.Concat("?", itemAttribute, "?"); - } + string attributeTextTag = ItemAttributeProvider.GetAttributeTextTag(data); + if (string.IsNullOrEmpty(attributeTextTag)) + { + this.Log.LogDebug($"Attribute unknown : {itemAttribute}"); + return string.Concat("?", itemAttribute, "?"); + } + + string textFromTag = this.GetFriendlyName(attributeTextTag); + if (string.IsNullOrEmpty(textFromTag)) + { + textFromTag = string.Concat("ATTR<", itemAttribute, "> TAG<"); + textFromTag = string.Concat(textFromTag, attributeTextTag, ">"); + } - string attributeTextTag = ItemAttributeProvider.GetAttributeTextTag(data); - if (string.IsNullOrEmpty(attributeTextTag)) + if (addVariable && data.Variable.Length > 0) + textFromTag = string.Concat(textFromTag, " ", data.Variable); + + return textFromTag; + } + + /// + /// Load our data from the db file. + /// + private void LoadDBFile() + { + this.LoadTextDB(); + this.LoadARZFile(); + this.BuildItemAffixTableMap(); + } + + /// + /// Extract all LootRandomizer + /// + /// + public ReadOnlyCollection ReadLootRandomizerList() + { + // Load all available loot randomizer + var lootRandomizerList = new[] { this.ArzFileMod, this.ArzFile } + .Where(db => db is not null) + .SelectMany(db => + db.RecordInfo.Where(r => r.Value.RecordType.Equals(RCLASS_LOOTRANDOMIZER, noCase)) + ) + .Select(r => { - this.Log.LogDebug($"Attribute unknown : {itemAttribute}"); - return string.Concat("?", itemAttribute, "?"); - } + var rec = GetRecordFromFile(r.Key); + + string Tag = rec.GetString(Variable.KEY_LOOTRANDNAME, 0); + int Cost = rec.GetInt32(Variable.KEY_LOOTRANDCOST, 0); + int LevelRequirement = rec.GetInt32(Variable.KEY_LEVELREQ, 0); + string ItemClass = rec.GetString(Variable.KEY_ITEMCLASS, 0); + string FileDescription = rec.GetString(Variable.KEY_FILEDESC, 0); + + var IsEmpty = string.IsNullOrWhiteSpace(Tag) + && string.IsNullOrWhiteSpace(ItemClass) + && string.IsNullOrWhiteSpace(FileDescription) + && Cost == 0 + && LevelRequirement == 0; + + if (IsEmpty) return null; + + var prettyFileName = r.Value.ID.PrettyFileName();// Use r.Value.ID because i need the non Normalized record Id + var exploded = prettyFileName.ExplodePrettyFileName(); + var val = new LootRandomizerItem( + r.Key + , Tag + , Cost + , LevelRequirement + , ItemClass + , FileDescription + , string.Empty // Translation + , prettyFileName + , exploded.Effect + , exploded.Number + ); + + return val; + }) + .Where(db => db is not null) + .ToList().AsReadOnly(); + + return lootRandomizerList; + } + + #region ItemAffixTableMap + - string textFromTag = this.GetFriendlyName(attributeTextTag); - if (string.IsNullOrEmpty(textFromTag)) + /// + /// Create a map between Items and affix loot tables + /// + private void BuildItemAffixTableMap() + { + // Load all available loot table + var data = new[] { this.ArzFileMod, this.ArzFile } + .Where(db => db is not null) + .SelectMany(db => db.RecordInfo + .Where(r => + r.Value.RecordType.Equals(RCLASS_LOOTITEMTABLE_FIXEDWEIGHT, noCase) + || r.Value.RecordType.Equals(RCLASS_LOOTITEMTABLE_DYNWEIGHT, noCase) + ) + ) + .Select(r => { - textFromTag = string.Concat("ATTR<", itemAttribute, "> TAG<"); - textFromTag = string.Concat(textFromTag, attributeTextTag, ">"); - } + var rec = GetRecordFromFile(r.Key); + var lootNames = new List(); + var records = new List<(string brokenTable, string prefixTable, string suffixTable, List lootNames)>(); - if (addVariable && data.Variable.Length > 0) - textFromTag = string.Concat(textFromTag, " ", data.Variable); + int forceReadMax = 3, lootTableIdx = 1; - return textFromTag; - } + // Read loot names + switch (r.Value.RecordType) + { + case RCLASS_LOOTITEMTABLE_FIXEDWEIGHT: + int lootNameIdx = 0; + bool noMore = false; + do // Because sometimes it start at "lootName2" + { + var name = "lootName" + ++lootNameIdx; + var lootName = rec.GetString(name, 0); + var haslootName = !string.IsNullOrWhiteSpace(lootName); + if (!haslootName) + { + if (lootNameIdx <= forceReadMax) continue; + + noMore = true; continue; + } + + lootNames.Add(lootName); + } + while (!noMore); + + break; + default: // LootItemTable_DynWeight + var itemNames = rec.GetAllStrings("itemNames"); + if (itemNames is not null && itemNames.Length > 0) + lootNames.AddRange(itemNames); + break; + } + + // Read loot tables + bool isOver; + do + { + var brokenTable = rec.GetString("brokenRandomizerName" + lootTableIdx, 0); + var prefixTable = rec.GetString("prefixRandomizerName" + lootTableIdx, 0); + var suffixTable = rec.GetString("suffixRandomizerName" + lootTableIdx, 0); + + isOver = string.IsNullOrWhiteSpace(brokenTable) + && string.IsNullOrWhiteSpace(prefixTable) + && string.IsNullOrWhiteSpace(suffixTable); + + if (isOver) continue; + + if (lootNames.Count > 0) + records.Add((brokenTable, prefixTable, suffixTable, lootNames)); + + lootTableIdx++; + } + while (!isOver); + + return records; + }) + .SelectMany(i => i) + .SelectMany(itemAffix => itemAffix.lootNames + .Select(itemId => new + { + itemId = TQData.NormalizeRecordPath(itemId), + itemAffix.suffixTable, + itemAffix.prefixTable, + itemAffix.brokenTable + }) + ) // Flatten + .GroupBy(i => i.itemId) + .ToDictionary(i => + i.Key + , j => j + .Select(k => new AffixTableMapItem(k.brokenTable, k.prefixTable, k.suffixTable)).Distinct() + .ToList().AsReadOnly() + ); + + ItemAffixTableMap = new ReadOnlyDictionary>(data); + } + + public ReadOnlyCollection GetItemAffixTableMap(string itemId) + { + itemId = TQData.NormalizeRecordPath(itemId); + + var affixmap = ItemAffixTableMap.SingleOrDefault(i => i.Key == itemId); + if (affixmap.Key is null) return null; + + return affixmap.Value; + } + + #endregion + + /// + /// Gets a DBRecord for the specified item ID string. + /// + /// + /// Changed by VillageIdiot + /// Changed search order so that IT records have precedence of TQ records. + /// Add Custom Map database. Custom Map records have precedence over IT records. + /// + /// Item Id which we are looking up + /// Returns the DBRecord for the item Id + public DBRecordCollection GetRecordFromFile(string itemId) + { + itemId = TQData.NormalizeRecordPath(itemId); - /// - /// Load our data from the db file. - /// - private void LoadDBFile() + var cachedDBRecordCollection = this.dbRecordCollections.GetOrAddAtomic(itemId, key => { - this.LoadTextDB(); - this.LoadARZFile(); - } - /// - /// Gets a DBRecord for the specified item ID string. - /// - /// - /// Changed by VillageIdiot - /// Changed search order so that IT records have precedence of TQ records. - /// Add Custom Map database. Custom Map records have precedence over IT records. - /// - /// Item Id which we are looking up - /// Returns the DBRecord for the item Id - public DBRecordCollection GetRecordFromFile(string itemId) - { - itemId = TQData.NormalizeRecordPath(itemId); - - var cachedDBRecordCollection = this.dbRecordCollections.GetOrAddAtomic(itemId, key => + if (this.ArzFileMod != null) { - - if (this.ArzFileMod != null) + DBRecordCollection recordMod = arzProv.GetItem(this.ArzFileMod, key); + if (recordMod != null) { - DBRecordCollection recordMod = arzProv.GetItem(this.ArzFileMod, key); - if (recordMod != null) - { - // Custom Map records have highest precedence. - return recordMod; - } + // Custom Map records have highest precedence. + return recordMod; } + } - if (this.ArzFileIT != null) + if (this.ArzFileIT != null) + { + // see if it's in IT ARZ file + DBRecordCollection recordIT = arzProv.GetItem(this.ArzFileIT, key); + if (recordIT != null) { - // see if it's in IT ARZ file - DBRecordCollection recordIT = arzProv.GetItem(this.ArzFileIT, key); - if (recordIT != null) - { - // IT file takes precedence over TQ. - return recordIT; - } + // IT file takes precedence over TQ. + return recordIT; } + } - return arzProv.GetItem(ArzFile, key); - }); + return arzProv.GetItem(ArzFile, key); + }); + + return cachedDBRecordCollection; + } - return cachedDBRecordCollection; - } + /// + /// Gets a resource from the database using the resource Id. + /// Modified by VillageIdiot to support loading resources from a custom map folder. + /// + /// Resource which we are fetching + /// Retruns a byte array of the resource. + public byte[] LoadResource(string resourceId) + { + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Database.LoadResource({0})", resourceId); + + resourceId = TQData.NormalizeRecordPath(resourceId); - /// - /// Gets a resource from the database using the resource Id. - /// Modified by VillageIdiot to support loading resources from a custom map folder. - /// - /// Resource which we are fetching - /// Retruns a byte array of the resource. - public byte[] LoadResource(string resourceId) + if (TQDebug.DatabaseDebugLevel > 1) + Log.LogDebug(" Normalized({0})", resourceId); + + byte[] cachedArcFileData = this.resourcesData.GetOrAddAtomic(resourceId, key => { - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Database.LoadResource({0})", resourceId); + var resourceIdSplited = key.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);// hguy : easier to understand than substring everywhere - resourceId = TQData.NormalizeRecordPath(resourceId); + // not a proper resourceID. + if (resourceIdSplited.Length == 1) + return null; + + // First we need to figure out the correct file to + // open, by grabbing it off the front of the resourceID + + string arcFile; bool isDLC = false; + string rootFolder; + byte[] arcFileData = null; + string arcFileBase = resourceIdSplited.First(); if (TQDebug.DatabaseDebugLevel > 1) - Log.LogDebug(" Normalized({0})", resourceId); + Log.LogDebug("arcFileBase = {0}", arcFileBase); - byte[] cachedArcFileData = this.resourcesData.GetOrAddAtomic(resourceId, key => + // Added by VillageIdiot + // Check the mod folder for the image resource. + if (GamePathResolver.IsCustom) { - var resourceIdSplited = key.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);// hguy : easier to understand than substring everywhere + if (TQDebug.DatabaseDebugLevel > 1) + Log.LogDebug("Checking Custom Resources."); - // not a proper resourceID. - if (resourceIdSplited.Length == 1) - return null; + rootFolder = Path.Combine(GamePathResolver.MapName, "resources"); - // First we need to figure out the correct file to - // open, by grabbing it off the front of the resourceID + arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); + arcFileData = this.ReadARCFile(arcFile, key); - string arcFile; bool isDLC = false; - string rootFolder; - byte[] arcFileData = null; - string arcFileBase = resourceIdSplited.First(); + if (TQDebug.DatabaseDebugLevel > 1 && arcFileData is not null) + Log.LogDebug(@"Custom resource found ""{resourceId}"" into ""{arcFile}""", key, arcFile); + } + // We either didn't load the resource or didn't find what we were looking for so check the normal game resources. + if (arcFileData == null) + { + // See if this guy is from Immortal Throne expansion pack. if (TQDebug.DatabaseDebugLevel > 1) - Log.LogDebug("arcFileBase = {0}", arcFileBase); + Log.LogDebug("Checking IT Resources."); - // Added by VillageIdiot - // Check the mod folder for the image resource. - if (GamePathResolver.IsCustom) + (arcFile, isDLC) = this.GamePathResolver.ResolveArcFileName(key); + if (isDLC) { - if (TQDebug.DatabaseDebugLevel > 1) - Log.LogDebug("Checking Custom Resources."); + // not a proper resourceID. + if (resourceIdSplited.Length == 2) + return null; - rootFolder = Path.Combine(GamePathResolver.MapName, "resources"); + arcFileBase = resourceIdSplited[1]; + key = resourceIdSplited.Skip(1).JoinString("\\"); + } - arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); - arcFileData = this.ReadARCFile(arcFile, key); + arcFileData = this.ReadARCFile(arcFile, key); - if (TQDebug.DatabaseDebugLevel > 1 && arcFileData is not null) - Log.LogDebug(@"Custom resource found ""{resourceId}"" into ""{arcFile}""", key, arcFile); - } + if (TQDebug.DatabaseDebugLevel > 0 && arcFileData is null) + Log.LogError(@"Resource not found ""{resourceId}"" into ""{arcFile}""", key, arcFile); + } - // We either didn't load the resource or didn't find what we were looking for so check the normal game resources. - if (arcFileData == null) - { - // See if this guy is from Immortal Throne expansion pack. - if (TQDebug.DatabaseDebugLevel > 1) - Log.LogDebug("Checking IT Resources."); + #region Fallback : It looks like we never go there - (arcFile, isDLC) = this.GamePathResolver.ResolveArcFileName(key); - if (isDLC) - { - // not a proper resourceID. - if (resourceIdSplited.Length == 2) - return null; + // Added by VillageIdiot + // Maybe the arc file is in the XPack folder even though the record does not state it. + // Also could be that it says xpack in the record but the file is in the root. + if (arcFileData == null) + { + rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Resources", "XPack"); + arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); + arcFileData = this.ReadARCFile(arcFile, key); - arcFileBase = resourceIdSplited[1]; - key = resourceIdSplited.Skip(1).JoinString("\\"); - } + if (TQDebug.DatabaseDebugLevel > 1 && arcFileData is not null) + Log.LogError(@"Resource misplaced ""{resourceId}"" into ""{arcFile}""", key, arcFile); + } - arcFileData = this.ReadARCFile(arcFile, key); + // Now, let's check if the item is in Ragnarok DLC + if (arcFileData == null && GamePathResolver.IsRagnarokInstalled) + { + rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Resources", "XPack2"); + arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); + arcFileData = this.ReadARCFile(arcFile, key); - if (TQDebug.DatabaseDebugLevel > 0 && arcFileData is null) - Log.LogError(@"Resource not found ""{resourceId}"" into ""{arcFile}""", key, arcFile); - } + if (TQDebug.DatabaseDebugLevel > 1 && arcFileData is not null) + Log.LogError(@"Resource misplaced ""{resourceId}"" into ""{arcFile}""", key, arcFile); + } - #region Fallback : It looks like we never go there + if (arcFileData == null && GamePathResolver.IsAtlantisInstalled) + { + rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Resources", "XPack3"); + arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); + arcFileData = this.ReadARCFile(arcFile, key); - // Added by VillageIdiot - // Maybe the arc file is in the XPack folder even though the record does not state it. - // Also could be that it says xpack in the record but the file is in the root. - if (arcFileData == null) - { - rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Resources", "XPack"); - arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); - arcFileData = this.ReadARCFile(arcFile, key); + if (TQDebug.DatabaseDebugLevel > 1 && arcFileData is not null) + Log.LogError(@"Resource misplaced ""{resourceId}"" into ""{arcFile}""", key, arcFile); + } - if (TQDebug.DatabaseDebugLevel > 1 && arcFileData is not null) - Log.LogError(@"Resource misplaced ""{resourceId}"" into ""{arcFile}""", key, arcFile); - } + if (arcFileData == null && GamePathResolver.IsEmbersInstalled) + { + rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Resources", "XPack4"); + arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); + arcFileData = this.ReadARCFile(arcFile, key); - // Now, let's check if the item is in Ragnarok DLC - if (arcFileData == null && GamePathResolver.IsRagnarokInstalled) - { - rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Resources", "XPack2"); - arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); - arcFileData = this.ReadARCFile(arcFile, key); + if (TQDebug.DatabaseDebugLevel > 1 && arcFileData is not null) + Log.LogError(@"Resource misplaced ""{resourceId}"" into ""{arcFile}""", key, arcFile); + } - if (TQDebug.DatabaseDebugLevel > 1 && arcFileData is not null) - Log.LogError(@"Resource misplaced ""{resourceId}"" into ""{arcFile}""", key, arcFile); - } + if (arcFileData == null) + { + // We are either vanilla TQ or have not found our resource yet. + // from the original TQ folder + if (TQDebug.DatabaseDebugLevel > 1) + Log.LogDebug("Checking TQ Resources."); - if (arcFileData == null && GamePathResolver.IsAtlantisInstalled) - { - rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Resources", "XPack3"); - arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); - arcFileData = this.ReadARCFile(arcFile, key); + rootFolder = GamePathResolver.TQPath; + rootFolder = Path.Combine(rootFolder, "Resources"); - if (TQDebug.DatabaseDebugLevel > 1 && arcFileData is not null) - Log.LogError(@"Resource misplaced ""{resourceId}"" into ""{arcFile}""", key, arcFile); - } + arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); + arcFileData = this.ReadARCFile(arcFile, key); - if (arcFileData == null && GamePathResolver.IsEmbersInstalled) - { - rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Resources", "XPack4"); - arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); - arcFileData = this.ReadARCFile(arcFile, key); + if (TQDebug.DatabaseDebugLevel > 0 && arcFileData is null) + Log.LogError(@"Resource unknown ""{resourceId}""", key); + } - if (TQDebug.DatabaseDebugLevel > 1 && arcFileData is not null) - Log.LogError(@"Resource misplaced ""{resourceId}"" into ""{arcFile}""", key, arcFile); - } + #endregion - if (arcFileData == null) - { - // We are either vanilla TQ or have not found our resource yet. - // from the original TQ folder - if (TQDebug.DatabaseDebugLevel > 1) - Log.LogDebug("Checking TQ Resources."); + return arcFileData; + }); - rootFolder = GamePathResolver.TQPath; - rootFolder = Path.Combine(rootFolder, "Resources"); + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Exiting Database.LoadResource()"); - arcFile = Path.Combine(rootFolder, Path.ChangeExtension(arcFileBase, ".arc")); - arcFileData = this.ReadARCFile(arcFile, key); + return cachedArcFileData; + } - if (TQDebug.DatabaseDebugLevel > 0 && arcFileData is null) - Log.LogError(@"Resource unknown ""{resourceId}""", key); - } - #endregion + #endregion Database Public Methods - return arcFileData; - }); + #region Database Private Methods + + /// + /// Reads data from an ARC file and puts it into a Byte array + /// + /// Name of the arc file. + /// Id of data which we are getting from the arc file + /// Byte array of the data from the arc file. + private byte[] ReadARCFile(string arcFileName, string dataId) + { + // See if we have this arcfile already and if not create it. + try + { if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Exiting Database.LoadResource()"); + Log.LogDebug("Database.ReadARCFile('{0}', '{1}')", arcFileName, dataId); - return cachedArcFileData; - } + ArcFile arcFile = ReadARCFile(arcFileName); + // Now retrieve the data + byte[] ans = arcProv.GetData(arcFile, dataId); - #endregion Database Public Methods + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Exiting Database.ReadARCFile()"); - #region Database Private Methods + return ans; + } + catch (Exception e) + { + Log.LogError(e, "Exception occurred"); + throw; + } + } + /// + /// Read ARC file + /// + /// + /// + public ArcFile ReadARCFile(string arcFileName) + { + // See if we have this arcfile already and if not create it. - /// - /// Reads data from an ARC file and puts it into a Byte array - /// - /// Name of the arc file. - /// Id of data which we are getting from the arc file - /// Byte array of the data from the arc file. - private byte[] ReadARCFile(string arcFileName, string dataId) + ArcFile arcFile = this.arcFiles.GetOrAddAtomic(arcFileName, k => { - // See if we have this arcfile already and if not create it. - try - { - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Database.ReadARCFile('{0}', '{1}')", arcFileName, dataId); + var file = new ArcFile(k); + arcProv.ReadARCToC(file);// Heavy lifting in GetOrAddAtomic + return file; + }); - ArcFile arcFile = ReadARCFile(arcFileName); + return arcFile; + } - // Now retrieve the data - byte[] ans = arcProv.GetData(arcFile, dataId); + /// + /// Tries to determine the name of the text database file. + /// This is based on the game language and the UI language. + /// Will use English if all else fails. + /// + /// Signals whether we are looking for Immortal Throne files or vanilla Titan Quest files. + /// Path to the text db file + private string FigureDBFileToUse(bool isImmortalThrone) + { + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Database.FigureDBFileToUse({0})", isImmortalThrone); - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Exiting Database.ReadARCFile()"); + string rootFolder; + if (isImmortalThrone) + { + if (GamePathResolver.ImmortalThronePath.Contains("Anniversary")) + rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Text"); + else + rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Resources"); - return ans; - } - catch (Exception e) + if (TQDebug.DatabaseDebugLevel > 1) { - Log.LogError(e, "Exception occurred"); - throw; + Log.LogDebug("Detecting Immortal Throne text files"); + Log.LogDebug("rootFolder = {0}", rootFolder); } } - - /// - /// Read ARC file - /// - /// - /// - public ArcFile ReadARCFile(string arcFileName) + else { - // See if we have this arcfile already and if not create it. + // from the original TQ folder + rootFolder = Path.Combine(GamePathResolver.TQPath, "Text"); - ArcFile arcFile = this.arcFiles.GetOrAddAtomic(arcFileName, k => + if (TQDebug.DatabaseDebugLevel > 1) { - var file = new ArcFile(k); - arcProv.ReadARCToC(file);// Heavy lifting in GetOrAddAtomic - return file; - }); - - return arcFile; + Log.LogDebug("Detecting Titan Quest text files"); + Log.LogDebug("rootFolder = {0}", rootFolder); + } } - /// - /// Tries to determine the name of the text database file. - /// This is based on the game language and the UI language. - /// Will use English if all else fails. - /// - /// Signals whether we are looking for Immortal Throne files or vanilla Titan Quest files. - /// Path to the text db file - private string FigureDBFileToUse(bool isImmortalThrone) + // make sure the damn directory exists + if (!Directory.Exists(rootFolder)) { if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Database.FigureDBFileToUse({0})", isImmortalThrone); - - string rootFolder; - if (isImmortalThrone) - { - if (GamePathResolver.ImmortalThronePath.Contains("Anniversary")) - rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Text"); - else - rootFolder = Path.Combine(GamePathResolver.ImmortalThronePath, "Resources"); - - if (TQDebug.DatabaseDebugLevel > 1) - { - Log.LogDebug("Detecting Immortal Throne text files"); - Log.LogDebug("rootFolder = {0}", rootFolder); - } - } - else - { - // from the original TQ folder - rootFolder = Path.Combine(GamePathResolver.TQPath, "Text"); - - if (TQDebug.DatabaseDebugLevel > 1) - { - Log.LogDebug("Detecting Titan Quest text files"); - Log.LogDebug("rootFolder = {0}", rootFolder); - } - } + Log.LogDebug("Error - Root Folder does not exist"); - // make sure the damn directory exists - if (!Directory.Exists(rootFolder)) - { - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Error - Root Folder does not exist"); + return null; // silently fail + } - return null; // silently fail - } + string baseFile = Path.Combine(rootFolder, "Text_"); + string suffix = ".arc"; - string baseFile = Path.Combine(rootFolder, "Text_"); - string suffix = ".arc"; + // Added explicit set to null though may not be needed + string cultureID = null; - // Added explicit set to null though may not be needed - string cultureID = null; + // Moved this declaration since the first use is inside of the loop. + string filename = null; - // Moved this declaration since the first use is inside of the loop. - string filename = null; + // First see if we can use the game setting + string gameLanguage = this.GameLanguage; + if (TQDebug.DatabaseDebugLevel > 1) + { + Log.LogDebug("gameLanguage = {0}", gameLanguage == null ? "NULL" : gameLanguage); + Log.LogDebug("baseFile = {0}", baseFile); + } - // First see if we can use the game setting - string gameLanguage = this.GameLanguage; - if (TQDebug.DatabaseDebugLevel > 1) - { - Log.LogDebug("gameLanguage = {0}", gameLanguage == null ? "NULL" : gameLanguage); - Log.LogDebug("baseFile = {0}", baseFile); - } + if (gameLanguage != null) + { + // Try this method of getting the culture + if (TQDebug.DatabaseDebugLevel > 2) + Log.LogDebug("Try looking up cultureID"); - if (gameLanguage != null) + foreach (CultureInfo cultureInfo in CultureInfo.GetCultures(CultureTypes.NeutralCultures)) { - // Try this method of getting the culture if (TQDebug.DatabaseDebugLevel > 2) - Log.LogDebug("Try looking up cultureID"); + Log.LogDebug("Trying {0}", cultureInfo.EnglishName.ToUpperInvariant()); - foreach (CultureInfo cultureInfo in CultureInfo.GetCultures(CultureTypes.NeutralCultures)) + if (cultureInfo.EnglishName.ToUpperInvariant().Equals(gameLanguage.ToUpperInvariant()) || cultureInfo.DisplayName.ToUpperInvariant().Equals(gameLanguage.ToUpperInvariant())) { - if (TQDebug.DatabaseDebugLevel > 2) - Log.LogDebug("Trying {0}", cultureInfo.EnglishName.ToUpperInvariant()); - - if (cultureInfo.EnglishName.ToUpperInvariant().Equals(gameLanguage.ToUpperInvariant()) || cultureInfo.DisplayName.ToUpperInvariant().Equals(gameLanguage.ToUpperInvariant())) - { - cultureID = cultureInfo.TwoLetterISOLanguageName; - break; - } + cultureID = cultureInfo.TwoLetterISOLanguageName; + break; } + } - // Titan Quest doesn't use the ISO language code for some languages - // Added null check to fix exception when there is no culture found. - if (cultureID != null) + // Titan Quest doesn't use the ISO language code for some languages + // Added null check to fix exception when there is no culture found. + if (cultureID != null) + { + if (cultureID.ToUpperInvariant() == "CS") { - if (cultureID.ToUpperInvariant() == "CS") - { - // Force Czech to use CZ instead of CS for the 2 letter code. - cultureID = "CZ"; - } - else if (cultureID.ToUpperInvariant() == "PT") - { - // Force brazilian portuguese to use BR instead of PT - cultureID = "BR"; - } - else if (cultureID.ToUpperInvariant() == "ZH") - { - // Force chinese to use CH instead of ZH - cultureID = "CH"; - } + // Force Czech to use CZ instead of CS for the 2 letter code. + cultureID = "CZ"; } - - if (TQDebug.DatabaseDebugLevel > 1) - Log.LogDebug("cultureID = {0}", cultureID); - - // Moved this inital check for the file into the loop - // and added a check to verify that we actually have a cultureID - if (cultureID != null) + else if (cultureID.ToUpperInvariant() == "PT") { - filename = string.Concat(baseFile, cultureID, suffix); - if (TQDebug.DatabaseDebugLevel > 1) - { - Log.LogDebug("Detected cultureID from gameLanguage"); - Log.LogDebug("filename = {0}", filename); - } - - if (File.Exists(filename)) - { - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Exiting Database.FigureDBFileToUse()"); - - return filename; - } + // Force brazilian portuguese to use BR instead of PT + cultureID = "BR"; + } + else if (cultureID.ToUpperInvariant() == "ZH") + { + // Force chinese to use CH instead of ZH + cultureID = "CH"; } } - // try to use the default culture for the OS - cultureID = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; if (TQDebug.DatabaseDebugLevel > 1) - { - Log.LogDebug("Using cultureID from OS"); Log.LogDebug("cultureID = {0}", cultureID); - } - // Added a check to verify that we actually have a cultureID - // though it may not be needed + // Moved this inital check for the file into the loop + // and added a check to verify that we actually have a cultureID if (cultureID != null) { filename = string.Concat(baseFile, cultureID, suffix); if (TQDebug.DatabaseDebugLevel > 1) + { + Log.LogDebug("Detected cultureID from gameLanguage"); Log.LogDebug("filename = {0}", filename); + } if (File.Exists(filename)) { @@ -706,277 +862,302 @@ private string FigureDBFileToUse(bool isImmortalThrone) return filename; } } + } - // Now just try EN - cultureID = "EN"; + // try to use the default culture for the OS + cultureID = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName; + if (TQDebug.DatabaseDebugLevel > 1) + { + Log.LogDebug("Using cultureID from OS"); + Log.LogDebug("cultureID = {0}", cultureID); + } + + // Added a check to verify that we actually have a cultureID + // though it may not be needed + if (cultureID != null) + { filename = string.Concat(baseFile, cultureID, suffix); if (TQDebug.DatabaseDebugLevel > 1) - { - Log.LogDebug("Forcing English Language"); - Log.LogDebug("cultureID = {0}", cultureID); Log.LogDebug("filename = {0}", filename); - } if (File.Exists(filename)) { if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Database.Exiting FigureDBFileToUse()"); + Log.LogDebug("Exiting Database.FigureDBFileToUse()"); return filename; } + } - // Now just see if we can find anything. + // Now just try EN + cultureID = "EN"; + filename = string.Concat(baseFile, cultureID, suffix); + if (TQDebug.DatabaseDebugLevel > 1) + { + Log.LogDebug("Forcing English Language"); + Log.LogDebug("cultureID = {0}", cultureID); + Log.LogDebug("filename = {0}", filename); + } + + if (File.Exists(filename)) + { if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Detection Failed - searching for files"); + Log.LogDebug("Database.Exiting FigureDBFileToUse()"); - string[] files = Directory.GetFiles(rootFolder, "Text_??.arc"); + return filename; + } - // Added check that files is not null. - if (files != null && files.Length > 0) - { - if (TQDebug.DatabaseDebugLevel > 1) - { - Log.LogDebug("Found some files"); - Log.LogDebug("filename = {0}", files[0]); - } + // Now just see if we can find anything. + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Detection Failed - searching for files"); - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Exiting Database.FigureDBFileToUse()"); + string[] files = Directory.GetFiles(rootFolder, "Text_??.arc"); - return files[0]; + // Added check that files is not null. + if (files != null && files.Length > 0) + { + if (TQDebug.DatabaseDebugLevel > 1) + { + Log.LogDebug("Found some files"); + Log.LogDebug("filename = {0}", files[0]); } if (TQDebug.DatabaseDebugLevel > 0) - { - Log.LogDebug("Failed to determine Language file!"); Log.LogDebug("Exiting Database.FigureDBFileToUse()"); - } - return null; + return files[0]; } - /// - /// Loads the Text database - /// - private void LoadTextDB() + if (TQDebug.DatabaseDebugLevel > 0) { - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Database.LoadTextDB()"); + Log.LogDebug("Failed to determine Language file!"); + Log.LogDebug("Exiting Database.FigureDBFileToUse()"); + } - string databaseFile = this.FigureDBFileToUse(false); - if (TQDebug.DatabaseDebugLevel > 1) + return null; + } + + /// + /// Loads the Text database + /// + private void LoadTextDB() + { + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Database.LoadTextDB()"); + + string databaseFile = this.FigureDBFileToUse(false); + if (TQDebug.DatabaseDebugLevel > 1) + { + Log.LogDebug("Find Titan Quest text file"); + Log.LogDebug("dbFile = {0}", databaseFile); + } + + if (!string.IsNullOrEmpty(databaseFile)) + { + string fileName = Path.GetFileNameWithoutExtension(databaseFile); + } + + if (databaseFile != null) + { + // Try to suck what we want into memory and then parse it. + this.ParseTextDB(databaseFile, "text\\commonequipment.txt"); + this.ParseTextDB(databaseFile, "text\\uniqueequipment.txt"); + this.ParseTextDB(databaseFile, "text\\quest.txt"); + this.ParseTextDB(databaseFile, "text\\ui.txt"); + this.ParseTextDB(databaseFile, "text\\skills.txt"); + this.ParseTextDB(databaseFile, "text\\monsters.txt"); // Added by VillageIdiot + this.ParseTextDB(databaseFile, "text\\menu.txt"); // Added by VillageIdiot + this.ParseTextDB(databaseFile, "text\\tutorial.txt"); + + // Immortal Throne data + this.ParseTextDB(databaseFile, "text\\xcommonequipment.txt"); + this.ParseTextDB(databaseFile, "text\\xuniqueequipment.txt"); + this.ParseTextDB(databaseFile, "text\\xquest.txt"); + this.ParseTextDB(databaseFile, "text\\xui.txt"); + this.ParseTextDB(databaseFile, "text\\xskills.txt"); + this.ParseTextDB(databaseFile, "text\\xmonsters.txt"); // Added by VillageIdiot + this.ParseTextDB(databaseFile, "text\\xmenu.txt"); // Added by VillageIdiot + this.ParseTextDB(databaseFile, "text\\xnpc.txt"); // Added by VillageIdiot + this.ParseTextDB(databaseFile, "text\\modstrings.txt"); // Added by VillageIdiot + this.ParseTextDB(databaseFile, "text\\xtutorial.txt"); // Added by hguy + + if (GamePathResolver.IsRagnarokInstalled) { - Log.LogDebug("Find Titan Quest text file"); - Log.LogDebug("dbFile = {0}", databaseFile); + this.ParseTextDB(databaseFile, "text\\x2commonequipment.txt"); + this.ParseTextDB(databaseFile, "text\\x2uniqueequipment.txt"); + this.ParseTextDB(databaseFile, "text\\x2quest.txt"); + this.ParseTextDB(databaseFile, "text\\x2ui.txt"); + this.ParseTextDB(databaseFile, "text\\x2skills.txt"); + this.ParseTextDB(databaseFile, "text\\x2monsters.txt"); // Added by VillageIdiot + this.ParseTextDB(databaseFile, "text\\x2menu.txt"); // Added by VillageIdiot + this.ParseTextDB(databaseFile, "text\\x2npc.txt"); // Added by VillageIdiot } - if (!string.IsNullOrEmpty(databaseFile)) + if (GamePathResolver.IsAtlantisInstalled) { - string fileName = Path.GetFileNameWithoutExtension(databaseFile); + this.ParseTextDB(databaseFile, "text\\x3basegame_nonvoiced.txt"); + this.ParseTextDB(databaseFile, "text\\x3items_nonvoiced.txt"); + this.ParseTextDB(databaseFile, "text\\x3mainquest_nonvoiced.txt"); + this.ParseTextDB(databaseFile, "text\\x3misctags_nonvoiced.txt"); + this.ParseTextDB(databaseFile, "text\\x3sidequests_nonvoiced.txt"); } - if (databaseFile != null) + if (GamePathResolver.IsEmbersInstalled) { - // Try to suck what we want into memory and then parse it. - this.ParseTextDB(databaseFile, "text\\commonequipment.txt"); - this.ParseTextDB(databaseFile, "text\\uniqueequipment.txt"); - this.ParseTextDB(databaseFile, "text\\quest.txt"); - this.ParseTextDB(databaseFile, "text\\ui.txt"); - this.ParseTextDB(databaseFile, "text\\skills.txt"); - this.ParseTextDB(databaseFile, "text\\monsters.txt"); // Added by VillageIdiot - this.ParseTextDB(databaseFile, "text\\menu.txt"); // Added by VillageIdiot - this.ParseTextDB(databaseFile, "text\\tutorial.txt"); - - // Immortal Throne data - this.ParseTextDB(databaseFile, "text\\xcommonequipment.txt"); - this.ParseTextDB(databaseFile, "text\\xuniqueequipment.txt"); - this.ParseTextDB(databaseFile, "text\\xquest.txt"); - this.ParseTextDB(databaseFile, "text\\xui.txt"); - this.ParseTextDB(databaseFile, "text\\xskills.txt"); - this.ParseTextDB(databaseFile, "text\\xmonsters.txt"); // Added by VillageIdiot - this.ParseTextDB(databaseFile, "text\\xmenu.txt"); // Added by VillageIdiot - this.ParseTextDB(databaseFile, "text\\xnpc.txt"); // Added by VillageIdiot - this.ParseTextDB(databaseFile, "text\\modstrings.txt"); // Added by VillageIdiot - this.ParseTextDB(databaseFile, "text\\xtutorial.txt"); // Added by hguy - - if (GamePathResolver.IsRagnarokInstalled) - { - this.ParseTextDB(databaseFile, "text\\x2commonequipment.txt"); - this.ParseTextDB(databaseFile, "text\\x2uniqueequipment.txt"); - this.ParseTextDB(databaseFile, "text\\x2quest.txt"); - this.ParseTextDB(databaseFile, "text\\x2ui.txt"); - this.ParseTextDB(databaseFile, "text\\x2skills.txt"); - this.ParseTextDB(databaseFile, "text\\x2monsters.txt"); // Added by VillageIdiot - this.ParseTextDB(databaseFile, "text\\x2menu.txt"); // Added by VillageIdiot - this.ParseTextDB(databaseFile, "text\\x2npc.txt"); // Added by VillageIdiot - } + this.ParseTextDB(databaseFile, "text\\x4basegame_nonvoiced.txt"); + this.ParseTextDB(databaseFile, "text\\x4items_nonvoiced.txt"); + this.ParseTextDB(databaseFile, "text\\x4mainquest_nonvoiced.txt"); + this.ParseTextDB(databaseFile, "text\\x4misctags_nonvoiced.txt"); + this.ParseTextDB(databaseFile, "text\\x4nametags_nonvoiced.txt"); + this.ParseTextDB(databaseFile, "text\\x4sidequests_nonvoiced.txt"); + } + } - if (GamePathResolver.IsAtlantisInstalled) - { - this.ParseTextDB(databaseFile, "text\\x3basegame_nonvoiced.txt"); - this.ParseTextDB(databaseFile, "text\\x3items_nonvoiced.txt"); - this.ParseTextDB(databaseFile, "text\\x3mainquest_nonvoiced.txt"); - this.ParseTextDB(databaseFile, "text\\x3misctags_nonvoiced.txt"); - this.ParseTextDB(databaseFile, "text\\x3sidequests_nonvoiced.txt"); - } + // For loading custom map text database. + if (GamePathResolver.IsCustom) + { + databaseFile = Path.Combine(GamePathResolver.MapName, "resources", "text.arc"); - if (GamePathResolver.IsEmbersInstalled) - { - this.ParseTextDB(databaseFile, "text\\x4basegame_nonvoiced.txt"); - this.ParseTextDB(databaseFile, "text\\x4items_nonvoiced.txt"); - this.ParseTextDB(databaseFile, "text\\x4mainquest_nonvoiced.txt"); - this.ParseTextDB(databaseFile, "text\\x4misctags_nonvoiced.txt"); - this.ParseTextDB(databaseFile, "text\\x4nametags_nonvoiced.txt"); - this.ParseTextDB(databaseFile, "text\\x4sidequests_nonvoiced.txt"); - } + if (TQDebug.DatabaseDebugLevel > 1) + { + Log.LogDebug("Find Custom Map text file"); + Log.LogDebug("dbFile = {0}", databaseFile); } - // For loading custom map text database. - if (GamePathResolver.IsCustom) - { - databaseFile = Path.Combine(GamePathResolver.MapName, "resources", "text.arc"); + if (databaseFile != null) + this.ParseTextDB(databaseFile, "text\\modstrings.txt"); + } - if (TQDebug.DatabaseDebugLevel > 1) - { - Log.LogDebug("Find Custom Map text file"); - Log.LogDebug("dbFile = {0}", databaseFile); - } + // Added this check to see if anything was loaded. + if (this.textDB.Count == 0) + { + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Exception - Could not load Text DB."); - if (databaseFile != null) - this.ParseTextDB(databaseFile, "text\\modstrings.txt"); - } + throw new FileLoadException("Could not load Text DB."); + } - // Added this check to see if anything was loaded. - if (this.textDB.Count == 0) - { - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Exception - Could not load Text DB."); + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Exiting Database.LoadTextDB()"); + } - throw new FileLoadException("Could not load Text DB."); - } + /// + /// Parses the text database to put the entries into a hash table. + /// + /// Database file name (arc file) + /// Name of the text DB file within the arc file + private void ParseTextDB(string databaseFile, string filename) + { + if (TQDebug.DatabaseDebugLevel > 0) + Log.LogDebug("Database.ParseTextDB({0}, {1})", databaseFile, filename); - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Exiting Database.LoadTextDB()"); - } + byte[] data = this.ReadARCFile(databaseFile, filename); - /// - /// Parses the text database to put the entries into a hash table. - /// - /// Database file name (arc file) - /// Name of the text DB file within the arc file - private void ParseTextDB(string databaseFile, string filename) + if (data == null) { + // Changed for mod support. Sometimes the text file has more entries than just the x or non-x prefix files. if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Database.ParseTextDB({0}, {1})", databaseFile, filename); + Log.LogDebug("Error in ARC File: {0} does not contain an entry for '{1}'", databaseFile, filename); - byte[] data = this.ReadARCFile(databaseFile, filename); + return; + } - if (data == null) + // now read it like a text file + // Changed to system default encoding since there might be extended ascii (or something else) in the text db. + using (StreamReader reader = new StreamReader(new MemoryStream(data), Encoding.Default)) + { + char delimiter = '='; + string line; + while ((line = reader.ReadLine()) != null) { - // Changed for mod support. Sometimes the text file has more entries than just the x or non-x prefix files. - if (TQDebug.DatabaseDebugLevel > 0) - Log.LogDebug("Error in ARC File: {0} does not contain an entry for '{1}'", databaseFile, filename); + line = line.Trim(); - return; - } + // delete short lines + if (line.Length < 2) + continue; - // now read it like a text file - // Changed to system default encoding since there might be extended ascii (or something else) in the text db. - using (StreamReader reader = new StreamReader(new MemoryStream(data), Encoding.Default)) - { - char delimiter = '='; - string line; - while ((line = reader.ReadLine()) != null) - { - line = line.Trim(); + // comment line + if (line.StartsWith("//", StringComparison.Ordinal)) + continue; - // delete short lines - if (line.Length < 2) - continue; + // split on the equal sign + string[] fields = line.Split(delimiter); - // comment line - if (line.StartsWith("//", StringComparison.Ordinal)) - continue; + // bad line + if (fields.Length < 2) + continue; - // split on the equal sign - string[] fields = line.Split(delimiter); + string label = fields[1].Trim(); - // bad line - if (fields.Length < 2) - continue; + // Now for the foreign languages there is a bunch of crap in here so the proper version of the adjective can be used with the proper + // noun form. I don' want to code all that so this next code will just take the first version of the adjective and then + // throw away all the metadata. - string label = fields[1].Trim(); + // hguy : one expression to rule them all + if (Regex.Match(label, @"^(?\[\w+\])(?