From c7c332ea22d0b78b1cda312055cca968cece63f6 Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Sat, 5 Sep 2026 16:59:38 -0700 Subject: [PATCH 1/2] [armory] Use item stats directly from the API for correct catalyzed item stats There's no indication from the API that a tier item was catalyzed from something else except for the actual stats on the item. No bonus ID or additional property in the JSON shows up. So the simplest way to get things working is just to accept all the stats from the Armory and push them using the stats= attribute on an item. --- engine/interfaces/bcp_api.cpp | 65 +++++++++++++++++++++++++++++++++++ engine/interfaces/bcp_api.hpp | 2 ++ 2 files changed, 67 insertions(+) diff --git a/engine/interfaces/bcp_api.cpp b/engine/interfaces/bcp_api.cpp index 1f189b9be6d..a6fb859b6cc 100644 --- a/engine/interfaces/bcp_api.cpp +++ b/engine/interfaces/bcp_api.cpp @@ -617,6 +617,42 @@ void parse_items( player_t* p, const player_spec_t& spec, const std::string& url { item.option_ilevel_str = util::to_string( slot_data[ "level" ][ "value" ].GetUint() ); } + // Use the item stats directly from the Armory - needed to get the proper stats from catalyzed items in 12.1. + // Armory JSON doesn't return any other indication of the source item (unlike redirected_base_stats from the addon) + else if ( slot_data.HasMember( "stats" ) ) + { + std::vector tokens; + for ( auto stat_idx = 0U, stat_end = slot_data[ "stats" ].Size(); stat_idx < stat_end; ++stat_idx ) + { + const auto& stat_data = slot_data[ "stats" ][ stat_idx ]; + + // Stat for another spec. Armory doesn't have AgiInt, it has Agi and Int and inactive ones are is_negated + if ( stat_data.HasMember( "is_negated" ) && stat_data[ "is_negated" ].GetBool() ) + { + continue; + } + + if ( !stat_data.HasMember( "type" ) || !stat_data[ "type" ].HasMember( "type" ) || + !stat_data.HasMember( "value" ) ) + { + continue; + } + + auto stat = bcp_api::translate_api_stat( stat_data[ "type" ][ "type" ].GetString() ); + if ( stat == STAT_NONE ) + { + continue; + } + + tokens.push_back( fmt::format( "{}{}", stat_data[ "value" ].GetInt(), util::stat_type_abbrev( stat ) ) ); + } + + if ( !tokens.empty() ) + { + item.option_stats_str = util::string_join( tokens, "_" ); + util::tolower( item.option_stats_str ); + } + } } } @@ -1093,6 +1129,35 @@ slot_e bcp_api::translate_api_slot( const std::string& slot_str ) return it->second; } +// bcp_api::translate_api_stat ============================================== + +stat_e bcp_api::translate_api_stat( util::string_view stat_str ) +{ + using record_t = std::pair; + static constexpr record_t stat_map[] = { + { "AGILITY", STAT_AGILITY }, + { "STRENGTH", STAT_STRENGTH }, + { "INTELLECT", STAT_INTELLECT }, + { "STAMINA", STAT_STAMINA }, + { "CRIT_RATING", STAT_CRIT_RATING }, + { "HASTE_RATING", STAT_HASTE_RATING }, + { "MASTERY_RATING", STAT_MASTERY_RATING }, + { "VERSATILITY", STAT_VERSATILITY_RATING }, + { "COMBAT_RATING_LIFESTEAL", STAT_LEECH_RATING }, + { "COMBAT_RATING_SPEED", STAT_SPEED_RATING }, + { "COMBAT_RATING_AVOIDANCE", STAT_AVOIDANCE_RATING } + }; + + auto it = range::find( stat_map, stat_str, &record_t::first ); + if ( it == range::end( stat_map ) ) + { + return STAT_NONE; + } + + return it->second; +} + + // bcp_api::download_player ================================================= player_t* bcp_api::download_player( sim_t* sim, const std::string& region, const std::string& server, diff --git a/engine/interfaces/bcp_api.hpp b/engine/interfaces/bcp_api.hpp index ab2bc0f5515..718706a03c5 100644 --- a/engine/interfaces/bcp_api.hpp +++ b/engine/interfaces/bcp_api.hpp @@ -8,6 +8,7 @@ #include "config.hpp" #include "sc_enums.hpp" #include "util/cache.hpp" +#include "util/string_view.hpp" #include #include @@ -33,5 +34,6 @@ void token_load(); void token_save(); slot_e translate_api_slot( const std::string& slot_str ); +stat_e translate_api_stat( util::string_view stat_str ); bool validate_api_key( const std::string& key ); } From 7fe4894aeed2b4cbee949db0bd5e9fb52a94453a Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Wed, 9 Sep 2026 14:48:31 -0700 Subject: [PATCH 2/2] [armory] Better comment on how the armory reports stats --- engine/interfaces/bcp_api.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/interfaces/bcp_api.cpp b/engine/interfaces/bcp_api.cpp index a6fb859b6cc..59cf23f3954 100644 --- a/engine/interfaces/bcp_api.cpp +++ b/engine/interfaces/bcp_api.cpp @@ -626,7 +626,8 @@ void parse_items( player_t* p, const player_spec_t& spec, const std::string& url { const auto& stat_data = slot_data[ "stats" ][ stat_idx ]; - // Stat for another spec. Armory doesn't have AgiInt, it has Agi and Int and inactive ones are is_negated + // Stat for another spec. Armory doesn't report the combined stats (StrAgiInt, AgiInt, etc). It uses the + // individual stats (Str, Agi, Int) uses is_negated to indicate which ones are inactive if ( stat_data.HasMember( "is_negated" ) && stat_data[ "is_negated" ].GetBool() ) { continue;