Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 136 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ class MyClass
}
}
```

### Examples

#### Parsing profile files/strings
Generating a profile object from a simc import file named `import.simc`:

Expand All @@ -58,7 +60,13 @@ ISimcGenerationService sgs = new SimcGenerationService();
// Using async
var profile = await sgs.GenerateProfileAsync(File.ReadAllText("import.simc"));

Console.WriteLine($"Profile object created for player {profile.Name}.");
// Output some details about the profile
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);
```

You can also generate a profile object from individual lines of an import file:
Expand All @@ -68,16 +76,28 @@ ISimcGenerationService sgs = new SimcGenerationService();

var lines = new List<string>()
{
"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);

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`.

Expand All @@ -86,65 +106,153 @@ 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 item = await sgs.GenerateItemAsync(itemOptions);

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<int> BonusIds { get; set; }
public IList<int> 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<int> BonusIds { get; set; } // Bonus IDs that modify stats, sockets, or item level
public IList<int> GemIds { get; set; } // Gem item IDs to socket into the item
public IList<int> 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 = 226,
SpellId = 343538,
SpellId = 1238697,
ItemLevel = 730,
ItemQuality = ItemQuality.ITEM_QUALITY_EPIC,
ItemInventoryType = InventoryType.INVTYPE_TRINKET
};

var spell = await sgs.GenerateSpellAsync(spellOptions);
```

Generating an player scaling spell (id 274740):
// 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);
```

**Player Scaling** - For spells that scale with player level (racials, class abilities):
```csharp
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);
```

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}");
```

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.
**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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ public void RDE_Generates_SpellScale_Multi()

var incomingRawData = new Dictionary<string, string>()
{
{ "ScaleData.raw", @"static constexpr double __spell_scaling[][80] = {
{ "ScaleData.raw", @"static constexpr double __spell_scaling[][90] = {
{
1, 0, 0, 0, 0, // 5
},
Expand Down Expand Up @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -110,7 +110,7 @@ public async Task SGS_Creates_PlayerSpell()
var spellOptions = new SimcSpellOptions()
{
SpellId = 274740,
PlayerLevel = 60
PlayerLevel = 90
};

// Act
Expand All @@ -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]
Expand All @@ -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));
}
}
}
34 changes: 17 additions & 17 deletions SimcProfileParser.Tests/SimcItemCreationServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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]
Expand All @@ -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);
}
}
}
Loading