diff --git a/Gw2Sharp.Benchmarks/ItemsPageDeserializeBenchmark.cs b/Gw2Sharp.Benchmarks/ItemsPageDeserializeBenchmark.cs new file mode 100644 index 000000000..ca8b3f539 --- /dev/null +++ b/Gw2Sharp.Benchmarks/ItemsPageDeserializeBenchmark.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using System.Text.Json; +using BenchmarkDotNet.Attributes; +using Gw2Sharp.WebApi.V2; +using Gw2Sharp.WebApi.V2.Models; + +namespace Gw2Sharp.Benchmarks +{ + [MedianColumn] + [MemoryDiagnoser] + public class ItemsPageDeserializeBenchmark + { + private const int OPERATIONS_PER_INVOKE = 50; + private byte[] jsonBytes = Array.Empty(); + private JsonSerializerOptions? jsonSerializerOptions = null; + private object? dump; + + [GlobalSetup] + public void GlobalSetup() + { + string path = Path.Combine("TestFiles", "Items", "Items.max_page_size.json"); + this.jsonBytes = System.IO.File.ReadAllBytes(path); + this.jsonSerializerOptions = SerializationHelpers.GetJsonSerializerOptions(); + } + + [Benchmark(OperationsPerInvoke=OPERATIONS_PER_INVOKE)] + public object DeserializeItemsPage() + { + for (int i = 0; i < OPERATIONS_PER_INVOKE; ++i) + { + this.dump = JsonSerializer.Deserialize>(this.jsonBytes, this.jsonSerializerOptions); + } + + return this.dump!; + } + } +} + diff --git a/Gw2Sharp.Benchmarks/ItemsPageGetCachedBenchmark.cs b/Gw2Sharp.Benchmarks/ItemsPageGetCachedBenchmark.cs new file mode 100644 index 000000000..879aa1df3 --- /dev/null +++ b/Gw2Sharp.Benchmarks/ItemsPageGetCachedBenchmark.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Gw2Sharp.Benchmarks +{ + /// + /// This benchmark essentially checks how much time + /// it takes to retrieve items page from the cache. + /// The page is initially loaded in + /// via once and retrieved from the same + /// instance during the benchmark. + /// + [MedianColumn] + [MemoryDiagnoser] + public class ItemsPageGetCachedBenchmark + { + private const int OPERATIONS_PER_INVOKE = 10; + private Connection? connection; + private Gw2Client? gw2Client; + private object? dump; + + [GlobalSetup] + public async Task GlobalSetupAsync() + { + this.connection = new Connection(); + this.gw2Client = new Gw2Client(this.connection); + await this.gw2Client.WebApi.V2.Items.PageAsync(1).ConfigureAwait(false); + } + + [GlobalCleanup] + public void GlobalCleanup() => this.gw2Client!.Dispose(); + + [Benchmark(OperationsPerInvoke=OPERATIONS_PER_INVOKE)] + public async Task DeserializeItemsBulkFastestAsync() + { + for (int i = 0; i < OPERATIONS_PER_INVOKE; ++i) + { + this.dump = await this.gw2Client!.WebApi.V2.Items.PageAsync(1).ConfigureAwait(false); + } + + return this.dump; + } + } +} + diff --git a/Gw2Sharp.Benchmarks/SerializationHelpers.cs b/Gw2Sharp.Benchmarks/SerializationHelpers.cs new file mode 100644 index 000000000..de8b6a3f7 --- /dev/null +++ b/Gw2Sharp.Benchmarks/SerializationHelpers.cs @@ -0,0 +1,29 @@ + +using System.Text.Json; +using Gw2Sharp.Json; +using Gw2Sharp.Json.Converters; + +namespace Gw2Sharp.Benchmarks +{ + public class SerializationHelpers + { + public static JsonSerializerOptions GetJsonSerializerOptions() + { + var options = new JsonSerializerOptions + { + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = SnakeCaseNamingPolicy.SnakeCase + }; + options.Converters.Add(new ApiEnumConverter()); + options.Converters.Add(new ApiFlagsConverter()); + options.Converters.Add(new ApiObjectConverter()); + options.Converters.Add(new ApiObjectListConverter()); + options.Converters.Add(new CastableTypeConverter()); + options.Converters.Add(new DictionaryIntKeyConverter()); + options.Converters.Add(new RenderUrlConverter(new Connection(), null!)); + options.Converters.Add(new TimeSpanConverter()); + return options; + } + } +} diff --git a/Gw2Sharp.Benchmarks/SnakeCaseJsonNamingPolicyBenchmark.cs b/Gw2Sharp.Benchmarks/SnakeCaseJsonNamingPolicyBenchmark.cs new file mode 100644 index 000000000..2e4d8cc88 --- /dev/null +++ b/Gw2Sharp.Benchmarks/SnakeCaseJsonNamingPolicyBenchmark.cs @@ -0,0 +1,38 @@ +using BenchmarkDotNet.Attributes; +using Gw2Sharp.Json; + +namespace Gw2Sharp.Benchmarks +{ + [MedianColumn] + [MemoryDiagnoser] + public class SnakeCaseJsonNamingPolicyBenchmark + { + private const int OPERATIONS_PER_INVOKE = 1_000_000; + private static readonly SnakeCaseNamingPolicy policy = SnakeCaseNamingPolicy.SnakeCase; + private const string CONVENTIONAL_PROPERTY_NAME = "HypotheticalPropertyNameWithUnrealisticlyManyWords"; + private const string UNCONVENTIONAL_PROPERTY_NAME = "HyPOThETicAl_PRopeRtYnAmE_wiThUNReAlisTicLyMaNy_wOrDs"; + private object? dump; + + [Benchmark(OperationsPerInvoke=OPERATIONS_PER_INVOKE)] + public object? ConvertConventionalPropertyNameFast() + { + for (int i = 0; i < OPERATIONS_PER_INVOKE; ++i) + { + this.dump = policy.ConvertName(CONVENTIONAL_PROPERTY_NAME); + } + + return this.dump; + } + + [Benchmark(OperationsPerInvoke=OPERATIONS_PER_INVOKE)] + public object? ConvertUnconventionalPropertyNameFast() + { + for (int i = 0; i < OPERATIONS_PER_INVOKE; ++i) + { + this.dump = policy.ConvertName(UNCONVENTIONAL_PROPERTY_NAME); + } + + return this.dump; + } + } +} diff --git a/Gw2Sharp.Tests/Json/SnakeCaseJsonNamingPolicyTests.cs b/Gw2Sharp.Tests/Json/SnakeCaseJsonNamingPolicyTests.cs new file mode 100644 index 000000000..5c127129c --- /dev/null +++ b/Gw2Sharp.Tests/Json/SnakeCaseJsonNamingPolicyTests.cs @@ -0,0 +1,43 @@ +using Gw2Sharp.Json; +using Xunit; + +namespace Gw2Sharp.Tests.Json.Converters +{ + public class SnakeCaseNamingPolicyTests + { + [Fact] + public void ConvertsUpperCamelCase() + { + const string propertyName = "UpperCamelCasePropertyName"; + string convertedPropertyName = SnakeCaseNamingPolicy.SnakeCase.ConvertName(propertyName); + Assert.Equal("upper_camel_case_property_name", convertedPropertyName); + } + + [Fact] + public void ConvertsLowerCamelCase() + { + const string propertyName = "lowerCamelCasePropertyName"; + string convertedPropertyName = SnakeCaseNamingPolicy.SnakeCase.ConvertName(propertyName); + Assert.Equal("lower_camel_case_property_name", convertedPropertyName); + } + + [Fact] + public void PreservesSnakeCase() + { + const string propertyName = "snake_case_property_name"; + string convertedPropertyName = SnakeCaseNamingPolicy.SnakeCase.ConvertName(propertyName); + Assert.Equal(propertyName, convertedPropertyName); + } + + [Theory] + [InlineData("GuildWars2EOD", "guild_wars2_eod")] + [InlineData("GW2EndOfDragons", "gw2_end_of_dragons")] + [InlineData("LWSeason2", "lwseason2")] + [InlineData("LW_Season2", "lw_season2")] + public void HandlesAcronyms(string propertyName, string expectedConvertedPropertyName) + { + string convertedPropertyName = SnakeCaseNamingPolicy.SnakeCase.ConvertName(propertyName); + Assert.Equal(expectedConvertedPropertyName, convertedPropertyName); + } + } +} diff --git a/Gw2Sharp.Tests/TestFiles/Items/Items.max_page_size.json b/Gw2Sharp.Tests/TestFiles/Items/Items.max_page_size.json new file mode 100644 index 000000000..2ce4c433e --- /dev/null +++ b/Gw2Sharp.Tests/TestFiles/Items/Items.max_page_size.json @@ -0,0 +1,2166 @@ +[ + { + "name": "Norgu's Coat", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Exotic", + "vendor_value": 396, + "default_skin": 9, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "HideSuffix", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 104, + "chat_link": "[&AgFoAAAA]", + "icon": "https://render.guildwars2.com/file/FB0AA64F98303AE5112408EF3DC8C7307EA118F8/61011.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 314, + "infusion_slots": [], + "attribute_adjustment": 384.12, + "infix_upgrade": { + "id": 159, + "attributes": [ + { + "attribute": "Power", + "modifier": 96 + }, + { + "attribute": "Precision", + "modifier": 134 + }, + { + "attribute": "ConditionDamage", + "modifier": 96 + } + ] + }, + "suffix_item_id": 24765, + "secondary_suffix_item_id": null + } + }, + { + "name": "Shaman's Seer Coat of the Flame Legion", + "description": "", + "type": "Armor", + "level": 65, + "rarity": "Exotic", + "vendor_value": 330, + "default_skin": 9, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 105, + "chat_link": "[&AgFpAAAA]", + "icon": "https://render.guildwars2.com/file/FB0AA64F98303AE5112408EF3DC8C7307EA118F8/61011.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 236, + "infusion_slots": [], + "attribute_adjustment": 286.47, + "infix_upgrade": { + "id": 153, + "attributes": [ + { + "attribute": "Vitality", + "modifier": 100 + }, + { + "attribute": "Healing", + "modifier": 72 + }, + { + "attribute": "ConditionDamage", + "modifier": 72 + } + ] + }, + "suffix_item_id": 24797, + "secondary_suffix_item_id": null + } + }, + { + "name": "Berserker's Sneakthief Coat of the Dolyak", + "description": "", + "type": "Armor", + "level": 62, + "rarity": "Exotic", + "vendor_value": 318, + "default_skin": 12, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 106, + "chat_link": "[&AgFqAAAA]", + "icon": "https://render.guildwars2.com/file/142DBA7E1144E3A61A02D9C8EC44225F327597F2/561549.png", + "details": { + "type": "Coat", + "weight_class": "Medium", + "defense": 238, + "infusion_slots": [], + "attribute_adjustment": 268.2, + "infix_upgrade": { + "id": 161, + "attributes": [ + { + "attribute": "Power", + "modifier": 94 + }, + { + "attribute": "Precision", + "modifier": 67 + }, + { + "attribute": "CritDamage", + "modifier": 67 + } + ] + }, + "suffix_item_id": 24699, + "secondary_suffix_item_id": null + } + }, + { + "name": "Shaman's Sneakthief Mask of the Grove", + "description": "", + "type": "Armor", + "level": 65, + "rarity": "Exotic", + "vendor_value": 275, + "default_skin": 95, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 107, + "chat_link": "[&AgFrAAAA]", + "icon": "https://render.guildwars2.com/file/65A0C7367206E6CE4EC7C8CBE07EABAE0191BFBA/561548.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 73, + "infusion_slots": [], + "attribute_adjustment": 127.32, + "infix_upgrade": { + "id": 153, + "attributes": [ + { + "attribute": "Vitality", + "modifier": 45 + }, + { + "attribute": "Healing", + "modifier": 32 + }, + { + "attribute": "ConditionDamage", + "modifier": 32 + } + ] + }, + "suffix_item_id": 24735, + "secondary_suffix_item_id": null + } + }, + { + "name": "Shaman's Seer Coat of Divinity", + "description": "", + "type": "Armor", + "level": 73, + "rarity": "Exotic", + "vendor_value": 366, + "default_skin": 9, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 108, + "chat_link": "[&AgFsAAAA]", + "icon": "https://render.guildwars2.com/file/FB0AA64F98303AE5112408EF3DC8C7307EA118F8/61011.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 276, + "infusion_slots": [], + "attribute_adjustment": 336.51, + "infix_upgrade": { + "id": 153, + "attributes": [ + { + "attribute": "Vitality", + "modifier": 118 + }, + { + "attribute": "Healing", + "modifier": 84 + }, + { + "attribute": "ConditionDamage", + "modifier": 84 + } + ] + }, + "suffix_item_id": 24732, + "secondary_suffix_item_id": null + } + }, + { + "name": "Shaman's Sneakthief Mask of Melandru", + "description": "", + "type": "Armor", + "level": 73, + "rarity": "Exotic", + "vendor_value": 305, + "default_skin": 95, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 109, + "chat_link": "[&AgFtAAAA]", + "icon": "https://render.guildwars2.com/file/65A0C7367206E6CE4EC7C8CBE07EABAE0191BFBA/561548.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 85, + "infusion_slots": [], + "attribute_adjustment": 149.56, + "infix_upgrade": { + "id": 153, + "attributes": [ + { + "attribute": "Vitality", + "modifier": 52 + }, + { + "attribute": "Healing", + "modifier": 37 + }, + { + "attribute": "ConditionDamage", + "modifier": 37 + } + ] + }, + "suffix_item_id": 24771, + "secondary_suffix_item_id": null + } + }, + { + "name": "Berserker's Reinforced Scale Boots", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Fine", + "vendor_value": 66, + "default_skin": 74, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 110, + "chat_link": "[&AgFuAAAA]", + "icon": "https://render.guildwars2.com/file/17C9516D0F0B2EBA616ED794C00F59411E931702/61014.png", + "details": { + "type": "Boots", + "weight_class": "Heavy", + "defense": 138, + "infusion_slots": [], + "attribute_adjustment": 96.984, + "infix_upgrade": { + "id": 161, + "attributes": [ + { + "attribute": "Power", + "modifier": 34 + }, + { + "attribute": "Precision", + "modifier": 24 + }, + { + "attribute": "CritDamage", + "modifier": 24 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Tahlkora's Coat", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Exotic", + "vendor_value": 396, + "default_skin": 9, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "HideSuffix", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 111, + "chat_link": "[&AgFvAAAA]", + "icon": "https://render.guildwars2.com/file/FB0AA64F98303AE5112408EF3DC8C7307EA118F8/61011.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 314, + "infusion_slots": [], + "attribute_adjustment": 384.12, + "infix_upgrade": { + "id": 153, + "attributes": [ + { + "attribute": "Vitality", + "modifier": 134 + }, + { + "attribute": "Healing", + "modifier": 96 + }, + { + "attribute": "ConditionDamage", + "modifier": 96 + } + ] + }, + "suffix_item_id": 24708, + "secondary_suffix_item_id": null + } + }, + { + "name": "Berserker's Sneakthief Coat of the Centaur", + "description": "", + "type": "Armor", + "level": 70, + "rarity": "Exotic", + "vendor_value": 354, + "default_skin": 12, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 112, + "chat_link": "[&AgFwAAAA]", + "icon": "https://render.guildwars2.com/file/142DBA7E1144E3A61A02D9C8EC44225F327597F2/561549.png", + "details": { + "type": "Coat", + "weight_class": "Medium", + "defense": 281, + "infusion_slots": [], + "attribute_adjustment": 317.79, + "infix_upgrade": { + "id": 161, + "attributes": [ + { + "attribute": "Power", + "modifier": 111 + }, + { + "attribute": "Precision", + "modifier": 79 + }, + { + "attribute": "CritDamage", + "modifier": 79 + } + ] + }, + "suffix_item_id": 24788, + "secondary_suffix_item_id": null + } + }, + { + "name": "Cleric's Seer Coat of the Eagle", + "description": "", + "type": "Armor", + "level": 66, + "rarity": "Exotic", + "vendor_value": 336, + "default_skin": 9, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 113, + "chat_link": "[&AgFxAAAA]", + "icon": "https://render.guildwars2.com/file/FB0AA64F98303AE5112408EF3DC8C7307EA118F8/61011.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 241, + "infusion_slots": [], + "attribute_adjustment": 292.77, + "infix_upgrade": { + "id": 155, + "attributes": [ + { + "attribute": "Power", + "modifier": 73 + }, + { + "attribute": "Toughness", + "modifier": 73 + }, + { + "attribute": "Healing", + "modifier": 102 + } + ] + }, + "suffix_item_id": 24723, + "secondary_suffix_item_id": null + } + }, + { + "name": "Mighty Worn Chain Greaves", + "description": "", + "type": "Armor", + "level": 16, + "rarity": "Fine", + "vendor_value": 20, + "default_skin": 40, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 114, + "chat_link": "[&AgFyAAAA]", + "icon": "https://render.guildwars2.com/file/044D675DA29AFB786E32612D6B9141CA08D1EFEA/61009.png", + "details": { + "type": "Boots", + "weight_class": "Heavy", + "defense": 27, + "infusion_slots": [], + "attribute_adjustment": 12.576, + "infix_upgrade": { + "id": 137, + "attributes": [ + { + "attribute": "Power", + "modifier": 4 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Reyna's Mask", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Exotic", + "vendor_value": 330, + "default_skin": 95, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "HideSuffix", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 115, + "chat_link": "[&AgFzAAAA]", + "icon": "https://render.guildwars2.com/file/65A0C7367206E6CE4EC7C8CBE07EABAE0191BFBA/561548.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 97, + "infusion_slots": [], + "attribute_adjustment": 170.72, + "infix_upgrade": { + "id": 153, + "attributes": [ + { + "attribute": "Vitality", + "modifier": 60 + }, + { + "attribute": "Healing", + "modifier": 43 + }, + { + "attribute": "ConditionDamage", + "modifier": 43 + } + ] + }, + "suffix_item_id": 24771, + "secondary_suffix_item_id": null + } + }, + { + "name": "Cleric's Sneakthief Mask of the Pack", + "description": "", + "type": "Armor", + "level": 66, + "rarity": "Exotic", + "vendor_value": 280, + "default_skin": 95, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 116, + "chat_link": "[&AgF0AAAA]", + "icon": "https://render.guildwars2.com/file/65A0C7367206E6CE4EC7C8CBE07EABAE0191BFBA/561548.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 74, + "infusion_slots": [], + "attribute_adjustment": 130.12, + "infix_upgrade": { + "id": 155, + "attributes": [ + { + "attribute": "Power", + "modifier": 33 + }, + { + "attribute": "Toughness", + "modifier": 33 + }, + { + "attribute": "Healing", + "modifier": 46 + } + ] + }, + "suffix_item_id": 24702, + "secondary_suffix_item_id": null + } + }, + { + "name": "Cleric's Seer Coat of the Afflicted", + "description": "", + "type": "Armor", + "level": 74, + "rarity": "Exotic", + "vendor_value": 366, + "default_skin": 9, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 117, + "chat_link": "[&AgF1AAAA]", + "icon": "https://render.guildwars2.com/file/FB0AA64F98303AE5112408EF3DC8C7307EA118F8/61011.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 282, + "infusion_slots": [], + "attribute_adjustment": 343.35, + "infix_upgrade": { + "id": 155, + "attributes": [ + { + "attribute": "Power", + "modifier": 86 + }, + { + "attribute": "Toughness", + "modifier": 86 + }, + { + "attribute": "Healing", + "modifier": 120 + } + ] + }, + "suffix_item_id": 24687, + "secondary_suffix_item_id": null + } + }, + { + "name": "Mhenlo's Coat", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Exotic", + "vendor_value": 396, + "default_skin": 9, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "HideSuffix", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 118, + "chat_link": "[&AgF2AAAA]", + "icon": "https://render.guildwars2.com/file/FB0AA64F98303AE5112408EF3DC8C7307EA118F8/61011.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 314, + "infusion_slots": [], + "attribute_adjustment": 384.12, + "infix_upgrade": { + "id": 155, + "attributes": [ + { + "attribute": "Power", + "modifier": 96 + }, + { + "attribute": "Toughness", + "modifier": 96 + }, + { + "attribute": "Healing", + "modifier": 134 + } + ] + }, + "suffix_item_id": 24768, + "secondary_suffix_item_id": null + } + }, + { + "name": "Berserker's Sneakthief Coat of Vampirism", + "description": "", + "type": "Armor", + "level": 78, + "rarity": "Exotic", + "vendor_value": 384, + "default_skin": 12, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 119, + "chat_link": "[&AgF3AAAA]", + "icon": "https://render.guildwars2.com/file/142DBA7E1144E3A61A02D9C8EC44225F327597F2/561549.png", + "details": { + "type": "Coat", + "weight_class": "Medium", + "defense": 327, + "infusion_slots": [], + "attribute_adjustment": 370.53, + "infix_upgrade": { + "id": 161, + "attributes": [ + { + "attribute": "Power", + "modifier": 130 + }, + { + "attribute": "Precision", + "modifier": 93 + }, + { + "attribute": "CritDamage", + "modifier": 93 + } + ] + }, + "suffix_item_id": 24711, + "secondary_suffix_item_id": null + } + }, + { + "name": "Strong Worn Chain Greaves", + "description": "", + "type": "Armor", + "level": 21, + "rarity": "Fine", + "vendor_value": 24, + "default_skin": 40, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 120, + "chat_link": "[&AgF4AAAA]", + "icon": "https://render.guildwars2.com/file/044D675DA29AFB786E32612D6B9141CA08D1EFEA/61009.png", + "details": { + "type": "Boots", + "weight_class": "Heavy", + "defense": 32, + "infusion_slots": [], + "attribute_adjustment": 16.584, + "infix_upgrade": { + "id": 142, + "attributes": [ + { + "attribute": "Power", + "modifier": 6 + }, + { + "attribute": "Precision", + "modifier": 4 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Cleric's Sneakthief Mask of Rage", + "description": "", + "type": "Armor", + "level": 74, + "rarity": "Exotic", + "vendor_value": 305, + "default_skin": 95, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 121, + "chat_link": "[&AgF5AAAA]", + "icon": "https://render.guildwars2.com/file/65A0C7367206E6CE4EC7C8CBE07EABAE0191BFBA/561548.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 87, + "infusion_slots": [], + "attribute_adjustment": 152.6, + "infix_upgrade": { + "id": 155, + "attributes": [ + { + "attribute": "Power", + "modifier": 38 + }, + { + "attribute": "Toughness", + "modifier": 38 + }, + { + "attribute": "Healing", + "modifier": 53 + } + ] + }, + "suffix_item_id": 24717, + "secondary_suffix_item_id": null + } + }, + { + "name": "Seer Coat of Rage", + "description": "", + "type": "Armor", + "level": 75, + "rarity": "Exotic", + "vendor_value": 372, + "default_skin": 9, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "AccountBound", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 122, + "chat_link": "[&AgF6AAAA]", + "icon": "https://render.guildwars2.com/file/FB0AA64F98303AE5112408EF3DC8C7307EA118F8/61011.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 287, + "infusion_slots": [], + "attribute_adjustment": 350.1, + "suffix_item_id": 24717, + "stat_choices": [ + 161, + 154, + 158 + ], + "secondary_suffix_item_id": null + } + }, + { + "name": "Zho's Mask", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Exotic", + "vendor_value": 330, + "default_skin": 95, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "HideSuffix", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 123, + "chat_link": "[&AgF7AAAA]", + "icon": "https://render.guildwars2.com/file/65A0C7367206E6CE4EC7C8CBE07EABAE0191BFBA/561548.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 97, + "infusion_slots": [], + "attribute_adjustment": 170.72, + "infix_upgrade": { + "id": 155, + "attributes": [ + { + "attribute": "Power", + "modifier": 43 + }, + { + "attribute": "Toughness", + "modifier": 43 + }, + { + "attribute": "Healing", + "modifier": 60 + } + ] + }, + "suffix_item_id": 24696, + "secondary_suffix_item_id": null + } + }, + { + "name": "Nika's Coat", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Exotic", + "vendor_value": 396, + "default_skin": 12, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "HideSuffix", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 125, + "chat_link": "[&AgF9AAAA]", + "icon": "https://render.guildwars2.com/file/142DBA7E1144E3A61A02D9C8EC44225F327597F2/561549.png", + "details": { + "type": "Coat", + "weight_class": "Medium", + "defense": 338, + "infusion_slots": [], + "attribute_adjustment": 384.12, + "infix_upgrade": { + "id": 161, + "attributes": [ + { + "attribute": "Power", + "modifier": 134 + }, + { + "attribute": "Precision", + "modifier": 96 + }, + { + "attribute": "CritDamage", + "modifier": 96 + } + ] + }, + "suffix_item_id": 24703, + "secondary_suffix_item_id": null + } + }, + { + "name": "Strong Worn Chain Greaves", + "description": "", + "type": "Armor", + "level": 26, + "rarity": "Fine", + "vendor_value": 28, + "default_skin": 40, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 126, + "chat_link": "[&AgF+AAAA]", + "icon": "https://render.guildwars2.com/file/044D675DA29AFB786E32612D6B9141CA08D1EFEA/61009.png", + "details": { + "type": "Boots", + "weight_class": "Heavy", + "defense": 37, + "infusion_slots": [], + "attribute_adjustment": 20.616, + "infix_upgrade": { + "id": 142, + "attributes": [ + { + "attribute": "Power", + "modifier": 7 + }, + { + "attribute": "Precision", + "modifier": 5 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Berserker's Seer Coat of Rata Sum", + "description": "", + "type": "Armor", + "level": 76, + "rarity": "Exotic", + "vendor_value": 378, + "default_skin": 9, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 128, + "chat_link": "[&AgGAAAAA]", + "icon": "https://render.guildwars2.com/file/FB0AA64F98303AE5112408EF3DC8C7307EA118F8/61011.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 292, + "infusion_slots": [], + "attribute_adjustment": 356.94, + "infix_upgrade": { + "id": 161, + "attributes": [ + { + "attribute": "Power", + "modifier": 125 + }, + { + "attribute": "Precision", + "modifier": 89 + }, + { + "attribute": "CritDamage", + "modifier": 89 + } + ] + }, + "suffix_item_id": 24726, + "secondary_suffix_item_id": null + } + }, + { + "name": "Strong Worn Scale Boots", + "description": "", + "type": "Armor", + "level": 31, + "rarity": "Fine", + "vendor_value": 31, + "default_skin": 65, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 129, + "chat_link": "[&AgGBAAAA]", + "icon": "https://render.guildwars2.com/file/9CD8B8786A6F047FD375A7E5301A52320AD60ACC/61012.png", + "details": { + "type": "Boots", + "weight_class": "Heavy", + "defense": 44, + "infusion_slots": [], + "attribute_adjustment": 26.16, + "infix_upgrade": { + "id": 142, + "attributes": [ + { + "attribute": "Power", + "modifier": 9 + }, + { + "attribute": "Precision", + "modifier": 7 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Errol's Mask", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Exotic", + "vendor_value": 330, + "default_skin": 95, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "HideSuffix", + "AccountBound", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 130, + "chat_link": "[&AgGCAAAA]", + "icon": "https://render.guildwars2.com/file/65A0C7367206E6CE4EC7C8CBE07EABAE0191BFBA/561548.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 97, + "infusion_slots": [], + "attribute_adjustment": 170.72, + "suffix_item_id": 24723, + "secondary_suffix_item_id": null, + "stat_choices": [ + 161, + 154, + 158 + ] + } + }, + { + "name": "Khilbron's Coat", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Exotic", + "vendor_value": 396, + "default_skin": 9, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "HideSuffix", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 131, + "chat_link": "[&AgGDAAAA]", + "icon": "https://render.guildwars2.com/file/FB0AA64F98303AE5112408EF3DC8C7307EA118F8/61011.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 314, + "infusion_slots": [], + "attribute_adjustment": 384.12, + "infix_upgrade": { + "id": 154, + "attributes": [ + { + "attribute": "Precision", + "modifier": 96 + }, + { + "attribute": "Toughness", + "modifier": 96 + }, + { + "attribute": "ConditionDamage", + "modifier": 134 + } + ] + }, + "suffix_item_id": 24688, + "secondary_suffix_item_id": null + } + }, + { + "name": "Mighty Country Coat", + "description": "", + "type": "Armor", + "level": 6, + "rarity": "Fine", + "vendor_value": 17, + "default_skin": 13, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 132, + "chat_link": "[&AgGEAAAA]", + "icon": "https://render.guildwars2.com/file/F03808FFE89B40044671EED2E427053B389BE0A1/61007.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 33, + "infusion_slots": [], + "attribute_adjustment": 18.576, + "infix_upgrade": { + "id": 137, + "attributes": [ + { + "attribute": "Power", + "modifier": 7 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Berserker's Sneakthief Mask of the Flame Legion", + "description": "", + "type": "Armor", + "level": 76, + "rarity": "Exotic", + "vendor_value": 315, + "default_skin": 95, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 133, + "chat_link": "[&AgGFAAAA]", + "icon": "https://render.guildwars2.com/file/65A0C7367206E6CE4EC7C8CBE07EABAE0191BFBA/561548.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 90, + "infusion_slots": [], + "attribute_adjustment": 158.64, + "infix_upgrade": { + "id": 161, + "attributes": [ + { + "attribute": "Power", + "modifier": 56 + }, + { + "attribute": "Precision", + "modifier": 40 + }, + { + "attribute": "CritDamage", + "modifier": 40 + } + ] + }, + "suffix_item_id": 24797, + "secondary_suffix_item_id": null + } + }, + { + "name": "Strong Worn Scale Boots", + "description": "", + "type": "Armor", + "level": 36, + "rarity": "Fine", + "vendor_value": 35, + "default_skin": 65, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 134, + "chat_link": "[&AgGGAAAA]", + "icon": "https://render.guildwars2.com/file/9CD8B8786A6F047FD375A7E5301A52320AD60ACC/61012.png", + "details": { + "type": "Boots", + "weight_class": "Heavy", + "defense": 51, + "infusion_slots": [], + "attribute_adjustment": 31.632, + "infix_upgrade": { + "id": 142, + "attributes": [ + { + "attribute": "Power", + "modifier": 11 + }, + { + "attribute": "Precision", + "modifier": 8 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Mighty Country Coat", + "description": "", + "type": "Armor", + "level": 11, + "rarity": "Fine", + "vendor_value": 24, + "default_skin": 13, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 135, + "chat_link": "[&AgGHAAAA]", + "icon": "https://render.guildwars2.com/file/F03808FFE89B40044671EED2E427053B389BE0A1/61007.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 40, + "infusion_slots": [], + "attribute_adjustment": 27.792, + "infix_upgrade": { + "id": 137, + "attributes": [ + { + "attribute": "Power", + "modifier": 10 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Vatlaaw's Mask", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Exotic", + "vendor_value": 330, + "default_skin": 95, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "HideSuffix", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 136, + "chat_link": "[&AgGIAAAA]", + "icon": "https://render.guildwars2.com/file/65A0C7367206E6CE4EC7C8CBE07EABAE0191BFBA/561548.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 97, + "infusion_slots": [], + "attribute_adjustment": 170.72, + "infix_upgrade": { + "id": 154, + "attributes": [ + { + "attribute": "Precision", + "modifier": 43 + }, + { + "attribute": "Toughness", + "modifier": 43 + }, + { + "attribute": "ConditionDamage", + "modifier": 60 + } + ] + }, + "suffix_item_id": 24797, + "secondary_suffix_item_id": null + } + }, + { + "name": "Mighty Country Coat", + "description": "", + "type": "Armor", + "level": 16, + "rarity": "Fine", + "vendor_value": 30, + "default_skin": 13, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 137, + "chat_link": "[&AgGJAAAA]", + "icon": "https://render.guildwars2.com/file/F03808FFE89B40044671EED2E427053B389BE0A1/61007.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 47, + "infusion_slots": [], + "attribute_adjustment": 37.728, + "infix_upgrade": { + "id": 137, + "attributes": [ + { + "attribute": "Power", + "modifier": 13 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Mighty Swindler Mask", + "description": "", + "type": "Armor", + "level": 11, + "rarity": "Fine", + "vendor_value": 20, + "default_skin": 97, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 138, + "chat_link": "[&AgGKAAAA]", + "icon": "https://render.guildwars2.com/file/F1D496205002073C9656A7E6E7550D0041045F0E/61016.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 13, + "infusion_slots": [], + "attribute_adjustment": 12.352, + "infix_upgrade": { + "id": 137, + "attributes": [ + { + "attribute": "Power", + "modifier": 4 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Carrion Sneakthief Coat of Mercy", + "description": "", + "type": "Armor", + "level": 63, + "rarity": "Exotic", + "vendor_value": 324, + "default_skin": 12, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 139, + "chat_link": "[&AgGLAAAA]", + "icon": "https://render.guildwars2.com/file/142DBA7E1144E3A61A02D9C8EC44225F327597F2/561549.png", + "details": { + "type": "Coat", + "weight_class": "Medium", + "defense": 243, + "infusion_slots": [], + "attribute_adjustment": 273.96, + "infix_upgrade": { + "id": 160, + "attributes": [ + { + "attribute": "Power", + "modifier": 68 + }, + { + "attribute": "Vitality", + "modifier": 68 + }, + { + "attribute": "ConditionDamage", + "modifier": 96 + } + ] + }, + "suffix_item_id": 24708, + "secondary_suffix_item_id": null + } + }, + { + "name": "Strong Worn Scale Boots", + "description": "", + "type": "Armor", + "level": 41, + "rarity": "Fine", + "vendor_value": 38, + "default_skin": 65, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 140, + "chat_link": "[&AgGMAAAA]", + "icon": "https://render.guildwars2.com/file/9CD8B8786A6F047FD375A7E5301A52320AD60ACC/61012.png", + "details": { + "type": "Boots", + "weight_class": "Heavy", + "defense": 58, + "infusion_slots": [], + "attribute_adjustment": 37.824, + "infix_upgrade": { + "id": 142, + "attributes": [ + { + "attribute": "Power", + "modifier": 13 + }, + { + "attribute": "Precision", + "modifier": 9 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Mighty Swindler Mask", + "description": "", + "type": "Armor", + "level": 16, + "rarity": "Fine", + "vendor_value": 25, + "default_skin": 97, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 141, + "chat_link": "[&AgGNAAAA]", + "icon": "https://render.guildwars2.com/file/F1D496205002073C9656A7E6E7550D0041045F0E/61016.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 15, + "infusion_slots": [], + "attribute_adjustment": 16.768, + "infix_upgrade": { + "id": 137, + "attributes": [ + { + "attribute": "Power", + "modifier": 6 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Precise Country Coat", + "description": "", + "type": "Armor", + "level": 7, + "rarity": "Fine", + "vendor_value": 18, + "default_skin": 13, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 142, + "chat_link": "[&AgGOAAAA]", + "icon": "https://render.guildwars2.com/file/F03808FFE89B40044671EED2E427053B389BE0A1/61007.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 34, + "infusion_slots": [], + "attribute_adjustment": 19.872, + "infix_upgrade": { + "id": 138, + "attributes": [ + { + "attribute": "Precision", + "modifier": 7 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Carrion Sneakthief Coat of the Dolyak", + "description": "", + "type": "Armor", + "level": 71, + "rarity": "Exotic", + "vendor_value": 354, + "default_skin": 12, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 143, + "chat_link": "[&AgGPAAAA]", + "icon": "https://render.guildwars2.com/file/142DBA7E1144E3A61A02D9C8EC44225F327597F2/561549.png", + "details": { + "type": "Coat", + "weight_class": "Medium", + "defense": 286, + "infusion_slots": [], + "attribute_adjustment": 324, + "infix_upgrade": { + "id": 160, + "attributes": [ + { + "attribute": "Power", + "modifier": 81 + }, + { + "attribute": "Vitality", + "modifier": 81 + }, + { + "attribute": "ConditionDamage", + "modifier": 113 + } + ] + }, + "suffix_item_id": 24699, + "secondary_suffix_item_id": null + } + }, + { + "name": "Precise Country Coat", + "description": "", + "type": "Armor", + "level": 12, + "rarity": "Fine", + "vendor_value": 26, + "default_skin": 13, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 144, + "chat_link": "[&AgGQAAAA]", + "icon": "https://render.guildwars2.com/file/F03808FFE89B40044671EED2E427053B389BE0A1/61007.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 41, + "infusion_slots": [], + "attribute_adjustment": 29.808, + "infix_upgrade": { + "id": 138, + "attributes": [ + { + "attribute": "Precision", + "modifier": 10 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Precise Swindler Mask", + "description": "", + "type": "Armor", + "level": 12, + "rarity": "Fine", + "vendor_value": 22, + "default_skin": 97, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 145, + "chat_link": "[&AgGRAAAA]", + "icon": "https://render.guildwars2.com/file/F1D496205002073C9656A7E6E7550D0041045F0E/61016.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 13, + "infusion_slots": [], + "attribute_adjustment": 13.248, + "infix_upgrade": { + "id": 138, + "attributes": [ + { + "attribute": "Precision", + "modifier": 5 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Precise Country Coat", + "description": "", + "type": "Armor", + "level": 17, + "rarity": "Fine", + "vendor_value": 32, + "default_skin": 13, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 146, + "chat_link": "[&AgGSAAAA]", + "icon": "https://render.guildwars2.com/file/F03808FFE89B40044671EED2E427053B389BE0A1/61007.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 49, + "infusion_slots": [], + "attribute_adjustment": 40.104, + "infix_upgrade": { + "id": 138, + "attributes": [ + { + "attribute": "Precision", + "modifier": 14 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Precise Swindler Mask", + "description": "", + "type": "Armor", + "level": 17, + "rarity": "Fine", + "vendor_value": 27, + "default_skin": 97, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 147, + "chat_link": "[&AgGTAAAA]", + "icon": "https://render.guildwars2.com/file/F1D496205002073C9656A7E6E7550D0041045F0E/61016.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 15, + "infusion_slots": [], + "attribute_adjustment": 17.824, + "infix_upgrade": { + "id": 138, + "attributes": [ + { + "attribute": "Precision", + "modifier": 6 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Mighty Worn Chain Greaves", + "type": "Armor", + "level": 0, + "rarity": "Basic", + "vendor_value": 4, + "default_skin": 40, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "NotUpgradeable" + ], + "restrictions": [], + "id": 148, + "chat_link": "[&AgGUAAAA]", + "icon": "https://render.guildwars2.com/file/044D675DA29AFB786E32612D6B9141CA08D1EFEA/61009.png", + "details": { + "type": "Boots", + "weight_class": "Heavy", + "defense": 13, + "infusion_slots": [], + "attribute_adjustment": 3.861, + "infix_upgrade": { + "id": 137, + "attributes": [ + { + "attribute": "Power", + "modifier": 1 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Vital Country Coat", + "description": "", + "type": "Armor", + "level": 8, + "rarity": "Fine", + "vendor_value": 20, + "default_skin": 13, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 149, + "chat_link": "[&AgGVAAAA]", + "icon": "https://render.guildwars2.com/file/F03808FFE89B40044671EED2E427053B389BE0A1/61007.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 35, + "infusion_slots": [], + "attribute_adjustment": 21.888, + "infix_upgrade": { + "id": 139, + "attributes": [ + { + "attribute": "Vitality", + "modifier": 8 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Vital Country Coat", + "description": "", + "type": "Armor", + "level": 13, + "rarity": "Fine", + "vendor_value": 27, + "default_skin": 13, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 150, + "chat_link": "[&AgGWAAAA]", + "icon": "https://render.guildwars2.com/file/F03808FFE89B40044671EED2E427053B389BE0A1/61007.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 43, + "infusion_slots": [], + "attribute_adjustment": 31.752, + "infix_upgrade": { + "id": 139, + "attributes": [ + { + "attribute": "Vitality", + "modifier": 11 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Carrion Sneakthief Coat of Hoelbrak", + "description": "", + "type": "Armor", + "level": 79, + "rarity": "Exotic", + "vendor_value": 390, + "default_skin": 12, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "SoulBindOnUse" + ], + "restrictions": [], + "id": 151, + "chat_link": "[&AgGXAAAA]", + "icon": "https://render.guildwars2.com/file/142DBA7E1144E3A61A02D9C8EC44225F327597F2/561549.png", + "details": { + "type": "Coat", + "weight_class": "Medium", + "defense": 333, + "infusion_slots": [], + "attribute_adjustment": 377.28, + "infix_upgrade": { + "id": 160, + "attributes": [ + { + "attribute": "Power", + "modifier": 94 + }, + { + "attribute": "Vitality", + "modifier": 94 + }, + { + "attribute": "ConditionDamage", + "modifier": 132 + } + ] + }, + "suffix_item_id": 24729, + "secondary_suffix_item_id": null + } + }, + { + "name": "Precise Worn Chain Greaves", + "description": "", + "type": "Armor", + "level": 7, + "rarity": "Fine", + "vendor_value": 12, + "default_skin": 40, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 152, + "chat_link": "[&AgGYAAAA]", + "icon": "https://render.guildwars2.com/file/044D675DA29AFB786E32612D6B9141CA08D1EFEA/61009.png", + "details": { + "type": "Boots", + "weight_class": "Heavy", + "defense": 20, + "infusion_slots": [], + "attribute_adjustment": 6.624, + "infix_upgrade": { + "id": 138, + "attributes": [ + { + "attribute": "Precision", + "modifier": 2 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Vital Swindler Mask", + "description": "", + "type": "Armor", + "level": 13, + "rarity": "Fine", + "vendor_value": 23, + "default_skin": 97, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 153, + "chat_link": "[&AgGZAAAA]", + "icon": "https://render.guildwars2.com/file/F1D496205002073C9656A7E6E7550D0041045F0E/61016.png", + "details": { + "type": "Helm", + "weight_class": "Medium", + "defense": 13, + "infusion_slots": [], + "attribute_adjustment": 14.112, + "infix_upgrade": { + "id": 139, + "attributes": [ + { + "attribute": "Vitality", + "modifier": 5 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Vital Country Coat", + "description": "", + "type": "Armor", + "level": 18, + "rarity": "Fine", + "vendor_value": 33, + "default_skin": 13, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [], + "restrictions": [], + "id": 154, + "chat_link": "[&AgGaAAAA]", + "icon": "https://render.guildwars2.com/file/F03808FFE89B40044671EED2E427053B389BE0A1/61007.png", + "details": { + "type": "Coat", + "weight_class": "Light", + "defense": 50, + "infusion_slots": [], + "attribute_adjustment": 42.552, + "infix_upgrade": { + "id": 139, + "attributes": [ + { + "attribute": "Vitality", + "modifier": 15 + } + ] + }, + "secondary_suffix_item_id": null + } + }, + { + "name": "Shiro's Coat", + "description": "", + "type": "Armor", + "level": 80, + "rarity": "Exotic", + "vendor_value": 396, + "default_skin": 12, + "game_types": [ + "Activity", + "Wvw", + "Dungeon", + "Pve" + ], + "flags": [ + "HideSuffix", + "SoulBindOnUse" + ], + "restrictions": [], + "id": 155, + "chat_link": "[&AgGbAAAA]", + "icon": "https://render.guildwars2.com/file/142DBA7E1144E3A61A02D9C8EC44225F327597F2/561549.png", + "details": { + "type": "Coat", + "weight_class": "Medium", + "defense": 338, + "infusion_slots": [], + "attribute_adjustment": 384.12, + "infix_upgrade": { + "id": 160, + "attributes": [ + { + "attribute": "Power", + "modifier": 96 + }, + { + "attribute": "Vitality", + "modifier": 96 + }, + { + "attribute": "ConditionDamage", + "modifier": 134 + } + ] + }, + "suffix_item_id": 24717, + "secondary_suffix_item_id": null + } + } +] diff --git a/Gw2Sharp/Json/SnakeCaseNamingPolicy.cs b/Gw2Sharp/Json/SnakeCaseNamingPolicy.cs index a9664bdc7..0d1fa7fc3 100644 --- a/Gw2Sharp/Json/SnakeCaseNamingPolicy.cs +++ b/Gw2Sharp/Json/SnakeCaseNamingPolicy.cs @@ -1,13 +1,50 @@ -using System.Linq; +using System; +using System.Text; using System.Text.Json; namespace Gw2Sharp.Json { - internal class SnakeCaseNamingPolicy : JsonNamingPolicy + /// + /// Many use cases are not supported (e.g. emojis). + /// Use with caution. + /// + public class SnakeCaseNamingPolicy : JsonNamingPolicy { - public static SnakeCaseNamingPolicy SnakeCase => new SnakeCaseNamingPolicy(); + /// + public static SnakeCaseNamingPolicy SnakeCase => new(); - public override string ConvertName(string name) => - string.Concat(name.Select((x, i) => i > 0 && char.IsUpper(x) ? $"_{x}" : x.ToString())).ToLowerInvariant(); + /// + public override string ConvertName(string name) + { + var sb = new StringBuilder(); + var nameSpan = name.AsSpan(); + bool lastCharWasLower = false; + + for (int i = 0; i < nameSpan.Length; ++i) + { + char character = nameSpan[i]; + + if (char.IsUpper(character)) + { + if (lastCharWasLower) + { + sb.Append('_'); + } + sb.Append(char.ToLowerInvariant(character)); + lastCharWasLower = false; + } + else if (character == '_') + { + sb.Append('_'); + lastCharWasLower = false; + } + else { + sb.Append(character); + lastCharWasLower = true; + } + } + + return sb.ToString(); + } } }