From e05a8ae7ddd92babb50e98e28d45b3ee345bda1e Mon Sep 17 00:00:00 2001 From: Niphyr Date: Sat, 25 Oct 2025 15:55:10 +1100 Subject: [PATCH 1/5] Updated default version to be 12.x (midnight) --- .../SimcGenerationServiceIntegrationTests.cs | 2 +- SimcProfileParser/DataSync/CacheService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SimcProfileParser.Tests/SimcGenerationServiceIntegrationTests.cs b/SimcProfileParser.Tests/SimcGenerationServiceIntegrationTests.cs index 42b21a8..fcd1d16 100644 --- a/SimcProfileParser.Tests/SimcGenerationServiceIntegrationTests.cs +++ b/SimcProfileParser.Tests/SimcGenerationServiceIntegrationTests.cs @@ -133,7 +133,7 @@ public async Task SGS_Gets_Game_Version() // Assert ClassicAssert.IsNotNull(version); - ClassicAssert.AreEqual("11.", version.Substring(0, 3)); + ClassicAssert.AreEqual("12.", version.Substring(0, 3)); } } } diff --git a/SimcProfileParser/DataSync/CacheService.cs b/SimcProfileParser/DataSync/CacheService.cs index 40111a2..7ed95e5 100644 --- a/SimcProfileParser/DataSync/CacheService.cs +++ b/SimcProfileParser/DataSync/CacheService.cs @@ -24,7 +24,7 @@ internal class CacheService : ICacheService protected readonly ILogger _logger; private bool _usePtrData = false; - private string _useBranchName = "thewarwithin"; + private string _useBranchName = "midnight"; internal string _getUrl(string fileName) => "https://raw.githubusercontent.com/simulationcraft/simc/" + _useBranchName + "/engine/dbc/generated/" + fileName From 8f10c7def50f7b434e9499494a45a96e5a3245d9 Mon Sep 17 00:00:00 2001 From: Niphyr Date: Sat, 25 Oct 2025 15:55:19 +1100 Subject: [PATCH 2/5] Improved public API documentation --- README.md | 31 ++- SimcProfileParser/SimcGenerationService.cs | 295 ++++++++++++++++++++- 2 files changed, 312 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1e08d20..77b48f8 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,11 @@ ISimcGenerationService sgs = new SimcGenerationService(); // Using async var profile = await sgs.GenerateProfileAsync(File.ReadAllText("import.simc")); +// Output some details about the profile Console.WriteLine($"Profile object created for player {profile.Name}."); + +var json = JsonSerializer.Serialize(profile, new JsonSerializerOptions() { WriteIndented = true }); +await File.WriteAllTextAsync("profile.json", json); ``` You can also generate a profile object from individual lines of an import file: @@ -68,14 +72,18 @@ ISimcGenerationService sgs = new SimcGenerationService(); var lines = new List() { - "level=70", - "main_hand=,id=178473,bonus_id=6774/1504/6646" + "level=90", + "main_hand=,id=237728,bonus_id=6652/10356/13446/1540/10255" }; var profile = await sgs.GenerateProfileAsync(lines); +// Output some details about the profile Console.WriteLine($"Profile object created for a level {profile.Level}"); Console.WriteLine($"They are weilding {profile.Items.FirstOrDefault().Name}."); + +var json = JsonSerializer.Serialize(profile, new JsonSerializerOptions() { WriteIndented = true }); +await File.WriteAllTextAsync("profile.json", json); ``` #### Creating a single item @@ -86,12 +94,15 @@ ISimcGenerationService sgs = new SimcGenerationService(); var itemOptions = new SimcItemOptions() { - ItemId = 177813, + ItemId = 242392, Quality = ItemQuality.ITEM_QUALITY_EPIC, - ItemLevel = 226 + ItemLevel = 730 }; var item = await sgs.GenerateItemAsync(spellOptions); + +var json = JsonSerializer.Serialize(profile, new JsonSerializerOptions() { WriteIndented = true }); +await File.WriteAllTextAsync("item.json", json); ``` There are other options that can be set, including bonus ids, gems and the original drop level: @@ -120,13 +131,16 @@ ISimcGenerationService sgs = new SimcGenerationService(); var spellOptions = new SimcSpellOptions() { - ItemLevel = 226, - SpellId = 343538, + ItemLevel = 730, + SpellId = 1238697, ItemQuality = ItemQuality.ITEM_QUALITY_EPIC, ItemInventoryType = InventoryType.INVTYPE_TRINKET }; var spell = await sgs.GenerateSpellAsync(spellOptions); + +var json = JsonSerializer.Serialize(spell, new JsonSerializerOptions() { WriteIndented = true }); +await File.WriteAllTextAsync("spell.json", json); ``` Generating an player scaling spell (id 274740): @@ -137,10 +151,13 @@ ISimcGenerationService sgs = new SimcGenerationService(); var spellOptions = new SimcSpellOptions() { SpellId = 274740, - PlayerLevel = 70 + PlayerLevel = 90 }; var spell = await sgs.GenerateSpellAsync(spellOptions); + +var json = JsonSerializer.Serialize(spell, new JsonSerializerOptions() { WriteIndented = true }); +await File.WriteAllTextAsync("spell.json", json); ``` The spell object has a property `ScaleBudget` which can be multiplied with a coeffecient from a spells effect if required. diff --git a/SimcProfileParser/SimcGenerationService.cs b/SimcProfileParser/SimcGenerationService.cs index 7e922ce..e13ef7d 100644 --- a/SimcProfileParser/SimcGenerationService.cs +++ b/SimcProfileParser/SimcGenerationService.cs @@ -12,6 +12,22 @@ namespace SimcProfileParser { + /// + /// Main service for parsing and generating SimulationCraft (SimC) profile data. + /// This service provides functionality to parse SimC addon exports, generate items with bonus IDs and gems, + /// create spells with proper scaling, retrieve talent information, and fetch game data versions. + /// + /// + /// + /// The SimC addon exports character data in a specific text format that includes items, talents, + /// character stats, and other game data. This service parses that format and enriches it with + /// detailed information from SimulationCraft's game data files. + /// + /// + /// This service can be used standalone by creating a new instance, or through dependency injection + /// using the AddSimcProfileParser() extension method. + /// + /// public class SimcGenerationService : ISimcGenerationService { private readonly ILogger _logger; @@ -22,6 +38,17 @@ public class SimcGenerationService : ISimcGenerationService private readonly ISimcTalentService _simcTalentService; private readonly ICacheService _cacheService; + /// + /// Initializes a new instance of the class with all dependencies. + /// This constructor is primarily used for dependency injection scenarios. + /// + /// Logger instance for diagnostic output. + /// Service for parsing SimC profile strings into structured data. + /// Service for creating and enriching item data with bonuses, gems, and effects. + /// Service for generating spell data with proper scaling calculations. + /// Service for retrieving game data version information. + /// Service for fetching and processing talent data. + /// Service for caching and retrieving raw game data files. public SimcGenerationService(ILogger logger, ISimcParserService simcParserService, ISimcItemCreationService simcItemCreationService, @@ -39,6 +66,15 @@ public SimcGenerationService(ILogger logger, _cacheService = cacheService; } + /// + /// Initializes a new instance of the class with automatic service initialization. + /// This constructor creates all necessary internal services and is suitable for standalone usage. + /// + /// Logger factory for creating logger instances for internal services. + /// + /// This constructor will automatically initialize all required services including data extraction, + /// caching, parsing, and generation services with the provided logger factory. + /// public SimcGenerationService(ILoggerFactory loggerFactory) : this(loggerFactory.CreateLogger(), null, null, null, null, null, null) { @@ -77,12 +113,45 @@ public SimcGenerationService(ILoggerFactory loggerFactory) } + /// + /// Initializes a new instance of the class with no logging. + /// This is the simplest constructor for basic usage scenarios. + /// + /// + /// This constructor uses a which means no logging output will be generated. + /// Use one of the other constructors if you need logging capabilities. + /// public SimcGenerationService() : this(NullLoggerFactory.Instance) { } + /// + /// Generates a complete SimC profile from a collection of profile lines. + /// + /// A list of individual lines from a SimC profile export, such as character data, items, talents, etc. + /// + /// A fully populated with parsed character information, + /// enriched item data including stats and effects, and talent details. + /// + /// Thrown when is null or empty. + /// Thrown when the profile string is invalid or produces no results. + /// + /// + /// This method performs the following operations: + /// + /// Parses the profile lines to extract character, item, and talent data + /// For each item: fetches base item data, applies bonus IDs, adds gems, and calculates final stats + /// For each item effect: generates spell data with proper scaling based on item level and quality + /// For each talent: retrieves spell ID, name, and rank information + /// + /// + /// + /// The resulting profile contains both the raw parsed data () + /// and enriched generated data ( and ). + /// + /// public async Task GenerateProfileAsync(List profileString) { if (profileString == null || profileString.Count == 0) @@ -90,7 +159,7 @@ public async Task GenerateProfileAsync(List profileString) // Process the incoming profile string var parsedProfile = await Task.Factory.StartNew( - () => _simcParserService.ParseProfileAsync(profileString)); + () => _simcParserService.ParseProfileAsync(profileString)); if (parsedProfile == null) throw new ArgumentOutOfRangeException(nameof(profileString), "profileString provided was invalid or produced no results"); @@ -111,7 +180,7 @@ public async Task GenerateProfileAsync(List profileString) } // Populate the details for each talent - foreach(var talent in newProfile.ParsedProfile.Talents) + foreach (var talent in newProfile.ParsedProfile.Talents) { var newTalent = await _simcTalentService.GetTalentDataAsync(talent.TalentId, talent.Rank); if (newTalent != null) @@ -121,6 +190,29 @@ public async Task GenerateProfileAsync(List profileString) return newProfile; } + /// + /// Generates a complete SimC profile from a single multi-line string. + /// This is a convenience overload that splits the string into lines before processing. + /// + /// + /// A multi-line string containing the complete SimC profile export. + /// Lines can be separated by any combination of \r\n, \r, or \n. + /// + /// + /// A fully populated object. + /// + /// Thrown when is null or empty. + /// Thrown when the profile string is invalid or produces no results. + /// + /// + /// This method is useful when reading SimC profile data from a file or API response as a single string. + /// It automatically handles different line ending formats (Windows, Unix, Mac) and empty lines. + /// + /// + /// The string is split using to ignore blank lines. + /// + /// + /// public async Task GenerateProfileAsync(string profileString) { if (string.IsNullOrEmpty(profileString)) @@ -129,15 +221,55 @@ public async Task GenerateProfileAsync(string profileString) throw new ArgumentNullException(nameof(profileString)); } - _logger?.LogInformation($"Splitting a string with {profileString.Length} character(s)"); + _logger?.LogInformation("Splitting a string with {Length} character(s)", profileString.Length); - var lines = profileString.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); + var lines = profileString.Split(["\r\n", "\r", "\n"], StringSplitOptions.RemoveEmptyEntries).ToList(); - _logger?.LogInformation($"Created {lines.Count} lines to be processed."); + _logger?.LogInformation("Created {Count} lines to be processed.", lines.Count); return await GenerateProfileAsync(lines); } + /// + /// Generates a detailed item with all stats, effects, gems, and bonuses applied. + /// + /// + /// Configuration object specifying the item ID and optional modifiers including item level, + /// bonus IDs, gem IDs, crafted stats, quality, and drop level. + /// + /// + /// A with: + /// + /// Base item properties (name, class, inventory type) + /// Calculated stats from item mods, scaled to the specified item level + /// Socket information and gem bonuses + /// All item effects with complete spell data including scaling + /// Quality-based stat budgets + /// + /// + /// Thrown when is null. + /// + /// Thrown when is 0 or the item ID is not found in the game data. + /// + /// + /// + /// This method processes items through multiple stages: + /// + /// Fetches base item data from SimulationCraft's game data + /// Applies item level scaling to adjust base stats + /// Processes bonus IDs which may add stats, change quality, add sockets, or modify item level + /// Applies gems to sockets and calculates their stat contributions + /// Handles crafted stats for player-crafted items + /// Generates spell effects for on-use, on-equip, and proc effects with proper scaling + /// + /// + /// + /// If both and bonus IDs that modify item level are provided, + /// the explicit item level takes precedence and bonus ID item level modifications are ignored. + /// + /// + /// + /// public async Task GenerateItemAsync(SimcItemOptions options) { if (options == null) @@ -157,40 +289,189 @@ public async Task GenerateItemAsync(SimcItemOptions options) return item; } + /// + /// Generates a spell with proper scaling based on either item level or player level. + /// + /// + /// Configuration object specifying the spell ID and scaling parameters. + /// Use for item-based spells (trinkets, enchants), + /// or for player-based spells (racials, class abilities). + /// + /// + /// A with: + /// + /// Basic spell properties (name, school, cooldown, duration, etc.) + /// All spell effects with properly scaled coefficients and values + /// Proc information (RPPM, proc chance, internal cooldown) + /// Power costs scaled to player level or spec + /// Scaling budgets for effects that scale with item level or player level + /// + /// + /// + /// + /// There are two types of spell scaling handled by this method: + /// + /// + /// Item Scaling (when is non-zero): + /// Used for spells from items like trinkets, weapons, and enchants. Scaling is based on: + /// + /// Item level - higher level items have stronger effects + /// Item quality - epic items scale better than rare items + /// Inventory type - different slots have different scaling budgets + /// + /// + /// + /// Player Scaling (when is 0): + /// Used for player abilities, racials, and class spells. Scaling is based on: + /// + /// Player level - abilities scale as the character levels up + /// Class and spec - different classes have different scaling coefficients + /// + /// + /// + /// Each spell effect contains a property that can be + /// multiplied with the effect's or + /// to get the final scaled value. + /// + /// + /// + /// + /// public async Task GenerateSpellAsync(SimcSpellOptions options) { SimcSpell spell; if (options.ItemLevel != 0) { - // TODO: Remove this await once the rest of the library chain supports async better spell = await _simcSpellCreationService.GenerateItemSpellAsync(options); } else { - // TODO: Remove this await once the rest of the library chain supports async better spell = await _simcSpellCreationService.GeneratePlayerSpellAsync(options); } return spell; } + /// + /// Retrieves the game data version string from SimulationCraft's data files. + /// + /// + /// A version string in the format "Major.Minor.Patch.Build" + /// (e.g., "10.2.6.53840"), representing the World of Warcraft client version that + /// the current game data was extracted from. + /// + /// + /// + /// The version is extracted from SimulationCraft's CLIENT_DATA_WOW_VERSION or + /// PTR_CLIENT_DATA_WOW_VERSION preprocessor definitions, depending on whether + /// is enabled. + /// + /// + /// + /// public async Task GetGameDataVersionAsync() { return await _simcVersionService.GetGameDataVersionAsync(); } + /// + /// Retrieves all available talents for a specific class and specialisation. + /// + /// + /// The numeric class ID (1-13). Examples: 1=Warrior, 2=Paladin, 5=Priest, 11=Druid, etc. + /// See SimulationCraft's class enumeration for complete mapping. + /// + /// + /// The numeric specialization ID. Examples: 256=Discipline Priest, 257=Holy Priest, 258=Shadow Priest. + /// See SimulationCraft's specialization enumeration for complete mapping. + /// + /// + /// A list of all objects available to the + /// specified class and specialization, including: + /// + /// Talent name and spell ID + /// Trait node entry ID for talent tree positioning + /// Maximum ranks available for the talent + /// Position information (row, column) in the talent tree + /// + /// Returns an empty list if no talents are found for the specified combination. + /// + /// + /// + /// The talent data includes both class-wide talents and specialization-specific talents. + /// Talents are returned without rank information - use the + /// to fetch detailed spell information for each rank if needed. + /// + /// + /// Note: This method returns the raw talent tree structure. It does not validate talent + /// prerequisites, choice nodes, or point requirements. + /// + /// + /// public async Task> GetAvailableTalentsAsync(int classId, int specId) { return await _simcTalentService.GetAvailableTalentsAsync(classId, specId); } + /// + /// Gets or sets whether to use PTR (Public Test Realm) data files instead of live game data. + /// Defaults to false (uses live data). + /// + /// + /// true to use PTR data files with the "_ptr" suffix; false to use live game data files. + /// + /// + /// + /// SimulationCraft maintains separate data files for the live game servers and the PTR servers + /// within the same GitHub branch. PTR data files have a "_ptr" suffix (e.g., "sc_spell_data_ptr.inc" + /// vs "sc_spell_data.inc"). + /// + /// + /// Important: Changing this property will clear all cached data to ensure consistency. + /// This may cause a temporary performance impact as data files are re-downloaded and re-parsed. + /// + /// + /// + /// public bool UsePtrData { get => _cacheService.UsePtrData; set => _cacheService.SetUsePtrData(value); } + /// + /// Gets or sets the GitHub branch name to use when fetching game data files from SimulationCraft's repository. + /// Defaults to "thewarwithin". + /// + /// + /// The branch name as a string (e.g., "midnight", "thewarwithin", "dragonflight"). + /// + /// + /// + /// SimulationCraft maintains separate branches for different World of Warcraft expansions. + /// Recent branch names include: + /// + /// "midnight" - Midnight (WoW 12.x) + /// "thewarwithin" - The War Within (WoW 11.x) + /// "dragonflight" - Dragonflight (WoW 10.x) + /// "shadowlands" - Shadowlands (WoW 9.x) + /// "bfa-dev" - Battle for Azeroth (WoW 8.x) + /// "legion-dev" - Legion (WoW 7.x) + /// + /// + /// + /// Important: Changing this property will clear all cached data to prevent mixing + /// data from different game versions. This may cause a temporary performance impact as + /// data files are re-downloaded and re-parsed. + /// + /// + /// Data files are fetched from: + /// https://raw.githubusercontent.com/simulationcraft/simc/{BranchName}/engine/dbc/generated/ + /// + /// + /// + /// public string UseBranchName { get => _cacheService.UseBranchName; From 0630ced2b52f09b1cd3eca9739cec91b61ce7633 Mon Sep 17 00:00:00 2001 From: Niphyr Date: Sat, 25 Oct 2025 16:08:02 +1100 Subject: [PATCH 3/5] README fixes --- README.md | 139 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 115 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 77b48f8..8b7ed40 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,9 @@ class MyClass } } ``` + ### Examples + #### Parsing profile files/strings Generating a profile object from a simc import file named `import.simc`: @@ -59,7 +61,9 @@ ISimcGenerationService sgs = new SimcGenerationService(); var profile = await sgs.GenerateProfileAsync(File.ReadAllText("import.simc")); // Output some details about the profile -Console.WriteLine($"Profile object created for player {profile.Name}."); +Console.WriteLine($"Profile: {profile.ParsedProfile.Name} - Level {profile.ParsedProfile.Level}"); +Console.WriteLine($"Items loaded: {profile.GeneratedItems.Count}"); +Console.WriteLine($"Talents loaded: {profile.Talents.Count}"); var json = JsonSerializer.Serialize(profile, new JsonSerializerOptions() { WriteIndented = true }); await File.WriteAllTextAsync("profile.json", json); @@ -78,14 +82,22 @@ var lines = new List() var profile = await sgs.GenerateProfileAsync(lines); -// Output some details about the profile -Console.WriteLine($"Profile object created for a level {profile.Level}"); -Console.WriteLine($"They are weilding {profile.Items.FirstOrDefault().Name}."); +// Access parsed data +Console.WriteLine($"Profile object created for a level {profile.ParsedProfile.Level}"); + +// Access enriched items with full stats +var firstItem = profile.GeneratedItems.FirstOrDefault(); +Console.WriteLine($"Wielding {firstItem.Name} with {firstItem.Mods.Count} stats"); var json = JsonSerializer.Serialize(profile, new JsonSerializerOptions() { WriteIndented = true }); await File.WriteAllTextAsync("profile.json", json); ``` +**Understanding Profile Output:** +- `ParsedProfile` - Raw parsed character data (name, class, spec, level, etc.) +- `GeneratedItems` - Fully enriched items with calculated stats, gems, sockets, and effects +- `Talents` - Talent details including spell IDs, names, and ranks + #### Creating a single item There are some basic options to manually create an item using `ISimcGenerationService.GenerateItemAsync`. @@ -99,52 +111,63 @@ var itemOptions = new SimcItemOptions() ItemLevel = 730 }; -var item = await sgs.GenerateItemAsync(spellOptions); +var item = await sgs.GenerateItemAsync(itemOptions); -var json = JsonSerializer.Serialize(profile, new JsonSerializerOptions() { WriteIndented = true }); +Console.WriteLine($"Item: {item.Name} (iLevel {item.ItemLevel})"); +Console.WriteLine($"Stats: {item.Mods.Count}, Sockets: {item.Sockets.Count}, Effects: {item.Effects.Count}"); + +var json = JsonSerializer.Serialize(item, new JsonSerializerOptions() { WriteIndented = true }); await File.WriteAllTextAsync("item.json", json); ``` -There are other options that can be set, including bonus ids, gems and the original drop level: +Available item options: ```csharp -public uint ItemId { get; set; } -public int ItemLevel { get; set; } -public IList BonusIds { get; set; } -public IList GemIds { get; set; } -public ItemQuality Quality { get; set; } -public int DropLevel { get; set; } +public uint ItemId { get; set; } // Required: The game item ID +public int ItemLevel { get; set; } // Override item level (takes precedence over bonus IDs) +public IList BonusIds { get; set; } // Bonus IDs that modify stats, sockets, or item level +public IList GemIds { get; set; } // Gem item IDs to socket into the item +public IList CraftedStatIds { get; set; } // Stat IDs for player-crafted items +public ItemQuality Quality { get; set; } // Override item quality +public int DropLevel { get; set; } // Character level when item dropped (affects scaling) ``` +**Item Generation Details:** +- **Bonus IDs**: Modify items by adding stats, sockets, changing item level, or quality +- **Gems**: Add secondary stats when socketed into items with sockets +- **Drop Level**: Affects item level scaling for items with dynamic scaling curves +- **Crafted Stats**: Specify which secondary stats player-crafted gear should have +- **Explicit ItemLevel**: If set, overrides any item level modifications from bonus IDs + #### Creating a single spell There are some basic options to manually create a spell using `ISimcGenerationService.GenerateSpellAsync`. -There are two types of generatable spells: - - - Player Scaling: the type that scale with the player level / class, such as racials. - - Item Scaling: the type that scales with the item quality/level, such as trinkets. - -Generating an item scaling spell (id 343538) for a **rare trinket at ilvl 226**: +**There are two types of spell scaling:** +**Item Scaling** - For spells that scale with item level (trinkets, enchants, weapon procs): ```csharp ISimcGenerationService sgs = new SimcGenerationService(); var spellOptions = new SimcSpellOptions() { - ItemLevel = 730, SpellId = 1238697, + ItemLevel = 730, ItemQuality = ItemQuality.ITEM_QUALITY_EPIC, ItemInventoryType = InventoryType.INVTYPE_TRINKET }; var spell = await sgs.GenerateSpellAsync(spellOptions); +// Calculate scaled effect values +var effect = spell.Effects.FirstOrDefault(); +var scaledValue = effect.BaseValue + (effect.Coefficient * effect.ScaleBudget); +Console.WriteLine($"Spell: {spell.Name}, Scaled Value: {scaledValue}"); + var json = JsonSerializer.Serialize(spell, new JsonSerializerOptions() { WriteIndented = true }); await File.WriteAllTextAsync("spell.json", json); ``` -Generating an player scaling spell (id 274740): - +**Player Scaling** - For spells that scale with player level (racials, class abilities): ```csharp ISimcGenerationService sgs = new SimcGenerationService(); @@ -160,8 +183,76 @@ var json = JsonSerializer.Serialize(spell, new JsonSerializerOptions() { WriteIn await File.WriteAllTextAsync("spell.json", json); ``` -The spell object has a property `ScaleBudget` which can be multiplied with a coeffecient from a spells effect if required. -Otherwise typically the BaseValue/ScaledValue of the effect will be what you're looking for. +Each spell effect has a `ScaleBudget` property that should be multiplied with the effect's coefficient to get the final scaled value. +For simple cases, the `BaseValue` property contains the unscaled value. + +#### Working with Talents + +```csharp +ISimcGenerationService sgs = new SimcGenerationService(); + +// Get all available talents for a class/spec +// Example: Holy Priest (classId: 5, specId: 257) +var talents = await sgs.GetAvailableTalentsAsync(classId: 5, specId: 257); + +Console.WriteLine($"Found {talents.Count} talents for Holy Priest"); +``` + +## Configuration + +### Switching Game Data Versions + +The library automatically downloads game data from [SimulationCraft's GitHub repository](https://github.com/simulationcraft/simc). +You can control which version of the data to use: + +```csharp +var service = new SimcGenerationService(); + +// Use PTR (Public Test Realm) data instead of live game data +service.UsePtrData = true; + +// Switch between expansions +service.UseBranchName = "midnight"; // Midnight expansion (WoW 12.x) - default +// service.UseBranchName = "thewarwithin"; // The War Within (WoW 11.x) +// service.UseBranchName = "dragonflight"; // Dragonflight (WoW 10.x) + +// Check which game version the data is from +var version = await service.GetGameDataVersionAsync(); +Console.WriteLine($"Using game data from WoW version: {version}"); +``` + +**Note:** Changing `UsePtrData` or `UseBranchName` will clear all cached data to prevent mixing incompatible game versions. + +### Data Caching + +Game data files are automatically downloaded and cached in your system's temp directory for performance. +The library handles: +- Automatic downloading when data is first needed +- ETag-based cache validation to check for updates +- Efficient memory caching of parsed data + +Cache location: `Path.GetTempPath() + "SimcProfileParserData"` + +## Important Notes + +### Supported Game Data +- **Item data**: Base stats, sockets, item levels, bonus IDs, gems, enchants +- **Spell data**: Effects, scaling, cooldowns, durations, proc rates (RPPM) +- **Talent data**: Talent trees, spell IDs, ranks, requirements +- **Scaling data**: Combat rating multipliers, stamina multipliers, spell scaling by level + +### Limitations +- Some newer bonus ID types may not be fully implemented +- Complex talent prerequisites and choice nodes are not validated +- Covenant/Soulbind data is parsed but may not be enriched (legacy content) + +### Error Handling +The library throws exceptions for invalid inputs: +- `ArgumentNullException` - When required parameters are null or empty +- `ArgumentOutOfRangeException` - When item/spell IDs are not found in the game data +- `NotImplementedException` - When encountering unsupported game features + +Always wrap API calls in try-catch blocks when dealing with user input. ## Support For bugs please search [issues](https://github.com/MechanicalPriest/SimcProfileParser/issues) From a6af9d69a6b4572a5d36d71daff4a0a6a0fc3cc9 Mon Sep 17 00:00:00 2001 From: Niphyr Date: Sat, 25 Oct 2025 16:08:33 +1100 Subject: [PATCH 4/5] Version bump to align with changing to midnight --- SimcProfileParser/SimcProfileParser.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SimcProfileParser/SimcProfileParser.csproj b/SimcProfileParser/SimcProfileParser.csproj index 78c1dde..e22a15c 100644 --- a/SimcProfileParser/SimcProfileParser.csproj +++ b/SimcProfileParser/SimcProfileParser.csproj @@ -3,9 +3,9 @@ net9.0 true - 3.0.0 - 3.0.0 - 3.0.0 + 3.1.0 + 3.1.0 + 3.1.0 Mechanical Priest Mechanical Priest GPL-3.0-only From 0ff88a7c1a4b0b69c3099ae09921b68eeaf602ca Mon Sep 17 00:00:00 2001 From: Niphyr Date: Sat, 25 Oct 2025 16:35:16 +1100 Subject: [PATCH 5/5] Midnight fixes for level 90 data - Includes fixed tests --- .../DataSync/RawDataExtractionServiceTests.cs | 4 +-- .../SimcGenerationServiceIntegrationTests.cs | 6 ++-- .../SimcItemCreationServiceTests.cs | 34 +++++++++---------- .../SimcSpellCreationServiceTests.cs | 18 +++++----- .../DataSync/RawDataExtractionService.cs | 4 +-- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/SimcProfileParser.Tests/DataSync/RawDataExtractionServiceTests.cs b/SimcProfileParser.Tests/DataSync/RawDataExtractionServiceTests.cs index aa26104..0348c3d 100644 --- a/SimcProfileParser.Tests/DataSync/RawDataExtractionServiceTests.cs +++ b/SimcProfileParser.Tests/DataSync/RawDataExtractionServiceTests.cs @@ -262,7 +262,7 @@ public void RDE_Generates_SpellScale_Multi() var incomingRawData = new Dictionary() { - { "ScaleData.raw", @"static constexpr double __spell_scaling[][80] = { + { "ScaleData.raw", @"static constexpr double __spell_scaling[][90] = { { 1, 0, 0, 0, 0, // 5 }, @@ -332,7 +332,7 @@ public void RDE_Generates_SpellScale_Multi() // Assert ClassicAssert.IsNotNull(result); ClassicAssert.AreEqual(23, result.Length); - ClassicAssert.AreEqual(80, result[0].Length); + ClassicAssert.AreEqual(90, result[0].Length); ClassicAssert.AreEqual(1d, result[0][0]); ClassicAssert.AreEqual(2d, result[1][0]); ClassicAssert.AreEqual(3d, result[2][0]); diff --git a/SimcProfileParser.Tests/SimcGenerationServiceIntegrationTests.cs b/SimcProfileParser.Tests/SimcGenerationServiceIntegrationTests.cs index fcd1d16..ce18d10 100644 --- a/SimcProfileParser.Tests/SimcGenerationServiceIntegrationTests.cs +++ b/SimcProfileParser.Tests/SimcGenerationServiceIntegrationTests.cs @@ -98,7 +98,7 @@ public async Task SGS_Creates_ItemSpell() ClassicAssert.IsNotNull(spell); ClassicAssert.IsNotNull(spell.Effects); ClassicAssert.AreEqual(2, spell.Effects.Count); - ClassicAssert.AreEqual(25.512510299999999d, spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(127.8034668d, spell.Effects[0].ScaleBudget); ClassicAssert.AreEqual(460.97500600000001d, spell.Effects[0].Coefficient); ClassicAssert.AreEqual(621.39996299999996d, spell.Effects[1].Coefficient); } @@ -110,7 +110,7 @@ public async Task SGS_Creates_PlayerSpell() var spellOptions = new SimcSpellOptions() { SpellId = 274740, - PlayerLevel = 60 + PlayerLevel = 90 }; // Act @@ -120,7 +120,7 @@ public async Task SGS_Creates_PlayerSpell() ClassicAssert.IsNotNull(spell); ClassicAssert.IsNotNull(spell.Effects); ClassicAssert.AreEqual(1.716, spell.Effects[0].Coefficient); - ClassicAssert.AreEqual(258.2211327d, spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(131.08048629999999d, spell.Effects[0].ScaleBudget); } [Test] diff --git a/SimcProfileParser.Tests/SimcItemCreationServiceTests.cs b/SimcProfileParser.Tests/SimcItemCreationServiceTests.cs index cd40c4e..469f195 100644 --- a/SimcProfileParser.Tests/SimcItemCreationServiceTests.cs +++ b/SimcProfileParser.Tests/SimcItemCreationServiceTests.cs @@ -121,13 +121,13 @@ public async Task ICS_Builds_Item_From_Options() ClassicAssert.AreEqual(202573, item.ItemId); // Stam - ClassicAssert.AreEqual(3422, item.Mods[0].StatRating); + ClassicAssert.AreEqual(892, item.Mods[0].StatRating); ClassicAssert.AreEqual(ItemModType.ITEM_MOD_STAMINA, item.Mods[0].Type); // Crit rating - ClassicAssert.AreEqual(460, item.Mods[1].StatRating); + ClassicAssert.AreEqual(269, item.Mods[1].StatRating); ClassicAssert.AreEqual(ItemModType.ITEM_MOD_CRIT_RATING, item.Mods[1].Type); // Haste rating - ClassicAssert.AreEqual(211, item.Mods[2].StatRating); + ClassicAssert.AreEqual(123, item.Mods[2].StatRating); ClassicAssert.AreEqual(ItemModType.ITEM_MOD_HASTE_RATING, item.Mods[2].Type); } @@ -174,8 +174,8 @@ public async Task ICS_ItemOptions_Correct_iLvl_Scaling() ClassicAssert.AreEqual(ItemQuality.ITEM_QUALITY_EPIC, item.Quality); ClassicAssert.AreEqual(181360, item.ItemId); // This will make sure the scale value that's being pulled for spells is using the right - // item level. In this cast it's 226 = 1.5. - ClassicAssert.AreEqual(1.5d, item.Effects[0].Spell.CombatRatingMultiplier); + // item level. + ClassicAssert.AreEqual(1.2376681566238403d, item.Effects[0].Spell.CombatRatingMultiplier); } [Test] @@ -252,8 +252,8 @@ public async Task ICS_ItemOptions_Correct_iLvl_Heal_EffectScaling() ClassicAssert.AreEqual(ItemQuality.ITEM_QUALITY_EPIC, item.Quality); ClassicAssert.AreEqual(178809, item.ItemId); // This will make sure the scale value that's being pulled for spells with healing/damage effects is using the right - // item level. In this cast it's 226 = 58. - ClassicAssert.AreEqual(25.512510299999999d, item.Effects[0].Spell.Effects[0].ScaleBudget); + // item level. + ClassicAssert.AreEqual(180.9646759d, item.Effects[0].Spell.Effects[0].ScaleBudget); } [Test] @@ -285,8 +285,8 @@ public async Task ICS_Builds_Trinket_From_ParsedItem_Secondary_Stat_UseEffect() ClassicAssert.AreEqual(336841, item.Effects[0].Spell.SpellId); ClassicAssert.AreEqual(90000, item.Effects[0].Spell.Cooldown); ClassicAssert.AreEqual(12000.0d, item.Effects[0].Spell.Duration); - ClassicAssert.AreEqual(1.5d, item.Effects[0].Spell.CombatRatingMultiplier); - ClassicAssert.AreEqual(25.512510299999999d, item.Effects[0].Spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(1.2376681566238403d, item.Effects[0].Spell.CombatRatingMultiplier); + ClassicAssert.AreEqual(180.9646759d, item.Effects[0].Spell.Effects[0].ScaleBudget); ClassicAssert.IsNotNull(item.Effects[0].Spell.Effects); ClassicAssert.AreEqual(1, item.Effects[0].Spell.Effects.Count); ClassicAssert.AreEqual(2.955178d, item.Effects[1].Spell.Effects[0].Coefficient); @@ -325,14 +325,14 @@ public async Task ICS_Builds_Trinket_From_ParsedItem_HealDmg_UseEffect() ClassicAssert.AreEqual(336866, item.Effects[0].Spell.SpellId); ClassicAssert.AreEqual(90000, item.Effects[0].Spell.Cooldown); ClassicAssert.AreEqual(6000, item.Effects[0].Spell.Duration); - ClassicAssert.AreEqual(1.5d, item.Effects[0].Spell.CombatRatingMultiplier); - ClassicAssert.AreEqual(25.512510299999999d, item.Effects[0].Spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(1.2376681566238403d, item.Effects[0].Spell.CombatRatingMultiplier); + ClassicAssert.AreEqual(180.9646759d, item.Effects[0].Spell.Effects[0].ScaleBudget); // Second effect ClassicAssert.AreEqual(135863, item.Effects[1].EffectId); ClassicAssert.IsNotNull(item.Effects[1].Spell); ClassicAssert.AreEqual(343538, item.Effects[1].Spell.SpellId); - ClassicAssert.AreEqual(1.5d, item.Effects[1].Spell.CombatRatingMultiplier); - ClassicAssert.AreEqual(25.512510299999999d, item.Effects[1].Spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(1.2376681566238403d, item.Effects[1].Spell.CombatRatingMultiplier); + ClassicAssert.AreEqual(127.8034668d, item.Effects[1].Spell.Effects[0].ScaleBudget); ClassicAssert.IsNotNull(item.Effects[1].Spell.Effects); ClassicAssert.AreEqual(2, item.Effects[1].Spell.Effects.Count); // Second effect's spells first effect @@ -370,8 +370,8 @@ public async Task ICS_Builds_Trinket_From_ParsedItem_Primary_ProcEffectt() ClassicAssert.IsNotNull(item.Effects[0].Spell); ClassicAssert.AreEqual(344117, item.Effects[0].Spell.SpellId); ClassicAssert.AreEqual(1.5, item.Effects[0].Spell.Rppm); - ClassicAssert.AreEqual(1.5d, item.Effects[0].Spell.CombatRatingMultiplier); - ClassicAssert.AreEqual(25.512510299999999d, item.Effects[0].Spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(1.2376681566238403d, item.Effects[0].Spell.CombatRatingMultiplier); + ClassicAssert.AreEqual(180.9646759d, item.Effects[0].Spell.Effects[0].ScaleBudget); // First effect's spells first effect trigger spells first effect (lol) // This is basically testing that the trigger spell gets linked. This particular spell // stores the proc coefficient in the trigger spell and multiplies it by 155. @@ -470,7 +470,7 @@ public async Task ICS_Creates_Premade_Ilvl_Scaling() ClassicAssert.AreEqual(193748, item.ItemId); ClassicAssert.AreEqual(395, item.ItemLevel); ClassicAssert.AreEqual(1, item.Mods.Count); - ClassicAssert.AreEqual(477, item.Mods[0].StatRating); + ClassicAssert.AreEqual(179, item.Mods[0].StatRating); } [Test] @@ -496,7 +496,7 @@ public async Task ICS_Creates_Premade_Ilvl_Scaling_HealEffect() ClassicAssert.AreEqual(194307, item.ItemId); ClassicAssert.AreEqual(398, item.ItemLevel); ClassicAssert.AreEqual(1, item.Mods.Count); - ClassicAssert.AreEqual(382, item.Mods[0].StatRating); + ClassicAssert.AreEqual(325, item.Mods[0].StatRating); } } } diff --git a/SimcProfileParser.Tests/SimcSpellCreationServiceTests.cs b/SimcProfileParser.Tests/SimcSpellCreationServiceTests.cs index 9769e67..5348361 100644 --- a/SimcProfileParser.Tests/SimcSpellCreationServiceTests.cs +++ b/SimcProfileParser.Tests/SimcSpellCreationServiceTests.cs @@ -63,7 +63,7 @@ public async Task SSC_Creates_Item_Spell_Spell_Options() ClassicAssert.IsNotNull(spell); ClassicAssert.IsNotNull(spell.Effects); ClassicAssert.AreEqual(2, spell.Effects.Count); - ClassicAssert.AreEqual(25.512510299999999d, spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(127.8034668d, spell.Effects[0].ScaleBudget); ClassicAssert.AreEqual(460.97500600000001d, spell.Effects[0].Coefficient); ClassicAssert.AreEqual(621.39996299999996d, spell.Effects[1].Coefficient); } @@ -110,7 +110,7 @@ public async Task SSC_Converts_OneScale_To_SevenScale() ClassicAssert.IsNotNull(spell); ClassicAssert.IsNotNull(spell.Effects); ClassicAssert.AreEqual(1, spell.Effects.Count); - ClassicAssert.AreEqual(125.98769760131836d, spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(162.23414856664567d, spell.Effects[0].ScaleBudget); ClassicAssert.AreEqual(1.65, spell.Effects[0].Coefficient); ClassicAssert.AreEqual(-7, spell.Effects[0].ScalingType); } @@ -134,7 +134,7 @@ public async Task SSC_Creates_Item_Spell_Raw_Obj() ClassicAssert.IsNotNull(spell); ClassicAssert.IsNotNull(spell.Effects); ClassicAssert.AreEqual(2, spell.Effects.Count); - ClassicAssert.AreEqual(25.512510299999999d, spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(127.8034668d, spell.Effects[0].ScaleBudget); ClassicAssert.AreEqual(460.97500600000001d, spell.Effects[0].Coefficient); ClassicAssert.AreEqual(621.39996299999996d, spell.Effects[1].Coefficient); } @@ -158,7 +158,7 @@ public async Task SSC_Creates_Item_Spell_Raw_Obj_9() ClassicAssert.IsNotNull(spell); ClassicAssert.IsNotNull(spell.Effects); ClassicAssert.AreEqual(1, spell.Effects.Count); - ClassicAssert.AreEqual(25.512510299999999d, spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(180.9646759d, spell.Effects[0].ScaleBudget); ClassicAssert.AreEqual(26.649944000000001d, spell.Effects[0].Coefficient); } @@ -170,7 +170,7 @@ public async Task SSC_Creates_Player_Spell_Spell_Options() var spellOptions = new SimcSpellOptions() { SpellId = 274740, - PlayerLevel = 80 + PlayerLevel = 90 }; // Act @@ -180,7 +180,7 @@ public async Task SSC_Creates_Player_Spell_Spell_Options() ClassicAssert.IsNotNull(spell); ClassicAssert.IsNotNull(spell.Effects); ClassicAssert.AreEqual(1.716d, spell.Effects[0].Coefficient); - ClassicAssert.AreEqual(3828.969615d, spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(131.08048629999999d, spell.Effects[0].ScaleBudget); } [Test] @@ -200,7 +200,7 @@ public async Task SSC_Creates_Player_Spell_With_Power() ClassicAssert.IsNotNull(spell); ClassicAssert.IsNotNull(spell.Effects); ClassicAssert.AreEqual(1.716d, spell.Effects[0].Coefficient); - ClassicAssert.AreEqual(3828.969615d, spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(33.631460769999997d, spell.Effects[0].ScaleBudget); } [Test] @@ -227,7 +227,7 @@ public async Task SSC_Creates_Player_Spell_With_Invalid_Trigger_Spell() public async Task SSC_Creates_Player_Spell_Raw() { // Arrange - var playerLevel = 70u; + var playerLevel = 90u; var spellId = 274740u; // Act @@ -237,7 +237,7 @@ public async Task SSC_Creates_Player_Spell_Raw() ClassicAssert.IsNotNull(spell); ClassicAssert.IsNotNull(spell.Effects); ClassicAssert.AreEqual(1.716d, spell.Effects[0].Coefficient); - ClassicAssert.AreEqual(453.3443671d, spell.Effects[0].ScaleBudget); + ClassicAssert.AreEqual(131.08048629999999d, spell.Effects[0].ScaleBudget); // Updated for midnight branch level 90 } [Test] diff --git a/SimcProfileParser/DataSync/RawDataExtractionService.cs b/SimcProfileParser/DataSync/RawDataExtractionService.cs index 0c2bbe9..c089a49 100644 --- a/SimcProfileParser/DataSync/RawDataExtractionService.cs +++ b/SimcProfileParser/DataSync/RawDataExtractionService.cs @@ -988,10 +988,10 @@ internal double[][] GenerateSpellScalingMultipliers(Dictionary i double[][] spellScalingTable = new double[numSpellScalingTables][]; for (int i = 0; i < numSpellScalingTables; i++) { - spellScalingTable[i] = new double[80]; + spellScalingTable[i] = new double[90]; } - string key = "_spell_scaling[][80] = {"; + string key = "_spell_scaling[][90] = {"; int start = rawData.IndexOf(key) + key.Length; int end = rawData.IndexOf("};", start);