From 21d459ab2584739610ac8bf566d0209e64d210d2 Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sat, 23 May 2026 13:31:25 -0700 Subject: [PATCH 01/13] [Sim] use priority-sorted list of initializers for init_actor() --- engine/sc_enums.hpp | 19 ++++ engine/sim/sim.cpp | 240 +++++++++++++++++++++++--------------------- engine/sim/sim.hpp | 14 ++- 3 files changed, 157 insertions(+), 116 deletions(-) diff --git a/engine/sc_enums.hpp b/engine/sc_enums.hpp index 3fa6efd79eb..aaf70e467b7 100644 --- a/engine/sc_enums.hpp +++ b/engine/sc_enums.hpp @@ -1452,3 +1452,22 @@ enum proc_trigger_type_e : unsigned short TRIGGER_AURA_APPLIED, TRIGGER_HEARTBEAT, }; + +enum init_actor_e +{ + INIT_ACTOR_INIT = 100, + INIT_ACTOR_PROPERTIES = 200, + INIT_ACTOR_TALENTS = 300, + INIT_ACTOR_ITEMS = 400, + INIT_ACTOR_SPELLS = 500, + INIT_ACTOR_CREATE_EFFECTS = 600, + INIT_ACTOR_BASE_STATS = 700, + INIT_ACTOR_CREATE_BUFFS = 800, + INIT_ACTOR_CREATE_ACTIONS = 900, + INIT_ACTOR_PETS = 1000, + INIT_ACTOR_INIT_EFFECTS = 1100, + INIT_ACTOR_INIT_ACTIONS = 1200, + INIT_ACTOR_INITIAL_STATS = 1300, + INIT_ACTOR_MISC = 1400, + INIT_ACTOR_ASSESSORS = 1600, +}; diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index 6971f47fe09..cbdae3aff14 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -2485,11 +2485,118 @@ void sim_t::init_parties() /// Initialize actors void sim_t::init_actors() { - if ( debug ) + // add default initializers + print_debug( "Registering actor initializers." ); + + // initialize class/enemy modules + register_actor_initializer( 0, []( player_t* p ) { + for ( player_e i = PLAYER_NONE; i < PLAYER_MAX; ++i ) + if ( auto m = module_t::get( i ) ) + m->init( p ); + } ); + + if ( default_actions ) { - out_debug.printf( "Initializing actors." ); + register_actor_initializer( INIT_ACTOR_INIT - 10, [ this ]( player_t* p ) { + if ( !p->is_pet() ) + { + p->clear_action_priority_lists(); + p->action_list_str.clear(); + } + } ); } + register_actor_initializer( INIT_ACTOR_INIT, &player_t::init ); + register_actor_initializer( INIT_ACTOR_INIT + 10, []( player_t* p ) { + p->initialized = true; + } ); + + // Initialize player properties + register_actor_initializer( INIT_ACTOR_PROPERTIES + 5, &player_t::init_race ); + register_actor_initializer( INIT_ACTOR_PROPERTIES + 10, &player_t::init_position ); + register_actor_initializer( INIT_ACTOR_PROPERTIES + 15, &player_t::init_target ); + register_actor_initializer( INIT_ACTOR_PROPERTIES + 20, &player_t::init_professions ); + + // Initialize talents before items, as some item options like temporary enchant can utilize expressions + register_actor_initializer( INIT_ACTOR_TALENTS, &player_t::init_talents ); + + // Initialize each actor's items, construct gear information & stats + register_actor_initializer( INIT_ACTOR_ITEMS, &player_t::init_items ); + register_actor_initializer( INIT_ACTOR_ITEMS + 10, &player_t::init_azerite ); + + // Apply spec spell overrides to the DBC, must be done before spell lookups + register_actor_initializer( INIT_ACTOR_SPELLS - 10, &player_t::replace_spells ); + // Main spell looksup. Populate class/spec/hero talents & spells. + register_actor_initializer( INIT_ACTOR_SPELLS, &player_t::init_spells ); + + // First-phase creation of special effects from various sources. Needed to be able to create actions (APLs, really) + // based on the presence of special effects on items. Certain effects, such as effects that modify base stats, may be + // flagged to have their custom initialization run on creation. + register_actor_initializer( INIT_ACTOR_CREATE_EFFECTS, &player_t::create_special_effects ); + + // Initialize stats from DBC. Base stats can be modified until init_initial_stats() + register_actor_initializer( INIT_ACTOR_BASE_STATS, &player_t::init_base_stats ); + + // Buffs are created before actions, as typically action constructors tends to be more customized than buff + // constructors. This allow actions to reference buff validity during instantiation, but the vice versa is not + // possible. + register_actor_initializer( INIT_ACTOR_CREATE_BUFFS, &player_t::create_buffs ); + + // Background actions must be created before actions as some actions can reference them in their constructors + register_actor_initializer( INIT_ACTOR_CREATE_ACTIONS - 10, &player_t::init_background_actions ); + // Validate the actor and create all the action objects and set up action lists properly. If actor is not valid, set + // quiet and skip action creation. + register_actor_initializer( INIT_ACTOR_CREATE_ACTIONS, []( player_t* p ) { + if ( p->validate_actor() ) + p->create_actions(); +#ifdef NDEBUG + else + p->quiet = true; +#endif + } ); + // Create shared actions provided by modules + register_actor_initializer( INIT_ACTOR_CREATE_ACTIONS + 10, []( player_t* p ) { + for ( player_e i = PLAYER_NONE; i < PLAYER_MAX; ++i ) + if ( auto m = module_t::get( i ) ) + m->create_actions( p ); + } ); + + // Create all actor pets before special effects get initialized. This ensures that we can use stuff like the presence + // of an action (created with create_actions()) to determine if a pet needs to be created or not. Similarly, talent, + // spec, and item based qualifiers would work. + register_actor_initializer( INIT_ACTOR_PETS, &player_t::create_pets ); + // Create persistent actors from dynamic spawners + register_actor_initializer( INIT_ACTOR_PETS + 10, []( player_t* p ) { spawner::create_persistent_actors( *p ); } ); + + // Second-phase initialize all special effects and register them to actors + register_actor_initializer( INIT_ACTOR_INIT_EFFECTS, &player_t::init_special_effects ); + + // Finally, initialize all action objects + register_actor_initializer( INIT_ACTOR_INIT_ACTIONS, &player_t::init_actions ); + + // Once all transient properties are initialized (e.g., base stats, spells, special effects, items), initialize the + // initial stats of the actor. Do not modify base stats after this call. + register_actor_initializer( INIT_ACTOR_INITIAL_STATS, &player_t::init_initial_stats ); + // And once initial stats are initialized, derive the passive defensive properties of the actor + register_actor_initializer( INIT_ACTOR_INITIAL_STATS + 10, &player_t::init_defense ); + + register_actor_initializer( INIT_ACTOR_MISC + 5, &player_t::init_scaling ); + register_actor_initializer( INIT_ACTOR_MISC + 10, &player_t::init_gains ); + register_actor_initializer( INIT_ACTOR_MISC + 15, &player_t::init_procs ); + register_actor_initializer( INIT_ACTOR_MISC + 20, &player_t::init_uptimes ); + register_actor_initializer( INIT_ACTOR_MISC + 25, &player_t::init_benefits ); + register_actor_initializer( INIT_ACTOR_MISC + 30, &player_t::init_rng ); + register_actor_initializer( INIT_ACTOR_MISC + 35, &player_t::init_stats ); + register_actor_initializer( INIT_ACTOR_MISC + 40, &player_t::init_distance_targeting ); + register_actor_initializer( INIT_ACTOR_MISC + 45, &player_t::init_absorb_priority ); + + register_actor_initializer( INIT_ACTOR_ASSESSORS, &player_t::init_assessors ); + + // sort initializers by priority + range::sort( actor_initializer, []( const auto& a, const auto& b ) { return a.first < b.first; } ); + + print_debug( "Initializing actors." ); + for ( size_t i = 0; i < player_no_pet_list.size(); ++i ) { player_no_pet_list[ i ]->create_permanent_actors(); @@ -2505,8 +2612,7 @@ void sim_t::init_actors() init_actor( target_list[ i ] ); } - if ( debug ) - out_debug.printf( "Initializing Players." ); + print_debug( "Initializing Players." ); if ( decorated_tooltips == -1 ) decorated_tooltips = 1; @@ -2554,115 +2660,10 @@ void sim_t::init_actor( player_t* p ) { try { - // initialize class/enemy modules - for ( player_e i = PLAYER_NONE; i < PLAYER_MAX; ++i ) - { - const module_t* m = module_t::get( i ); - if ( m ) - m->init( p ); - } - - if ( default_actions && !p->is_pet() ) + for ( const auto& initializer : actor_initializer ) { - p->clear_action_priority_lists(); - p->action_list_str.clear(); + initializer.second( p ); } - - p->init(); - p->initialized = true; - - // This next section handles all the ugly details of initialization. Ideally, each of these - // init_* methods will eventually return a bool to indicate success or failure, from which - // we can either continue or halt initialization. - - p->init_target(); - - // Initialize player characteristics - p->init_race(); - p->init_talents(); - - p->replace_spells(); - p->init_position(); - p->init_professions(); - - // Initialize each actor's items, construct gear information & stats - p->init_items(); - - // Must be done after init_items (processes item options, so we know selected azerite powers in - // each item), and before init_spells (class modules "find_azerite_spell" in these). - p->init_azerite(); - - // Main spell looksup. Populate class/spec/hero talents & spells. - p->init_spells(); - - // First-phase creation of special effects from various sources. Needed to be able to create actions (APLs, really) - // based on the presence of special effects on items. Certain effects, such as effects that modify base stats, may - // be flagged to have their custom initialization run on creation. - p->create_special_effects(); - - // Initialize stats from DBC. Base stats can be modified until init_initial_stats(). - p->init_base_stats(); - - // Buffs are created before actions, as typically action constructors tends to be more customized than buff - // constructors. This allow actions to reference buff validity during instantiation, but the vice versa is not - // possible. - p->create_buffs(); - - // Currently this only holds leech_t. - p->init_background_actions(); - - // First, validate the actor and create all the action objects and set up action lists properly. - // If actor is not valid, set quiet and skip action creation. - if ( p->validate_actor() ) - { - p->create_actions(); - } -#ifdef NDBEBUG - else - { - quiet = true; - } - #endif - - // More initilization of class modules. Needed to create shared actions provided by a class. - for ( player_e i = PLAYER_NONE; i < PLAYER_MAX; ++i ) - { - const module_t* m = module_t::get( i ); - if ( m ) - m->create_actions( p ); - } - - // Create persistent actors from dynamic spawners - spawner::create_persistent_actors( *p ); - - // Create all actor pets before special effects get initialized. This ensures that we can use - // stuff like the presence of an action (created with create_actions()) to determine if a pet - // needs to be created or not. Similarly, talent, spec, and item based qualifiers would work. - p->create_pets(); - - // Second-phase initialize all special effects and register them to actors - p->init_special_effects(); - - // Finally, initialize all action objects - p->init_actions(); - - // Once all transient properties are initialized (e.g., base stats, spells, special effects, - // items), initialize the initial stats of the actor. Do not modify base stats after this call. - p->init_initial_stats(); - - // And once initial stats are initialized, derive the passive defensive properties of the actor. - p->init_defense(); - - p->init_scaling(); - p->init_gains(); - p->init_procs(); - p->init_uptimes(); - p->init_benefits(); - p->init_rng(); - p->init_stats(); - p->init_distance_targeting(); - p->init_absorb_priority(); - p->init_assessors(); } catch ( const std::exception& ) { @@ -4764,11 +4765,26 @@ void sim_t::heartbeat_event_callback() heartbeat_event_callback_function[ i ]( this ); } -void sim_t::register_heartbeat_event_callback(std::function fn) +void sim_t::register_heartbeat_event_callback( std::function fn ) { heartbeat_event_callback_function.emplace_back( std::move( fn ) ); } +void sim_t::register_target_data_initializer( std::function fn ) +{ + target_data_initializer.emplace_back( std::move( fn ) ); +} + +void sim_t::register_actor_initializer( int priority, std::function fn ) +{ + actor_initializer.emplace_back( priority, std::move( fn ) ); +} + +void sim_t::register_actor_initializer( int priority, void ( player_t::*fn)() ) +{ + actor_initializer.emplace_back( priority, [ fn ]( player_t* p ) { std::invoke( fn, p ); } ); +} + bool sim_t::rethrow_exception_queue() { if ( !exception_queue.empty() ) diff --git a/engine/sim/sim.hpp b/engine/sim/sim.hpp index cf834b3d3d7..72ac35f9e0c 100644 --- a/engine/sim/sim.hpp +++ b/engine/sim/sim.hpp @@ -626,7 +626,11 @@ struct sim_t : private sc_thread_t // List of callbacks to call when an actor_target_data_t object is created. Currently used to // initialize the generic targetdata debuffs/dots we have. - std::vector > target_data_initializer; + std::vector> target_data_initializer; + + // Priority-based actor initialization callbacks. Each callback is run on the player object during init_actor() in + // priority order, and additional callbacks can be inserted at any point from external modules. + std::vector>> actor_initializer; bool display_hotfixes, disable_hotfixes; bool display_bonus_ids; @@ -752,15 +756,17 @@ struct sim_t : private sc_thread_t void activate_actors(); void heartbeat_event_callback(); - std::vector> heartbeat_event_callback_function; + std::vector> heartbeat_event_callback_function; void register_heartbeat_event_callback( std::function fn ); + void register_target_data_initializer( std::function fn ); + void register_actor_initializer( int priority, std::function fn ); + void register_actor_initializer( int priority, void ( player_t::*fn )() ); + timespan_t current_time() const { return event_mgr.current_time; } static double distribution_mean_error( const sim_t& s, const extended_sample_data_t& sd ) { return s.confidence_estimator * sd.mean_std_dev; } - void register_target_data_initializer(std::function cb) - { target_data_initializer.push_back( cb ); } const rng::rng_t& rng() const { return _rng; } rng::rng_t& rng() From 13d60688bcff83fa3c6d1214c0505600f4e9d69c Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sat, 23 May 2026 14:54:21 -0700 Subject: [PATCH 02/13] add optional name to initializer entry --- engine/sim/sim.cpp | 19 ++++++++++++++----- engine/sim/sim.hpp | 4 ++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index cbdae3aff14..3771e3140fa 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -2593,7 +2593,9 @@ void sim_t::init_actors() register_actor_initializer( INIT_ACTOR_ASSESSORS, &player_t::init_assessors ); // sort initializers by priority - range::sort( actor_initializer, []( const auto& a, const auto& b ) { return a.first < b.first; } ); + range::sort( actor_initializer, []( const auto& a, const auto& b ) { + return std::get( a ) < std::get( b ); + } ); print_debug( "Initializing actors." ); @@ -2662,7 +2664,7 @@ void sim_t::init_actor( player_t* p ) { for ( const auto& initializer : actor_initializer ) { - initializer.second( p ); + std::get<1>( initializer )( p ); } } catch ( const std::exception& ) @@ -4775,14 +4777,21 @@ void sim_t::register_target_data_initializer( std::function fn ) +void sim_t::register_actor_initializer( int priority, std::function fn, std::string name ) { - actor_initializer.emplace_back( priority, std::move( fn ) ); + if ( !name.empty() && range::contains( actor_initializer, name, []( const auto& e ) { + return std::get( e ); + } ) ) + { + return; + } + + actor_initializer.emplace_back( priority, std::move( fn ), std::move( name ) ); } void sim_t::register_actor_initializer( int priority, void ( player_t::*fn)() ) { - actor_initializer.emplace_back( priority, [ fn ]( player_t* p ) { std::invoke( fn, p ); } ); + actor_initializer.emplace_back( priority, [ fn ]( player_t* p ) { std::invoke( fn, p ); }, "" ); } bool sim_t::rethrow_exception_queue() diff --git a/engine/sim/sim.hpp b/engine/sim/sim.hpp index 72ac35f9e0c..cdc9dcd2868 100644 --- a/engine/sim/sim.hpp +++ b/engine/sim/sim.hpp @@ -630,7 +630,7 @@ struct sim_t : private sc_thread_t // Priority-based actor initialization callbacks. Each callback is run on the player object during init_actor() in // priority order, and additional callbacks can be inserted at any point from external modules. - std::vector>> actor_initializer; + std::vector, std::string>> actor_initializer; bool display_hotfixes, disable_hotfixes; bool display_bonus_ids; @@ -760,7 +760,7 @@ struct sim_t : private sc_thread_t void register_heartbeat_event_callback( std::function fn ); void register_target_data_initializer( std::function fn ); - void register_actor_initializer( int priority, std::function fn ); + void register_actor_initializer( int priority, std::function fn, std::string name = "" ); void register_actor_initializer( int priority, void ( player_t::*fn )() ); timespan_t current_time() const From f37fa5f066a05d6da6b653dedd69c68ba8abae60 Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sat, 23 May 2026 17:28:22 -0700 Subject: [PATCH 03/13] register with an offset from another entry --- engine/sim/sim.cpp | 129 ++++++++++++++++++++++++++++----------------- engine/sim/sim.hpp | 9 +++- 2 files changed, 90 insertions(+), 48 deletions(-) diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index 3771e3140fa..a30cbd45830 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -2489,7 +2489,7 @@ void sim_t::init_actors() print_debug( "Registering actor initializers." ); // initialize class/enemy modules - register_actor_initializer( 0, []( player_t* p ) { + register_actor_initializer( 1, []( player_t* p ) { for ( player_e i = PLAYER_NONE; i < PLAYER_MAX; ++i ) if ( auto m = module_t::get( i ) ) m->init( p ); @@ -2506,44 +2506,40 @@ void sim_t::init_actors() } ); } - register_actor_initializer( INIT_ACTOR_INIT, &player_t::init ); - register_actor_initializer( INIT_ACTOR_INIT + 10, []( player_t* p ) { - p->initialized = true; - } ); + register_actor_initializer( INIT_ACTOR_INIT, &player_t::init, "init" ); + register_actor_initializer( "init", 10, []( player_t* p ) { p->initialized = true; } ); // Initialize player properties - register_actor_initializer( INIT_ACTOR_PROPERTIES + 5, &player_t::init_race ); - register_actor_initializer( INIT_ACTOR_PROPERTIES + 10, &player_t::init_position ); - register_actor_initializer( INIT_ACTOR_PROPERTIES + 15, &player_t::init_target ); - register_actor_initializer( INIT_ACTOR_PROPERTIES + 20, &player_t::init_professions ); + register_actor_initializer( INIT_ACTOR_PROPERTIES + 5, &player_t::init_race, "race" ); + register_actor_initializer( INIT_ACTOR_PROPERTIES + 10, &player_t::init_position, "position" ); + register_actor_initializer( INIT_ACTOR_PROPERTIES + 15, &player_t::init_target, "target" ); + register_actor_initializer( INIT_ACTOR_PROPERTIES + 20, &player_t::init_professions, "professions" ); // Initialize talents before items, as some item options like temporary enchant can utilize expressions - register_actor_initializer( INIT_ACTOR_TALENTS, &player_t::init_talents ); + register_actor_initializer( INIT_ACTOR_TALENTS, &player_t::init_talents, "talents" ); // Initialize each actor's items, construct gear information & stats - register_actor_initializer( INIT_ACTOR_ITEMS, &player_t::init_items ); - register_actor_initializer( INIT_ACTOR_ITEMS + 10, &player_t::init_azerite ); + register_actor_initializer( INIT_ACTOR_ITEMS, &player_t::init_items, "items" ); + register_actor_initializer( "items", 10, &player_t::init_azerite ); - // Apply spec spell overrides to the DBC, must be done before spell lookups - register_actor_initializer( INIT_ACTOR_SPELLS - 10, &player_t::replace_spells ); // Main spell looksup. Populate class/spec/hero talents & spells. - register_actor_initializer( INIT_ACTOR_SPELLS, &player_t::init_spells ); + register_actor_initializer( INIT_ACTOR_SPELLS, &player_t::init_spells, "spells" ); + // Apply spec spell overrides to the DBC, must be done before spell lookups + register_actor_initializer( "spells", -10, &player_t::replace_spells ); // First-phase creation of special effects from various sources. Needed to be able to create actions (APLs, really) // based on the presence of special effects on items. Certain effects, such as effects that modify base stats, may be // flagged to have their custom initialization run on creation. - register_actor_initializer( INIT_ACTOR_CREATE_EFFECTS, &player_t::create_special_effects ); + register_actor_initializer( INIT_ACTOR_CREATE_EFFECTS, &player_t::create_special_effects, "create_effects" ); // Initialize stats from DBC. Base stats can be modified until init_initial_stats() - register_actor_initializer( INIT_ACTOR_BASE_STATS, &player_t::init_base_stats ); + register_actor_initializer( INIT_ACTOR_BASE_STATS, &player_t::init_base_stats, "base_stats" ); // Buffs are created before actions, as typically action constructors tends to be more customized than buff // constructors. This allow actions to reference buff validity during instantiation, but the vice versa is not // possible. - register_actor_initializer( INIT_ACTOR_CREATE_BUFFS, &player_t::create_buffs ); + register_actor_initializer( INIT_ACTOR_CREATE_BUFFS, &player_t::create_buffs, "create_buffs" ); - // Background actions must be created before actions as some actions can reference them in their constructors - register_actor_initializer( INIT_ACTOR_CREATE_ACTIONS - 10, &player_t::init_background_actions ); // Validate the actor and create all the action objects and set up action lists properly. If actor is not valid, set // quiet and skip action creation. register_actor_initializer( INIT_ACTOR_CREATE_ACTIONS, []( player_t* p ) { @@ -2553,44 +2549,46 @@ void sim_t::init_actors() else p->quiet = true; #endif - } ); + }, "create_actions" ); // Create shared actions provided by modules - register_actor_initializer( INIT_ACTOR_CREATE_ACTIONS + 10, []( player_t* p ) { + register_actor_initializer( "create_actions", 10, []( player_t* p ) { for ( player_e i = PLAYER_NONE; i < PLAYER_MAX; ++i ) if ( auto m = module_t::get( i ) ) m->create_actions( p ); } ); + // Background actions must be created before actions as some actions can reference them in their constructors + register_actor_initializer( "create_actions", -10, &player_t::init_background_actions ); // Create all actor pets before special effects get initialized. This ensures that we can use stuff like the presence // of an action (created with create_actions()) to determine if a pet needs to be created or not. Similarly, talent, // spec, and item based qualifiers would work. - register_actor_initializer( INIT_ACTOR_PETS, &player_t::create_pets ); + register_actor_initializer( INIT_ACTOR_PETS, &player_t::create_pets, "pets" ); // Create persistent actors from dynamic spawners - register_actor_initializer( INIT_ACTOR_PETS + 10, []( player_t* p ) { spawner::create_persistent_actors( *p ); } ); + register_actor_initializer( "pets", 10, []( player_t* p ) { spawner::create_persistent_actors( *p ); } ); // Second-phase initialize all special effects and register them to actors - register_actor_initializer( INIT_ACTOR_INIT_EFFECTS, &player_t::init_special_effects ); + register_actor_initializer( INIT_ACTOR_INIT_EFFECTS, &player_t::init_special_effects, "init_effects" ); // Finally, initialize all action objects - register_actor_initializer( INIT_ACTOR_INIT_ACTIONS, &player_t::init_actions ); + register_actor_initializer( INIT_ACTOR_INIT_ACTIONS, &player_t::init_actions, "init_actions" ); // Once all transient properties are initialized (e.g., base stats, spells, special effects, items), initialize the // initial stats of the actor. Do not modify base stats after this call. - register_actor_initializer( INIT_ACTOR_INITIAL_STATS, &player_t::init_initial_stats ); + register_actor_initializer( INIT_ACTOR_INITIAL_STATS, &player_t::init_initial_stats, "initial_stats" ); // And once initial stats are initialized, derive the passive defensive properties of the actor - register_actor_initializer( INIT_ACTOR_INITIAL_STATS + 10, &player_t::init_defense ); + register_actor_initializer( "initial_stats", 10, &player_t::init_defense ); - register_actor_initializer( INIT_ACTOR_MISC + 5, &player_t::init_scaling ); - register_actor_initializer( INIT_ACTOR_MISC + 10, &player_t::init_gains ); - register_actor_initializer( INIT_ACTOR_MISC + 15, &player_t::init_procs ); - register_actor_initializer( INIT_ACTOR_MISC + 20, &player_t::init_uptimes ); - register_actor_initializer( INIT_ACTOR_MISC + 25, &player_t::init_benefits ); - register_actor_initializer( INIT_ACTOR_MISC + 30, &player_t::init_rng ); - register_actor_initializer( INIT_ACTOR_MISC + 35, &player_t::init_stats ); - register_actor_initializer( INIT_ACTOR_MISC + 40, &player_t::init_distance_targeting ); - register_actor_initializer( INIT_ACTOR_MISC + 45, &player_t::init_absorb_priority ); + register_actor_initializer( INIT_ACTOR_MISC + 5, &player_t::init_scaling, "scaling" ); + register_actor_initializer( INIT_ACTOR_MISC + 10, &player_t::init_gains, "gains" ); + register_actor_initializer( INIT_ACTOR_MISC + 15, &player_t::init_procs, "procs" ); + register_actor_initializer( INIT_ACTOR_MISC + 20, &player_t::init_uptimes, "uptimes" ); + register_actor_initializer( INIT_ACTOR_MISC + 25, &player_t::init_benefits, "benefits" ); + register_actor_initializer( INIT_ACTOR_MISC + 30, &player_t::init_rng, "rng" ); + register_actor_initializer( INIT_ACTOR_MISC + 35, &player_t::init_stats, "stats" ); + register_actor_initializer( INIT_ACTOR_MISC + 40, &player_t::init_distance_targeting, "distance_targeting" ); + register_actor_initializer( INIT_ACTOR_MISC + 45, &player_t::init_absorb_priority, "absorb_priority" ); - register_actor_initializer( INIT_ACTOR_ASSESSORS, &player_t::init_assessors ); + register_actor_initializer( INIT_ACTOR_ASSESSORS, &player_t::init_assessors, "assessors" ); // sort initializers by priority range::sort( actor_initializer, []( const auto& a, const auto& b ) { @@ -4777,21 +4775,58 @@ void sim_t::register_target_data_initializer( std::function( e ) == name; + } ); + + if ( it != actor_initializer.end() ) + return std::get( *it ); + + return 0; +} + +void sim_t::register_actor_initializer( int priority, void ( player_t::*fn)(), std::string name ) +{ + if ( priority == 0 ) + throw sc_initialization_error( fmt::format( "Actor initializer '{}' priority cannot be 0.", name ) ); + + if ( get_actor_initializer_priority( name ) == 0 ) + actor_initializer.emplace_back( priority, [ fn ]( player_t* p ) { std::invoke( fn, p ); }, std::move( name ) ); +} + void sim_t::register_actor_initializer( int priority, std::function fn, std::string name ) { - if ( !name.empty() && range::contains( actor_initializer, name, []( const auto& e ) { - return std::get( e ); - } ) ) - { - return; - } + if ( priority == 0 ) + throw sc_initialization_error( fmt::format( "Actor initializer '{}' priority cannot be 0.", name ) ); - actor_initializer.emplace_back( priority, std::move( fn ), std::move( name ) ); + if ( get_actor_initializer_priority( name ) == 0 ) + actor_initializer.emplace_back( priority, std::move( fn ), std::move( name ) ); } -void sim_t::register_actor_initializer( int priority, void ( player_t::*fn)() ) +void sim_t::register_actor_initializer( std::string_view base, int offset, void ( player_t::*fn )(), std::string name ) { - actor_initializer.emplace_back( priority, [ fn ]( player_t* p ) { std::invoke( fn, p ); }, "" ); + auto priority = get_actor_initializer_priority( base ); + + if ( priority == 0 ) + throw sc_initialization_error( fmt::format( "Actor initializer '{}' not found as base for '{}'.", base, name ) ); + + register_actor_initializer( priority + offset, fn, std::move( name ) ); +} + +void sim_t::register_actor_initializer( std::string_view base, int offset, std::function fn, + std::string name ) +{ + auto priority = get_actor_initializer_priority( base ); + + if ( priority == 0 ) + throw sc_initialization_error( fmt::format( "Actor initializer '{}' not found as base for '{}'.", base, name ) ); + + register_actor_initializer( priority + offset, std::move( fn ), std::move( name ) ); } bool sim_t::rethrow_exception_queue() diff --git a/engine/sim/sim.hpp b/engine/sim/sim.hpp index cdc9dcd2868..628986c1f34 100644 --- a/engine/sim/sim.hpp +++ b/engine/sim/sim.hpp @@ -760,8 +760,15 @@ struct sim_t : private sc_thread_t void register_heartbeat_event_callback( std::function fn ); void register_target_data_initializer( std::function fn ); + + // Check if named initializer exists. Return 0 if not found or name is empty. + int get_actor_initializer_priority( std::string_view name ) const; + void register_actor_initializer( int priority, void ( player_t::*fn )(), std::string name = "" ); void register_actor_initializer( int priority, std::function fn, std::string name = "" ); - void register_actor_initializer( int priority, void ( player_t::*fn )() ); + // Register with an offset from another named initializer + void register_actor_initializer( std::string_view base, int offset, void ( player_t::*fn )(), std::string name = "" ); + void register_actor_initializer( std::string_view base, int offset, std::function fn, + std::string name = "" ); timespan_t current_time() const { return event_mgr.current_time; } From 0bbe2283918aa2fb71c86f55bc24501c071935dc Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sat, 23 May 2026 17:53:00 -0700 Subject: [PATCH 04/13] rename player_t::init_stats to player_t::init_stat_data --- engine/class_modules/sc_druid.cpp | 6 +++--- engine/class_modules/sc_enemy.cpp | 14 ++++++-------- engine/player/player.cpp | 4 ++-- engine/player/player.hpp | 2 +- engine/sim/sim.cpp | 2 +- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 4460559d459..39465d2ab9f 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -1322,7 +1322,7 @@ struct druid_t final : public parse_player_effects_t parsed_assisted_combat_rule_t parse_assisted_combat_rule( const assisted_combat_rule_data_t&, const assisted_combat_step_data_t& ) const override; void init_base_stats() override; - void init_stats() override; + void init_initial_stats() override; void init_rng() override; void init_gains() override; void init_procs() override; @@ -10711,9 +10711,9 @@ void druid_t::init_base_stats() ready_type = ready_e::READY_TRIGGER; } -void druid_t::init_stats() +void druid_t::init_initial_stats() { - player_t::init_stats(); + player_t::init_initial_stats(); // enable CP & energy for cat form if ( uses_cat_form() ) diff --git a/engine/class_modules/sc_enemy.cpp b/engine/class_modules/sc_enemy.cpp index 9de1211cc80..48b8e2fee3d 100644 --- a/engine/class_modules/sc_enemy.cpp +++ b/engine/class_modules/sc_enemy.cpp @@ -79,7 +79,7 @@ struct enemy_t : public player_t virtual std::string generate_action_list(); virtual void generate_heal_raid_event(); void init_action_list() override; - void init_stats() override; + void init_actions() override; double resource_loss( resource_e, double, gain_t*, action_t* ) override; void create_options() override; pet_t* create_pet( util::string_view add_name, util::string_view pet_type = {} ) override; @@ -1293,9 +1293,7 @@ void enemy_t::init_base_stats() true_level = sim->max_player_level + 3; // waiting_time override - waiting_time = timespan_t::from_seconds( 5.0 ); - if ( waiting_time < timespan_t::from_seconds( 1.0 ) ) - waiting_time = timespan_t::from_seconds( 1.0 ); + waiting_time = 5_s; base.attack_crit_chance = 0.05; @@ -1645,13 +1643,13 @@ void enemy_t::init_action_list() action_list_str = new_action_list_str; } } + player_t::init_action_list(); } -// Hack to get this executed after player_t::init_action_list. -void enemy_t::init_stats() +void enemy_t::init_actions() { - player_t::init_stats(); + player_t::init_actions(); // Small hack to increase waiting time for target without any actions for ( size_t i = 0; i < action_list.size(); ++i ) @@ -1663,7 +1661,7 @@ void enemy_t::init_stats() continue; if ( action->name_str.find( "auto_attack" ) != std::string::npos ) continue; - waiting_time = timespan_t::from_seconds( 1.0 ); + waiting_time = 1_s; break; } } diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 9ab1fc9e5dd..a912f4b52b3 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -3476,9 +3476,9 @@ void player_t::init_rng() sim->print_debug( "Initializing random number generators for {}.", *this ); } -void player_t::init_stats() +void player_t::init_stat_data() { - sim->print_debug( "Initializing stats for {}.", *this ); + sim->print_debug( "Initializing stat data for {}.", *this ); if ( sim->maximize_reporting ) { diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 4dd803e7d2c..6c853ec7dd1 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -1217,7 +1217,7 @@ struct player_t : public actor_t virtual void init_uptimes(); virtual void init_benefits(); virtual void init_rng(); - virtual void init_stats(); + virtual void init_stat_data(); virtual void init_distance_targeting(); virtual void init_absorb_priority(); virtual void init_assessors(); diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index a30cbd45830..c4115a702c7 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -2584,7 +2584,7 @@ void sim_t::init_actors() register_actor_initializer( INIT_ACTOR_MISC + 20, &player_t::init_uptimes, "uptimes" ); register_actor_initializer( INIT_ACTOR_MISC + 25, &player_t::init_benefits, "benefits" ); register_actor_initializer( INIT_ACTOR_MISC + 30, &player_t::init_rng, "rng" ); - register_actor_initializer( INIT_ACTOR_MISC + 35, &player_t::init_stats, "stats" ); + register_actor_initializer( INIT_ACTOR_MISC + 35, &player_t::init_stat_data, "stat_data" ); register_actor_initializer( INIT_ACTOR_MISC + 40, &player_t::init_distance_targeting, "distance_targeting" ); register_actor_initializer( INIT_ACTOR_MISC + 45, &player_t::init_absorb_priority, "absorb_priority" ); From 2ac6ef81b2ec6aac0614cf0707560a5f8a0b9c4c Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sat, 23 May 2026 19:30:52 -0700 Subject: [PATCH 05/13] move raid events related actor initialization code into the raid event --- engine/player/player.cpp | 31 +++---------------------------- engine/sim/raid_event.cpp | 28 ++++++++++++++++++++++++++++ engine/sim/sim.cpp | 26 ++++++++++++++++++++------ 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/engine/player/player.cpp b/engine/player/player.cpp index a912f4b52b3..2957c9721dd 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -58,7 +58,6 @@ #include "sim/plot.hpp" #include "sim/proc.hpp" #include "sim/proc_rng.hpp" -#include "sim/raid_event.hpp" #include "sim/scale_factor_control.hpp" #include "sim/sim.hpp" #include "util/io.hpp" @@ -3545,13 +3544,11 @@ void player_t::init_scaling() scaling->enable( STAT_MASTERY_RATING ); scaling->enable( STAT_VERSATILITY_RATING ); - scaling->set( STAT_SPEED_RATING, sim->has_raid_event( "movement" ) ); - // scaling -> set( STAT_AVOIDANCE_RATING ] = tank; // Waste of sim time vast majority of the time. Can be - // enabled manually. - scaling->set( STAT_LEECH_RATING, tank ); + // scaling->enable( STAT_SPEED_RATING ); // handled in raid_events movement_event_t + // scaling->set( STAT_AVOIDANCE_RATING, tank ); // can be enabled manually if need be + scaling->set( STAT_LEECH_RATING, heal ); scaling->set( STAT_WEAPON_DPS, attack ); - scaling->set( STAT_ARMOR, tank ); auto add_stat = []( double& to, double value, double lower_limit ) @@ -4487,21 +4484,6 @@ void player_t::init_assessors() return assessor::CONTINUE; } ); - // Credit absorbed-on-enemy damage back to attacker stats when an absorb raid event is configured. - if ( !is_enemy() && range::any_of( sim->raid_events, - []( const auto& e ) { return e->type == "absorb"; } ) ) - { - assessor_out_damage.add( assessor::TARGET_DAMAGE + 1, []( result_amount_type, action_state_t* s ) { - if ( s->target && s->target->is_enemy() ) - { - double absorbed = s->result_mitigated - s->result_absorbed; - if ( absorbed > 0 ) - s->result_amount += absorbed; - } - return assessor::CONTINUE; - } ); - } - // Logging and debug .. Technically, this should probably be in action_t::assess_damage, but we // don't need this piece of code for the vast majority of sims, so it makes sense to yank it out // completely from there, and only conditionally include it if logging/debugging is enabled. @@ -5124,13 +5106,6 @@ void player_t::create_buffs() debuffs.damage_taken = make_buff( this, "damage_taken" ) ->set_duration( timespan_t::from_seconds( 20.0 ) ) ->set_max_stack( 999 ); - - if ( sim->has_raid_event( "damage_done" ) ) - { - buffs.damage_done = make_buff( this, "damage_done" ) - ->set_max_stack( 1 ) - ->add_invalidate( CACHE_PLAYER_DAMAGE_MULTIPLIER ); - } } item_t* player_t::find_item_by_name( util::string_view item_name ) diff --git a/engine/sim/raid_event.cpp b/engine/sim/raid_event.cpp index 41c8bad99af..9534ae8e144 100644 --- a/engine/sim/raid_event.cpp +++ b/engine/sim/raid_event.cpp @@ -12,6 +12,7 @@ #include "player/pet.hpp" #include "player/player.hpp" #include "player/player_demise_event.hpp" +#include "player/player_scaling.hpp" #include "player/pet_spawner.hpp" #include "raid_event.hpp" #include "sim/event.hpp" @@ -1210,6 +1211,11 @@ struct movement_event_t final : public raid_event_t { direction = util::parse_movement_direction( move_direction ); } + + sim->register_actor_initializer( "scaling", 1, []( player_t* p ) { + if ( !p->is_pet() && !p->is_enemy() ) + p->scaling->enable( STAT_SPEED_RATING ); + }, "scaling_speed_rating" ); } bool parse_target( std::string_view value ) @@ -1531,6 +1537,12 @@ struct damage_done_buff_event_t final : public raid_event_t { add_option( opt_float( "multiplier", multiplier ) ); parse_options( options_str ); + + sim->register_actor_initializer( "create_buffs", 1, []( player_t* p ) { + p->buffs.damage_done = make_buff( p, "damage_done" ) + ->set_max_stack( 1 ) + ->add_invalidate( CACHE_PLAYER_DAMAGE_MULTIPLIER ); + }, "create_buffs_damage_done" ); } void _start() override @@ -1696,6 +1708,22 @@ struct absorb_event_t final : public raid_event_t if ( school == SCHOOL_NONE ) throw std::invalid_argument( fmt::format( "Unknown absorb raid event school '{}'", school_str ) ); } + + // Credit absorbed-on-enemy damage back to attacker stats + sim->register_actor_initializer( INIT_ACTOR_ASSESSORS + 10, []( player_t* p ) { + if ( p->is_enemy() ) + return; + + p->assessor_out_damage.add( assessor::TARGET_DAMAGE + 1, []( auto, action_state_t* s ) { + if ( s->target && s->target->is_enemy() ) + { + if ( auto absorbed = s->result_mitigated - s->result_absorbed; absorbed > 0 ) + s->result_amount += absorbed; + } + + return assessor::CONTINUE; + } ); + }, "assessors_absorb_event" ); } player_t* resolve_target() const diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index c4115a702c7..69471ea905e 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -4792,41 +4792,55 @@ int sim_t::get_actor_initializer_priority( std::string_view name ) const void sim_t::register_actor_initializer( int priority, void ( player_t::*fn)(), std::string name ) { + if ( get_actor_initializer_priority( name ) != 0 ) + return; + if ( priority == 0 ) throw sc_initialization_error( fmt::format( "Actor initializer '{}' priority cannot be 0.", name ) ); - if ( get_actor_initializer_priority( name ) == 0 ) - actor_initializer.emplace_back( priority, [ fn ]( player_t* p ) { std::invoke( fn, p ); }, std::move( name ) ); + actor_initializer.emplace_back( priority, [ fn ]( player_t* p ) { + std::invoke( fn, p ); + }, std::move( name ) ); } void sim_t::register_actor_initializer( int priority, std::function fn, std::string name ) { + if ( get_actor_initializer_priority( name ) != 0 ) + return; + if ( priority == 0 ) throw sc_initialization_error( fmt::format( "Actor initializer '{}' priority cannot be 0.", name ) ); - if ( get_actor_initializer_priority( name ) == 0 ) - actor_initializer.emplace_back( priority, std::move( fn ), std::move( name ) ); + actor_initializer.emplace_back( priority, std::move( fn ), std::move( name ) ); } void sim_t::register_actor_initializer( std::string_view base, int offset, void ( player_t::*fn )(), std::string name ) { + if ( get_actor_initializer_priority( name ) != 0 ) + return; + auto priority = get_actor_initializer_priority( base ); if ( priority == 0 ) throw sc_initialization_error( fmt::format( "Actor initializer '{}' not found as base for '{}'.", base, name ) ); - register_actor_initializer( priority + offset, fn, std::move( name ) ); + actor_initializer.emplace_back( priority + offset, [ fn ]( player_t* p ) { + std::invoke( fn, p ); + }, std::move( name ) ); } void sim_t::register_actor_initializer( std::string_view base, int offset, std::function fn, std::string name ) { + if ( get_actor_initializer_priority( name ) != 0 ) + return; + auto priority = get_actor_initializer_priority( base ); if ( priority == 0 ) throw sc_initialization_error( fmt::format( "Actor initializer '{}' not found as base for '{}'.", base, name ) ); - register_actor_initializer( priority + offset, std::move( fn ), std::move( name ) ); + actor_initializer.emplace_back( priority + offset, std::move( fn ), std::move( name ) ); } bool sim_t::rethrow_exception_queue() From fed44acb6ef71eb82c844155384ed0300c8c2dde Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sat, 23 May 2026 20:02:14 -0700 Subject: [PATCH 06/13] use index loop for running initializers --- engine/sim/sim.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index 69471ea905e..5a6690f3fdd 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -2654,15 +2654,14 @@ void sim_t::init_actors() // sim_t::init_actor ======================================================== -// This method handles the bulk of player initialization. Order is pretty -// critical here. Called in sim_t::init() void sim_t::init_actor( player_t* p ) { try { - for ( const auto& initializer : actor_initializer ) + // Use index loop to allow initializers to add more initializers + for ( size_t i = 0; i < actor_initializer.size(); ++i ) { - std::get<1>( initializer )( p ); + std::get<1>( actor_initializer[ i ] )( p ); } } catch ( const std::exception& ) From 50c172677625d1e3e146f8f3ea4f1d3180efc86f Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sat, 23 May 2026 22:13:46 -0700 Subject: [PATCH 07/13] move emerald_coachs_whistle_ally_ilvl code to unique_gear_dragonflight --- engine/player/player.cpp | 34 ------------------- engine/player/unique_gear.cpp | 5 +++ engine/player/unique_gear.hpp | 2 ++ engine/player/unique_gear_dragonflight.cpp | 39 ++++++++++++++++++++++ engine/player/unique_gear_dragonflight.hpp | 1 + engine/sim/sim.cpp | 1 + 6 files changed, 48 insertions(+), 34 deletions(-) diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 2957c9721dd..6f5f70ae340 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -2203,40 +2203,6 @@ void player_t::create_special_effects() } } - if ( dragonflight_opts.emerald_coachs_whistle_ally_ilvl > 0 ) - { - struct emerald_coachs_whistle_ally_t : public special_effect_t - { - std::unique_ptr _item; - - emerald_coachs_whistle_ally_t( player_t* p ) : special_effect_t( p ) - { - // make a fake - _item = std::make_unique( - p, fmt::format( ",id=193718,ilevel={}", p->dragonflight_opts.emerald_coachs_whistle_ally_ilvl ) ); - _item->parse_options(); - _item->initialize_data(); - _item->init(); - - // validate data - auto it = range::find( _item->parsed.data.effects, ITEM_SPELLTRIGGER_ON_EQUIP, &item_effect_t::type ); - if ( it == _item->parsed.data.effects.end() ) - { - throw sc_invalid_player_argument( - "Cannot find on-equip effect on item id=193718 for 'dragonflight.emerald_coachs_whistle_ally_ilvl'." ); - } - - spell_id = p->dragonflight_opts.emerald_coachs_whistle_ally_is_healer ? 386578 : it->spell_id; - name_str = "emerald_coachs_whistle_ally"; - item = _item.get(); - - unique_gear::initialize_special_effect( *this, spell_id ); - } - }; - - special_effects.push_back( new emerald_coachs_whistle_ally_t( this ) ); - } - unique_gear::initialize_racial_effects( this ); if ( sim->overrides.skyfury && may_benefit_from_skyfury() ) diff --git a/engine/player/unique_gear.cpp b/engine/player/unique_gear.cpp index 2bf28b80392..a06bb0965bf 100644 --- a/engine/player/unique_gear.cpp +++ b/engine/player/unique_gear.cpp @@ -4880,6 +4880,11 @@ void unique_gear::register_target_data_initializers( sim_t* sim ) midnight::register_target_data_initializers( *sim ); } +void unique_gear::register_actor_initializers( sim_t& sim ) +{ + dragonflight::register_actor_initializers( sim ); +} + std::vector unique_gear::find_special_effects( player_t* p, unsigned id, special_effect_e type ) { std::vector effects; diff --git a/engine/player/unique_gear.hpp b/engine/player/unique_gear.hpp index 6540ed34c17..ae805da8668 100644 --- a/engine/player/unique_gear.hpp +++ b/engine/player/unique_gear.hpp @@ -70,6 +70,8 @@ void register_target_data_initializers( sim_t* ); void register_target_data_initializers_legion( sim_t* ); // Legion targetdata initializers void register_target_data_initializers_bfa( sim_t* ); // Battle for Azeroth targetdata initializers +void register_actor_initializers( sim_t& ); + void init( player_t* ); std::vector find_special_effects( player_t*, unsigned, special_effect_e = SPECIAL_EFFECT_NONE ); diff --git a/engine/player/unique_gear_dragonflight.cpp b/engine/player/unique_gear_dragonflight.cpp index 43858c707e8..8e22fbdb1fd 100644 --- a/engine/player/unique_gear_dragonflight.cpp +++ b/engine/player/unique_gear_dragonflight.cpp @@ -11736,6 +11736,45 @@ void register_hotfixes() { } +void register_actor_initializers( sim_t& sim ) +{ + sim.register_actor_initializer( INIT_ACTOR_CREATE_EFFECTS + 10, []( player_t* p ) { + if ( p->dragonflight_opts.emerald_coachs_whistle_ally_ilvl > 0 ) + { + struct emerald_coachs_whistle_ally_t : public special_effect_t + { + std::unique_ptr _item; + + emerald_coachs_whistle_ally_t( player_t* p ) : special_effect_t( p ) + { + // make a fake + _item = std::make_unique( + p, fmt::format( ",id=193718,ilevel={}", p->dragonflight_opts.emerald_coachs_whistle_ally_ilvl ) ); + _item->parse_options(); + _item->initialize_data(); + _item->init(); + + // validate data + auto it = range::find( _item->parsed.data.effects, ITEM_SPELLTRIGGER_ON_EQUIP, &item_effect_t::type ); + if ( it == _item->parsed.data.effects.end() ) + { + throw sc_invalid_player_argument( + "Cannot find on-equip effect on item id=193718 for 'dragonflight.emerald_coachs_whistle_ally_ilvl'." ); + } + + spell_id = p->dragonflight_opts.emerald_coachs_whistle_ally_is_healer ? 386578 : it->spell_id; + name_str = "emerald_coachs_whistle_ally"; + item = _item.get(); + + custom_init = &items::emerald_coachs_whistle; + } + }; + + p->special_effects.push_back( new emerald_coachs_whistle_ally_t( p ) ); + } + }, "create_buffs_dragonflight" ); +} + // check and return multiplier for toxified armor patch // TODO: spell data seems to indicate you can have up to 4 stacks. currently implemented as a simple check double toxified_mul( player_t* player ) diff --git a/engine/player/unique_gear_dragonflight.hpp b/engine/player/unique_gear_dragonflight.hpp index eb4dee78980..8689e7f038f 100644 --- a/engine/player/unique_gear_dragonflight.hpp +++ b/engine/player/unique_gear_dragonflight.hpp @@ -94,6 +94,7 @@ void raging_tempests( special_effect_t& ); void register_special_effects(); void register_target_data_initializers( sim_t& ); +void register_actor_initializers( sim_t& ); void register_hotfixes(); double toxified_mul( player_t* ); double inhibitor_mul( player_t* ); diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index 5a6690f3fdd..256e988eb6e 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -2710,6 +2710,7 @@ void sim_t::init() event_mgr.init(); unique_gear::register_target_data_initializers( this ); + unique_gear::register_actor_initializers( *this ); // Seed RNG if ( seed == 0 ) From f64c93ab136e11e8dafe5ff1889abe03fa9e0635 Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sun, 24 May 2026 08:38:37 -0700 Subject: [PATCH 08/13] move external soleahs_secret_technique code to unique_gear_shadowlands --- engine/player/player.cpp | 19 ------ engine/player/player.hpp | 1 - engine/player/unique_gear.cpp | 1 + engine/player/unique_gear.hpp | 5 ++ engine/player/unique_gear_dragonflight.cpp | 69 +++++++--------------- engine/player/unique_gear_helper.cpp | 27 +++++++++ engine/player/unique_gear_helper.hpp | 12 ++++ engine/player/unique_gear_midnight.cpp | 4 -- engine/player/unique_gear_shadowlands.cpp | 61 ++++++++++++------- engine/player/unique_gear_shadowlands.hpp | 1 + engine/player/unique_gear_thewarwithin.cpp | 4 -- 11 files changed, 105 insertions(+), 99 deletions(-) diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 6f5f70ae340..925a197fe57 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -4936,22 +4936,6 @@ void player_t::create_buffs() ->add_invalidate( CACHE_HASTE ); // External trinkets - if ( external_buffs.soleahs_secret_technique ) - { - // TODO: confirm what happens if ratings are the same. For now assuming it follows same priority as IQD. - static constexpr std::array ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, - STAT_HASTE_RATING, STAT_CRIT_RATING }; - - auto ilevel = external_buffs.soleahs_secret_technique; - auto coeff = find_spell( 368513 )->effectN( 2 ).m_coefficient(); - auto points = dbc->random_property( ilevel ).p_epic[ 0 ]; - auto mult = dbc->combat_rating_multiplier( ilevel, CR_MULTIPLIER_TRINKET ); - - buffs.soleahs_secret_technique_external = - make_buff( this, "soleahs_secret_technique_external", find_spell( 368510 ) ) - ->add_stat( util::highest_stat( this, ratings ), coeff * points * mult ); - } - if ( !external_buffs.elegy_of_the_eternals.empty() ) { buffs.elegy_of_the_eternals_external = @@ -7303,9 +7287,6 @@ void player_t::arise() if ( buffs.focus_magic && external_buffs.focus_magic ) buffs.focus_magic->override_buff(); - if ( buffs.soleahs_secret_technique_external ) - buffs.soleahs_secret_technique_external->trigger(); - if ( buffs.elegy_of_the_eternals_external ) buffs.elegy_of_the_eternals_external->trigger(); diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 6c853ec7dd1..1a52e4b8a59 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -552,7 +552,6 @@ struct player_t : public actor_t buff_t* echo_of_eonar; // passive self buff // Trinkets - buff_t* soleahs_secret_technique_external; buff_t* elegy_of_the_eternals_external; // 9.2 Sepulcher of the First Ones diff --git a/engine/player/unique_gear.cpp b/engine/player/unique_gear.cpp index a06bb0965bf..6c64a25d0f4 100644 --- a/engine/player/unique_gear.cpp +++ b/engine/player/unique_gear.cpp @@ -4882,6 +4882,7 @@ void unique_gear::register_target_data_initializers( sim_t* sim ) void unique_gear::register_actor_initializers( sim_t& sim ) { + shadowlands::register_actor_initializers( sim ); dragonflight::register_actor_initializers( sim ); } diff --git a/engine/player/unique_gear.hpp b/engine/player/unique_gear.hpp index ae805da8668..2617f93de3e 100644 --- a/engine/player/unique_gear.hpp +++ b/engine/player/unique_gear.hpp @@ -101,4 +101,9 @@ const spell_data_t* spell_from_spell_text( const special_effect_t& ); std::vector equipped_gem_list( player_t*, util::span ); std::vector unique_gem_list( player_t*, util::span ); + +// assuming priority for highest/lowest secondary is vers > mastery > haste > crit +static constexpr std::array secondary_ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, + STAT_HASTE_RATING, STAT_CRIT_RATING }; + } // namespace unique_gear diff --git a/engine/player/unique_gear_dragonflight.cpp b/engine/player/unique_gear_dragonflight.cpp index 8e22fbdb1fd..e2c7b21b799 100644 --- a/engine/player/unique_gear_dragonflight.cpp +++ b/engine/player/unique_gear_dragonflight.cpp @@ -622,9 +622,8 @@ void projectile_propulsion_pinion( special_effect_t& effect ) }; effect.player->register_combat_begin( [ buffs ]( player_t* p ) { - static constexpr std::array ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, STAT_HASTE_RATING, STAT_CRIT_RATING }; - buffs.find( util::highest_stat( p, ratings ) ) -> second -> trigger(); - buffs.find( util::lowest_stat( p, ratings ) ) -> second -> trigger(); + buffs.find( util::highest_stat( p, secondary_ratings ) )->second->trigger(); + buffs.find( util::lowest_stat( p, secondary_ratings ) )->second->trigger(); } ); } @@ -3275,8 +3274,6 @@ void frenzying_signoll_flare(special_effect_t& effect) action_t* smorfs; action_t* barfs; std::shared_ptr> siki_buffs; - // When selecting the highest stat, the priority of equal secondary stats is Vers > Mastery > Haste > Crit. - std::array ratings; frenzying_signoll_flare_t(const special_effect_t& e) : proc_spell_t("frenzying_signoll_flare", e.player, e.player -> find_spell(382119), e.item) @@ -3291,9 +3288,7 @@ void frenzying_signoll_flare(special_effect_t& effect) siki_buffs = std::make_shared>(); double amount = e.driver()->effectN( 1 ).average( e.item ); - ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, STAT_HASTE_RATING, - STAT_CRIT_RATING }; - for ( auto stat : ratings ) + for ( auto stat : secondary_ratings ) { auto name = std::string( "sikis_ambush_" ) + util::stat_type_string( stat ); buff_t* buff = buff_t::find( e.player, name ); @@ -3328,7 +3323,7 @@ void frenzying_signoll_flare(special_effect_t& effect) else if (selected_effect == 2 ) { - stat_e max_stat = util::highest_stat( player, ratings ); + stat_e max_stat = util::highest_stat( player, secondary_ratings ); ( *siki_buffs )[ max_stat ]->trigger(); } } @@ -5554,9 +5549,6 @@ void mirror_of_fractured_tomorrows( special_effect_t& e ) } }; - static constexpr std::array ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, STAT_HASTE_RATING, - STAT_CRIT_RATING }; - struct mirror_of_fractured_tomorrows_t : public spell_t { spawner::pet_spawner_t spawner; @@ -5571,7 +5563,7 @@ void mirror_of_fractured_tomorrows( special_effect_t& e ) dual = false; auto amount = e.driver()->effectN( 1 ).average( e.item ); - for ( auto stat : ratings ) + for ( auto stat : secondary_ratings ) { auto name = std::string( "mirror_of_fractured_tomorrows_" ) + util::stat_type_string( stat ); auto buff = create_buff( e.player, name, e.player->find_spell( 418527 ) ) @@ -5643,7 +5635,7 @@ void mirror_of_fractured_tomorrows( special_effect_t& e ) spell_t::execute(); spawner.spawn(); - stat_e max_stat = util::highest_stat( effect.player, ratings ); + stat_e max_stat = util::highest_stat( effect.player, secondary_ratings ); buffs[ max_stat ]->trigger(); } }; @@ -8860,11 +8852,10 @@ void potent_venom( special_effect_t& effect ) auto buff = create_buff(effect.player, effect.driver()->effectN(3).trigger(), effect); buff->set_stack_change_callback( [effect, buff, gain, loss] ( buff_t*, int, int new_ ) { - static constexpr std::array ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, STAT_HASTE_RATING, STAT_CRIT_RATING }; if ( new_ ) { - buff->gain = util::highest_stat(effect.player, ratings); - buff->loss = util::lowest_stat(effect.player, ratings); + buff->gain = util::highest_stat(effect.player, secondary_ratings); + buff->loss = util::lowest_stat(effect.player, secondary_ratings); buff->player->stat_gain(buff->gain, gain); buff->player->stat_loss(buff->loss, loss); @@ -9103,14 +9094,11 @@ void voice_of_the_silent_star( special_effect_t& effect ) "power_beyond_imagination_haste_rating", "power_beyond_imagination_versatility_rating" } ) ) return; - static constexpr std::array ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, STAT_HASTE_RATING, - STAT_CRIT_RATING }; - auto buffs = std::make_shared>(); double amount = effect.driver()->effectN( 1 ).average( effect.item ) + ( effect.driver()->effectN( 3 ).average( effect.item ) * effect.driver()->effectN( 4 ).base_value() ); - for ( auto stat : ratings ) + for ( auto stat : secondary_ratings ) { auto name = std::string( "power_beyond_imagination_" ) + util::stat_type_string( stat ); auto buff = create_buff( effect.player, name, effect.player->find_spell( 409447 ), effect.item ) @@ -9125,7 +9113,7 @@ void voice_of_the_silent_star( special_effect_t& effect ) ->set_stack_change_callback( [ buffs, effect ]( buff_t*, int, int new_ ) { if ( !new_ ) { - stat_e max_stat = util::highest_stat( effect.player, ratings ); + stat_e max_stat = util::highest_stat( effect.player, secondary_ratings ); ( *buffs )[ max_stat ]->trigger(); } } ); @@ -9732,9 +9720,6 @@ void raging_tempests( special_effect_t& effect ) if ( check_set( B2 ) ) { - static constexpr std::array ratings = - { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, STAT_HASTE_RATING, STAT_CRIT_RATING }; - auto buff = create_buff( effect.player, effect.driver() ); buff->set_constant_behavior( buff_constant_behavior::ALWAYS_CONSTANT ); @@ -9744,7 +9729,7 @@ void raging_tempests( special_effect_t& effect ) // temporary buffs during equip, instead of implementing as a passive stat bonus we create a buff to trigger on // combat start, accounting for anything in the precombat apl. effect.player->register_combat_begin( [ buff, effect, val ]( player_t* p ) { - buff->set_stat( util::highest_stat( p, ratings ), val ); + buff->set_stat( util::highest_stat( p, secondary_ratings ), val ); buff->trigger(); } ); } @@ -11738,33 +11723,18 @@ void register_hotfixes() void register_actor_initializers( sim_t& sim ) { + // +10 for wow version 10.x sim.register_actor_initializer( INIT_ACTOR_CREATE_EFFECTS + 10, []( player_t* p ) { if ( p->dragonflight_opts.emerald_coachs_whistle_ally_ilvl > 0 ) { - struct emerald_coachs_whistle_ally_t : public special_effect_t + struct emerald_coachs_whistle_ally_t : public external_special_effect_t { - std::unique_ptr _item; - - emerald_coachs_whistle_ally_t( player_t* p ) : special_effect_t( p ) + emerald_coachs_whistle_ally_t( player_t* p ) + : external_special_effect_t( p, "emerald_coachs_whistle_ally", 193718, + p->dragonflight_opts.emerald_coachs_whistle_ally_ilvl ) { - // make a fake - _item = std::make_unique( - p, fmt::format( ",id=193718,ilevel={}", p->dragonflight_opts.emerald_coachs_whistle_ally_ilvl ) ); - _item->parse_options(); - _item->initialize_data(); - _item->init(); - - // validate data - auto it = range::find( _item->parsed.data.effects, ITEM_SPELLTRIGGER_ON_EQUIP, &item_effect_t::type ); - if ( it == _item->parsed.data.effects.end() ) - { - throw sc_invalid_player_argument( - "Cannot find on-equip effect on item id=193718 for 'dragonflight.emerald_coachs_whistle_ally_ilvl'." ); - } - - spell_id = p->dragonflight_opts.emerald_coachs_whistle_ally_is_healer ? 386578 : it->spell_id; - name_str = "emerald_coachs_whistle_ally"; - item = _item.get(); + if ( p->dragonflight_opts.emerald_coachs_whistle_ally_is_healer ) + spell_id = 386578; custom_init = &items::emerald_coachs_whistle; } @@ -11772,7 +11742,8 @@ void register_actor_initializers( sim_t& sim ) p->special_effects.push_back( new emerald_coachs_whistle_ally_t( p ) ); } - }, "create_buffs_dragonflight" ); + }, + "create_buffs_dragonflight" ); } // check and return multiplier for toxified armor patch diff --git a/engine/player/unique_gear_helper.cpp b/engine/player/unique_gear_helper.cpp index a016067938f..9881ed96717 100644 --- a/engine/player/unique_gear_helper.cpp +++ b/engine/player/unique_gear_helper.cpp @@ -4,3 +4,30 @@ // ========================================================================== #include "unique_gear_helper.hpp" + +#include "item/item.hpp" + +namespace unique_gear +{ +external_special_effect_t::external_special_effect_t( player_t* p, std::string_view name, unsigned item_id, + unsigned ilevel ) + : special_effect_t( p ) +{ + // make a fake + _item = std::make_unique( p, fmt::format( ",id={},ilevel={}", item_id, ilevel ) ); + _item->parse_options(); + _item->initialize_data(); + _item->init(); + + auto it = range::find( _item->parsed.data.effects, ITEM_SPELLTRIGGER_ON_EQUIP, &item_effect_t::type ); + if ( it == _item->parsed.data.effects.end() ) + { + throw sc_invalid_player_argument( + fmt::format( "Cannot find on-equip effect for external item '{}'.", *_item.get() ) ); + } + + spell_id = it->spell_id; + name_str = name; + item = _item.get(); +} +} // namespace unique_gear diff --git a/engine/player/unique_gear_helper.hpp b/engine/player/unique_gear_helper.hpp index 31b63e6db57..f5b8e914dc6 100644 --- a/engine/player/unique_gear_helper.hpp +++ b/engine/player/unique_gear_helper.hpp @@ -26,6 +26,8 @@ #include #include +struct item_t; + namespace unique_gear { // Old-style special effect registering functions @@ -803,4 +805,14 @@ struct unique_gear_pet_t : public pet_t pet_t::init_action_list(); } }; + +// proxy to be used for external effects coming from items on other sources +struct external_special_effect_t : public special_effect_t +{ +private: + std::unique_ptr _item; + +public: + external_special_effect_t( player_t* p, std::string_view name, unsigned item_id, unsigned ilevel ); +}; } // unique_gear diff --git a/engine/player/unique_gear_midnight.cpp b/engine/player/unique_gear_midnight.cpp index 24e41383ee8..ebfbddb4d18 100644 --- a/engine/player/unique_gear_midnight.cpp +++ b/engine/player/unique_gear_midnight.cpp @@ -40,10 +40,6 @@ void set_min_version( wowv_t build ) void set_max_version( wowv_t build ) { version_max = build; } -// assuming priority for highest/lowest secondary is vers > mastery > haste > crit -static constexpr std::array secondary_ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, - STAT_HASTE_RATING, STAT_CRIT_RATING }; - // from item_naming.inc enum gem_color_e : unsigned { diff --git a/engine/player/unique_gear_shadowlands.cpp b/engine/player/unique_gear_shadowlands.cpp index 9bafcd592ce..19d8eb5afa7 100644 --- a/engine/player/unique_gear_shadowlands.cpp +++ b/engine/player/unique_gear_shadowlands.cpp @@ -1229,7 +1229,6 @@ void infinitely_divisible_ooze( special_effect_t& effect ) */ void inscrutable_quantum_device ( special_effect_t& effect ) { - static constexpr std::array ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, STAT_HASTE_RATING, STAT_CRIT_RATING }; static constexpr std::array buff_ids = { 330367, 330380, 330368, 330366 }; struct inscrutable_quantum_device_execute_t : public proc_spell_t @@ -1258,16 +1257,16 @@ void inscrutable_quantum_device ( special_effect_t& effect ) proc_spell_t( "inscrutable_quantum_device", e.player, e.player->find_spell( 330323 ) ) { buffs[ STAT_NONE ] = nullptr; - for ( unsigned i = 0; i < ratings.size(); i++ ) + for ( unsigned i = 0; i < secondary_ratings.size(); i++ ) { - auto name = std::string( "inscrutable_quantum_device_" ) + util::stat_type_string( ratings[ i ] ); + auto name = std::string( "inscrutable_quantum_device_" ) + util::stat_type_string( secondary_ratings[ i ] ); stat_buff_t* buff = debug_cast( buff_t::find( e.player, name ) ); if ( !buff ) { buff = make_buff( e.player, name, e.player->find_spell( buff_ids[ i ] ), e.item ); buff->set_cooldown( 0_ms ); } - buffs[ ratings[ i ] ] = buff; + buffs[ secondary_ratings[ i ] ] = buff; } execute_damage = create_proc_action( "inscrutable_quantum_device_execute", e ); } @@ -1310,7 +1309,7 @@ void inscrutable_quantum_device ( special_effect_t& effect ) buff_t* buff; timespan_t duration_adjustment; - s1 = util::highest_stat( player, ratings ); + s1 = util::highest_stat( player, secondary_ratings ); if ( is_buff_extended() ) { @@ -1321,7 +1320,7 @@ void inscrutable_quantum_device ( special_effect_t& effect ) { if ( rng().roll( sim->shadowlands_opts.iqd_stat_fail_chance ) ) return; - for ( auto s : ratings ) + for ( auto s : secondary_ratings ) { auto v = util::stat_value( player, s ); if ( ( s2 == STAT_NONE || v > util::stat_value( player, s2 ) ) && @@ -2185,16 +2184,13 @@ void miniscule_mailemental_in_an_envelope( special_effect_t& effect ) */ void titanic_ocular_gland( special_effect_t& effect ) { - // When selecting the highest stat, the priority of equal secondary stats is Vers > Mastery > Haste > Crit. - static constexpr std::array ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, STAT_HASTE_RATING, STAT_CRIT_RATING }; - // Use a separate buff for each rating type so that individual uptimes are reported nicely and APLs can easily reference them. // Store these in pointers to reduce the size of the events that use them. auto worthy_buffs = std::make_shared>(); auto unworthy_buffs = std::make_shared>(); double amount = effect.driver()->effectN( 1 ).average( effect.item ); - for ( auto stat : ratings ) + for ( auto stat : secondary_ratings ) { auto name = std::string( "worthy_" ) + util::stat_type_string( stat ); buff_t* buff = buff_t::find( effect.player, name ); @@ -2223,11 +2219,11 @@ void titanic_ocular_gland( special_effect_t& effect ) { bool worthy = p->rng().roll( p->sim->shadowlands_opts.titanic_ocular_gland_worthy_chance ); bool buff_active = false; - stat_e max_stat = util::highest_stat( p, ratings ); + stat_e max_stat = util::highest_stat( p, secondary_ratings ); // Iterate over all of the buffs and expire any that should not be active. Only one buff is // active at a time, so this process only needs to continue until a single active buff is found. - for ( auto stat : ratings ) + for ( auto stat : secondary_ratings ) { if ( ( *worthy_buffs )[ stat ]->check() ) { @@ -2236,7 +2232,7 @@ void titanic_ocular_gland( special_effect_t& effect ) if ( max_stat != stat || !worthy ) { ( *worthy_buffs )[ stat ]->expire(); - max_stat = util::highest_stat( p, ratings ); + max_stat = util::highest_stat( p, secondary_ratings ); } else { @@ -2250,7 +2246,7 @@ void titanic_ocular_gland( special_effect_t& effect ) if ( worthy ) { ( *unworthy_buffs )[ stat ]->expire(); - max_stat = util::highest_stat( p, ratings ); + max_stat = util::highest_stat( p, secondary_ratings ); } else { @@ -3283,17 +3279,13 @@ void cosmic_gladiators_resonator( special_effect_t& effect ) void elegy_of_the_eternals( special_effect_t& effect ) { - // TODO: confirm stat priority when stats are equal. for now assuming same as titanic ocular gland - static constexpr std::array ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, STAT_HASTE_RATING, - STAT_CRIT_RATING }; - auto buff_list = std::make_shared>(); // TODO: 369544 has same data as the driver, but with the presumably correct -7 scaling effect. Confirm that the // driver really is 367246 and that 369544 is an unreferenced placeholder spell for the correct scaling effect. double amount = effect.player->find_spell( 369544 )->effectN( 1 ).average( effect.item ); - for ( auto stat : ratings ) + for ( auto stat : secondary_ratings ) { auto name = fmt::format( "elegy_of_the_eternals_{}", util::stat_type_abbrev( stat ) ); auto buff = buff_t::find( effect.player, name ); @@ -3308,16 +3300,16 @@ void elegy_of_the_eternals( special_effect_t& effect ) } auto update_buffs = [ p = effect.player, buff_list ]() mutable { - auto max_stat = util::highest_stat( p, ratings ); + auto max_stat = util::highest_stat( p, secondary_ratings ); - for ( auto stat : ratings ) + for ( auto stat : secondary_ratings ) { if ( ( *buff_list )[ stat ]->check() ) { if ( max_stat != stat ) { ( *buff_list )[ stat ]->expire(); - max_stat = util::highest_stat( p, ratings ); + max_stat = util::highest_stat( p, secondary_ratings ); } break; @@ -5236,4 +5228,29 @@ void register_target_data_initializers( sim_t& sim ) sim.register_target_data_initializer( remnants_despair_init_t() ); } +void register_actor_initializers( sim_t& sim ) +{ + // +9 for wow version 9.x + sim.register_actor_initializer( INIT_ACTOR_CREATE_BUFFS + 9, []( player_t* p ) { + if ( p->external_buffs.soleahs_secret_technique ) + { + struct soleahs_secret_technique_external_t : public external_special_effect_t + { + soleahs_secret_technique_external_t( player_t* p ) + : external_special_effect_t( p, "soleahs_secret_technique_external", 190958, + p->external_buffs.soleahs_secret_technique ) + { + auto stat = util::highest_stat( p, secondary_ratings ); + auto buff = make_buff( p, "soleahs_secret_technique_external", p->find_spell( 368510 ) ) + ->add_stat( stat, driver()->effectN( 2 ).average( *this ) ) + ->set_name_reporting( fmt::format( "External {}", util::stat_type_abbrev( stat ) ) ); + + p->register_on_arise_callback( p, [ buff ] { buff->trigger(); } ); + } + }; + + p->special_effects.push_back( new soleahs_secret_technique_external_t( p ) ); + } + }, "create_buffs_shadowlands" ); +} } // namespace unique_gear diff --git a/engine/player/unique_gear_shadowlands.hpp b/engine/player/unique_gear_shadowlands.hpp index 61e9354457d..0d72c5a8a6e 100644 --- a/engine/player/unique_gear_shadowlands.hpp +++ b/engine/player/unique_gear_shadowlands.hpp @@ -81,5 +81,6 @@ void register_hotfixes(); void register_special_effects(); action_t* create_action( player_t* player, util::string_view name, util::string_view options ); void register_target_data_initializers( sim_t& ); +void register_actor_initializers( sim_t& ); } // namespace unique_gear::shadowlands diff --git a/engine/player/unique_gear_thewarwithin.cpp b/engine/player/unique_gear_thewarwithin.cpp index 96fafa94c31..a286aff4876 100644 --- a/engine/player/unique_gear_thewarwithin.cpp +++ b/engine/player/unique_gear_thewarwithin.cpp @@ -41,10 +41,6 @@ void set_min_version( wowv_t build ) void set_max_version( wowv_t build ) { version_max = build; } -// assuming priority for highest/lowest secondary is vers > mastery > haste > crit -static constexpr std::array secondary_ratings = { STAT_VERSATILITY_RATING, STAT_MASTERY_RATING, - STAT_HASTE_RATING, STAT_CRIT_RATING }; - // from item_naming.inc enum gem_color_e : unsigned { From 4ff7a8c18d2862c2ef672d8e925b37a42d254c3f Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sun, 24 May 2026 09:30:48 -0700 Subject: [PATCH 09/13] move potion bomb of power code to unique_gear_thewarwithin --- engine/player/player.cpp | 17 ------------- engine/player/player.hpp | 1 - engine/player/unique_gear.cpp | 2 ++ engine/player/unique_gear_dragonflight.cpp | 6 ++--- engine/player/unique_gear_midnight.cpp | 3 +++ engine/player/unique_gear_midnight.hpp | 1 + engine/player/unique_gear_thewarwithin.cpp | 28 ++++++++++++++++++++-- engine/player/unique_gear_thewarwithin.hpp | 1 + 8 files changed, 35 insertions(+), 24 deletions(-) diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 925a197fe57..94e730e17f7 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -4995,22 +4995,6 @@ void player_t::create_buffs() buffs.tome_of_unstable_power = buff; } - // Potion Bomb of Power Primary Stat - // Buff cannot stack - // Does not take into account the fire damage on enemies - if ( !external_buffs.potion_bomb_of_power.empty() ) - { - auto driver_spell = find_spell( 453205 ); - auto buff_spell = find_spell( 453245 ); - - // Value in buff data is for 5 people, need to split based on targets for a single player - auto main_stat_amount = buff_spell->effectN( 1 ).average( this ) / driver_spell->effectN( 4 ).base_value(); - - auto buff = make_buff( this, "potion_bomb_of_power_external", buff_spell ) - ->add_stat_from_effect_type( A_MOD_STAT, main_stat_amount ); - buffs.potion_bomb_of_power = buff; - } - // 9.2 Jailer raid buff // Values are hard-coded because difficulty-specific spell data is not fully extracted. buffs.boon_of_azeroth = make_buff( this, "boon_of_azeroth", find_spell( 363338 ) ) @@ -6428,7 +6412,6 @@ void player_t::combat_begin() add_timed_buff_triggers( external_buffs.boon_of_azeroth, buffs.boon_of_azeroth ); add_timed_buff_triggers( external_buffs.boon_of_azeroth_mythic, buffs.boon_of_azeroth_mythic ); add_timed_buff_triggers( external_buffs.tome_of_unstable_power, buffs.tome_of_unstable_power ); - add_timed_buff_triggers( external_buffs.potion_bomb_of_power, buffs.potion_bomb_of_power ); // Trigger registered combat-begin functions for ( const auto& f : combat_begin_functions) diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 1a52e4b8a59..ac9eb8bb4f6 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -576,7 +576,6 @@ struct player_t : public actor_t buff_t* quickwicks_quick_trick_wick_walk; // quickwick candlestick movement speed buff buff_t* building_momentum; // scroll of momentum counter buff buff_t* full_momentum; // scroll of momentum max buff - buff_t* potion_bomb_of_power; // potion bomb of power primary stat } buffs; struct debuffs_t diff --git a/engine/player/unique_gear.cpp b/engine/player/unique_gear.cpp index 6c64a25d0f4..821274bf635 100644 --- a/engine/player/unique_gear.cpp +++ b/engine/player/unique_gear.cpp @@ -4884,6 +4884,8 @@ void unique_gear::register_actor_initializers( sim_t& sim ) { shadowlands::register_actor_initializers( sim ); dragonflight::register_actor_initializers( sim ); + thewarwithin::register_actor_initializers( sim ); + midnight::register_actor_initializers( sim ); } std::vector unique_gear::find_special_effects( player_t* p, unsigned id, special_effect_e type ) diff --git a/engine/player/unique_gear_dragonflight.cpp b/engine/player/unique_gear_dragonflight.cpp index e2c7b21b799..c238fe2d358 100644 --- a/engine/player/unique_gear_dragonflight.cpp +++ b/engine/player/unique_gear_dragonflight.cpp @@ -11714,12 +11714,10 @@ void register_special_effects() } void register_target_data_initializers( sim_t& ) -{ -} +{} void register_hotfixes() -{ -} +{} void register_actor_initializers( sim_t& sim ) { diff --git a/engine/player/unique_gear_midnight.cpp b/engine/player/unique_gear_midnight.cpp index ebfbddb4d18..2fcc6cbc33b 100644 --- a/engine/player/unique_gear_midnight.cpp +++ b/engine/player/unique_gear_midnight.cpp @@ -4048,6 +4048,9 @@ void register_special_effects() void register_target_data_initializers( sim_t& ) {} +void register_actor_initializers( sim_t& ) +{} + void register_hotfixes() {} diff --git a/engine/player/unique_gear_midnight.hpp b/engine/player/unique_gear_midnight.hpp index 9a6a2316f6e..134d683df27 100644 --- a/engine/player/unique_gear_midnight.hpp +++ b/engine/player/unique_gear_midnight.hpp @@ -20,6 +20,7 @@ extern std::vector __mid_special_effect_ids; void register_special_effects(); void register_target_data_initializers( sim_t& ); +void register_actor_initializers( sim_t& ); void register_hotfixes(); action_t* create_action( player_t*, std::string_view, std::string_view ); double bandolier_mul( player_t* p ); diff --git a/engine/player/unique_gear_thewarwithin.cpp b/engine/player/unique_gear_thewarwithin.cpp index a286aff4876..69784529e6e 100644 --- a/engine/player/unique_gear_thewarwithin.cpp +++ b/engine/player/unique_gear_thewarwithin.cpp @@ -12624,12 +12624,36 @@ void register_special_effects() } void register_target_data_initializers( sim_t& ) +{} + +void register_actor_initializers( sim_t& sim ) { + // +11 for wow version 11.x + sim.register_actor_initializer( INIT_ACTOR_CREATE_BUFFS + 11, []( player_t* p ) { + // Potion Bomb of Power Primary Stat + // Buff cannot stack + // Does not take into account the fire damage on enemies + if ( !p->external_buffs.potion_bomb_of_power.empty() ) + { + auto driver_spell = p->find_spell( 453205 ); + auto buff_spell = p->find_spell( 453245 ); + + // Value in buff data is for 5 people, need to split based on targets for a single player + auto main_stat_amount = buff_spell->effectN( 1 ).average( p ) / driver_spell->effectN( 4 ).base_value(); + + auto buff = make_buff( p, "potion_bomb_of_power_external", buff_spell ) + ->add_stat_from_effect_type( A_MOD_STAT, main_stat_amount ); + + p->register_combat_begin( [ buff ]( player_t* p ) { + for ( auto t : p->external_buffs.potion_bomb_of_power ) + make_event( *p->sim, t, [ buff ] { buff->trigger(); }); + } ); + } + }, "create_buffs_thewarwithin" ); } void register_hotfixes() -{ -} +{} action_t* create_action( player_t* p, std::string_view n, std::string_view options ) { diff --git a/engine/player/unique_gear_thewarwithin.hpp b/engine/player/unique_gear_thewarwithin.hpp index eb5c6adc123..2514edc4d8d 100644 --- a/engine/player/unique_gear_thewarwithin.hpp +++ b/engine/player/unique_gear_thewarwithin.hpp @@ -18,6 +18,7 @@ namespace unique_gear::thewarwithin { void register_special_effects(); void register_target_data_initializers( sim_t& ); +void register_actor_initializers( sim_t& ); void register_hotfixes(); action_t* create_action( player_t*, std::string_view, std::string_view ); double writhing_mul( player_t* ); From 2e93fada81c8d1a42efaa101e49249974f4f431c Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sun, 24 May 2026 10:07:30 -0700 Subject: [PATCH 10/13] move rallying cry code to sc_warrior --- engine/class_modules/sc_warrior.cpp | 26 ++++++++++++++++++++++++-- engine/player/player.cpp | 1 - engine/player/player.hpp | 1 - 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/engine/class_modules/sc_warrior.cpp b/engine/class_modules/sc_warrior.cpp index dca00fe8511..6dc6263a753 100644 --- a/engine/class_modules/sc_warrior.cpp +++ b/engine/class_modules/sc_warrior.cpp @@ -252,6 +252,7 @@ struct warrior_t : public parse_player_effects_t buff_t* into_the_fray; buff_t* last_stand; buff_t* martial_prowess; + buff_t* rallying_cry; buff_t* ravager; buff_t* recklessness; buff_t* revenge; @@ -6824,7 +6825,9 @@ struct rallying_cry_t : public warrior_spell_t void execute() override { warrior_spell_t::execute(); - player->buffs.rallying_cry->trigger(); + + assert( p()->buff.rallying_cry ); + p()->buff.rallying_cry->trigger(); } }; @@ -9403,7 +9406,26 @@ struct warrior_module_t : public module_t void init( player_t* p ) const override { - p->buffs.rallying_cry = make_buff( p ); + bool has_external_rallying = !p->external_buffs.rallying_cry.empty(); + bool has_talent_rallying = p->type == WARRIOR && debug_cast( p )->talents.warrior.rallying_cry.ok(); + + if ( has_external_rallying || has_talent_rallying ) + { + auto buff = make_buff( p ); + + if ( has_external_rallying ) + { + p->register_combat_begin( [ buff ]( player_t* p ) { + for ( auto t : p->external_buffs.rallying_cry ) + make_event( *p->sim, t, [ buff ] { buff->trigger(); } ); + } ); + } + + if ( has_talent_rallying ) + { + debug_cast( p )->buff.rallying_cry = buff; + } + } } void combat_begin( sim_t* ) const override { diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 94e730e17f7..d699d994185 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -6408,7 +6408,6 @@ void player_t::combat_begin() }; add_timed_buff_triggers( external_buffs.power_infusion, buffs.power_infusion ); - add_timed_buff_triggers( external_buffs.rallying_cry, buffs.rallying_cry ); add_timed_buff_triggers( external_buffs.boon_of_azeroth, buffs.boon_of_azeroth ); add_timed_buff_triggers( external_buffs.boon_of_azeroth_mythic, buffs.boon_of_azeroth_mythic ); add_timed_buff_triggers( external_buffs.tome_of_unstable_power, buffs.tome_of_unstable_power ); diff --git a/engine/player/player.hpp b/engine/player/player.hpp index ac9eb8bb4f6..f28cd5cd993 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -545,7 +545,6 @@ struct player_t : public actor_t // 9.0 class buffs buff_t* focus_magic; // Mage talent buff_t* power_infusion; // Priest spell - buff_t* rallying_cry; // Warrior spell // 9.0 Runecarves buff_t* norgannons_sagacity; // consume stacks to allow casting while moving From 4568132ba5d5fbc3f78656f05060f55464ac88eb Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sun, 24 May 2026 11:31:28 -0700 Subject: [PATCH 11/13] move power infusion code to sc_priest --- engine/class_modules/priest/sc_priest.cpp | 43 ++++++++++++++----- engine/class_modules/priest/sc_priest.hpp | 1 + .../class_modules/priest/sc_priest_pets.cpp | 28 ++++++------ engine/class_modules/sc_evoker.cpp | 2 +- engine/class_modules/sc_warrior.cpp | 31 +++++++------ engine/player/player.cpp | 12 ------ engine/player/player.hpp | 1 - 7 files changed, 66 insertions(+), 52 deletions(-) diff --git a/engine/class_modules/priest/sc_priest.cpp b/engine/class_modules/priest/sc_priest.cpp index d959f5339bc..d7278b7f4b0 100644 --- a/engine/class_modules/priest/sc_priest.cpp +++ b/engine/class_modules/priest/sc_priest.cpp @@ -859,7 +859,7 @@ struct power_infusion_t final : public priest_spell_t double power_infusion_magnitude; power_infusion_t( priest_t& p, util::string_view options_str, util::string_view name ) : priest_spell_t( name, p, p.talents.power_infusion ), - power_infusion_magnitude( player->buffs.power_infusion->default_value ) + power_infusion_magnitude( p.buffs.power_infusion->default_value ) { parse_options( options_str ); harmful = false; @@ -872,8 +872,8 @@ struct power_infusion_t final : public priest_spell_t // Trigger PI on the actor only if casting on itself if ( priest().options.self_power_infusion || priest().talents.twins_of_the_sun_priestess.enabled() ) { - player->buffs.power_infusion->trigger( 1, power_infusion_magnitude, -1, - player->buffs.power_infusion->buff_duration() ); + priest().buffs.power_infusion->trigger( 1, power_infusion_magnitude, -1, + priest().buffs.power_infusion->buff_duration() ); } } }; @@ -4245,14 +4245,35 @@ struct priest_module_t final : public module_t } void init( player_t* p ) const override { - p->buffs.body_and_soul = make_buff( p, "body_and_soul", p->find_spell( 65081 ) ); - p->buffs.angelic_feather = make_buff( p, "angelic_feather", p->find_spell( 121557 ) ); - p->buffs.guardian_spirit = make_buff( p, "guardian_spirit", p->find_spell( 47788 ) ) - ->set_default_value_from_effect_type( A_MOD_HEALING_RECEIVED_PCT ) - ->set_cooldown( 0_ms ); // Let the ability handle the CD - p->buffs.pain_suppression = make_buff( p, "pain_suppression", p->find_spell( 33206 ) ) - ->set_default_value_from_effect_type( A_MOD_DAMAGE_PERCENT_TAKEN ) - ->set_cooldown( 0_ms ); // Let the ability handle the CD + if ( !p->is_pet() ) + { + p->buffs.body_and_soul = make_buff( p, "body_and_soul", p->find_spell( 65081 ) ); + p->buffs.angelic_feather = make_buff( p, "angelic_feather", p->find_spell( 121557 ) ); + p->buffs.guardian_spirit = make_buff( p, "guardian_spirit", p->find_spell( 47788 ) ) + ->set_default_value_from_effect_type( A_MOD_HEALING_RECEIVED_PCT ) + ->set_cooldown( 0_ms ); // Let the ability handle the CD + p->buffs.pain_suppression = make_buff( p, "pain_suppression", p->find_spell( 33206 ) ) + ->set_default_value_from_effect_type( A_MOD_DAMAGE_PERCENT_TAKEN ) + ->set_cooldown( 0_ms ); // Let the ability handle the CD + + auto pi_buff = make_buff( p, "power_infusion", p->find_spell( 10060 ) ) + ->set_pct_buff_type( STAT_PCT_BUFF_HASTE ) + ->set_default_value_from_effect_type( A_HASTE_ALL ) + ->set_cooldown( 0_ms ); + + if ( p->type == PRIEST ) + { + debug_cast( p )->buffs.power_infusion = pi_buff; + } + + if ( !p->external_buffs.power_infusion.empty() ) + { + p->register_combat_begin( [ pi_buff ]( player_t* p ) { + for ( auto t : p->external_buffs.power_infusion ) + make_event( *p->sim, t, [ pi_buff ] { pi_buff->trigger(); } ); + } ); + } + } } void static_init() const override { diff --git a/engine/class_modules/priest/sc_priest.hpp b/engine/class_modules/priest/sc_priest.hpp index aad05eb617c..7f7da71034e 100644 --- a/engine/class_modules/priest/sc_priest.hpp +++ b/engine/class_modules/priest/sc_priest.hpp @@ -173,6 +173,7 @@ struct priest_t final : public player_t absorb_buff_t* power_word_shield; propagate_const fade; propagate_const levitate; + propagate_const power_infusion; // Talents propagate_const twist_of_fate_heal_self_fake; diff --git a/engine/class_modules/priest/sc_priest_pets.cpp b/engine/class_modules/priest/sc_priest_pets.cpp index fde6b109c78..365cd664a7b 100644 --- a/engine/class_modules/priest/sc_priest_pets.cpp +++ b/engine/class_modules/priest/sc_priest_pets.cpp @@ -72,8 +72,10 @@ namespace actions */ struct priest_pet_t : public pet_t { + buff_t* power_infusion_buff; + priest_pet_t( sim_t* sim, priest_t& owner, util::string_view pet_name, bool guardian = false ) - : pet_t( sim, &owner, pet_name, PET_NONE, guardian ) + : pet_t( sim, &owner, pet_name, PET_NONE, guardian ), power_infusion_buff( nullptr ) { } @@ -114,18 +116,18 @@ struct priest_pet_t : public pet_t { pet_t::create_buffs(); - buffs.power_infusion = make_buff( this, "power_infusion", find_spell( 10060 ) ) - ->set_default_value_from_effect( 1 ) - ->set_cooldown( 0_ms ) - ->add_invalidate( CACHE_HASTE ); + power_infusion_buff = make_buff( this, "power_infusion", find_spell( 10060 ) ) + ->set_default_value_from_effect_type( A_HASTE_ALL ) + ->set_cooldown( 0_ms ) + ->add_invalidate( CACHE_HASTE ); } double composite_melee_haste() const override { double h = pet_t::composite_melee_haste(); - if ( buffs.power_infusion ) - h *= 1.0 / ( 1.0 + buffs.power_infusion->check_value() ); + if ( power_infusion_buff ) + h *= 1.0 / ( 1.0 + power_infusion_buff->check_value() ); return h; } @@ -134,8 +136,8 @@ struct priest_pet_t : public pet_t { double h = pet_t::composite_spell_haste(); - if ( buffs.power_infusion ) - h *= 1.0 / ( 1.0 + buffs.power_infusion->check_value() ); + if ( power_infusion_buff ) + h *= 1.0 / ( 1.0 + power_infusion_buff->check_value() ); return h; } @@ -144,8 +146,8 @@ struct priest_pet_t : public pet_t { double h = pet_t::composite_melee_auto_attack_speed(); - if ( buffs.power_infusion ) - h *= 1.0 / ( 1.0 + buffs.power_infusion->check_value() ); + if ( power_infusion_buff ) + h *= 1.0 / ( 1.0 + power_infusion_buff->check_value() ); return h; } @@ -154,8 +156,8 @@ struct priest_pet_t : public pet_t { double h = pet_t::composite_spell_cast_speed(); - if ( buffs.power_infusion ) - h *= 1.0 / ( 1.0 + buffs.power_infusion->check_value() ); + if ( power_infusion_buff ) + h *= 1.0 / ( 1.0 + power_infusion_buff->check_value() ); return h; } diff --git a/engine/class_modules/sc_evoker.cpp b/engine/class_modules/sc_evoker.cpp index c7737bd790b..74b617230be 100644 --- a/engine/class_modules/sc_evoker.cpp +++ b/engine/class_modules/sc_evoker.cpp @@ -9264,7 +9264,7 @@ void evoker_t::init_finished() { if ( CT( p, "Power Infusion" ).ok() ) { - allied_major_cds[ p ] = p->buffs.power_infusion; + allied_major_cds[ p ] = buff_t::find( p, "power_infusion" ); } else if ( ST( p, "Void Eruption" ).ok() ) { diff --git a/engine/class_modules/sc_warrior.cpp b/engine/class_modules/sc_warrior.cpp index 6dc6263a753..c7420545394 100644 --- a/engine/class_modules/sc_warrior.cpp +++ b/engine/class_modules/sc_warrior.cpp @@ -9406,24 +9406,27 @@ struct warrior_module_t : public module_t void init( player_t* p ) const override { - bool has_external_rallying = !p->external_buffs.rallying_cry.empty(); - bool has_talent_rallying = p->type == WARRIOR && debug_cast( p )->talents.warrior.rallying_cry.ok(); - - if ( has_external_rallying || has_talent_rallying ) + if ( !p->is_pet() ) { - auto buff = make_buff( p ); + bool has_external_rallying = !p->external_buffs.rallying_cry.empty(); + bool has_talent_rallying = p->type == WARRIOR && debug_cast( p )->talents.warrior.rallying_cry.ok(); - if ( has_external_rallying ) + if ( has_external_rallying || has_talent_rallying ) { - p->register_combat_begin( [ buff ]( player_t* p ) { - for ( auto t : p->external_buffs.rallying_cry ) - make_event( *p->sim, t, [ buff ] { buff->trigger(); } ); - } ); - } + auto buff = make_buff( p ); - if ( has_talent_rallying ) - { - debug_cast( p )->buff.rallying_cry = buff; + if ( has_external_rallying ) + { + p->register_combat_begin( [ buff ]( player_t* p ) { + for ( auto t : p->external_buffs.rallying_cry ) + make_event( *p->sim, t, [ buff ] { buff->trigger(); } ); + } ); + } + + if ( has_talent_rallying ) + { + debug_cast( p )->buff.rallying_cry = buff; + } } } } diff --git a/engine/player/player.cpp b/engine/player/player.cpp index d699d994185..f4a59f61e22 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -4930,11 +4930,6 @@ void player_t::create_buffs() ->set_default_value_from_effect( 1 ) ->add_invalidate( CACHE_SPELL_CRIT_CHANCE ); - buffs.power_infusion = make_buff( this, "power_infusion", find_spell( 10060 ) ) - ->set_default_value_from_effect( 1 ) - ->set_cooldown( 0_ms ) - ->add_invalidate( CACHE_HASTE ); - // External trinkets if ( !external_buffs.elegy_of_the_eternals.empty() ) { @@ -5145,9 +5140,6 @@ double player_t::composite_melee_haste() const if ( timeofday == NIGHT_TIME ) h *= 1.0 / ( 1.0 + racials.touch_of_elune->effectN( 1 ).percent() ); - - if ( buffs.power_infusion ) - h *= 1.0 / ( 1.0 + buffs.power_infusion->check_value() ); } return h; @@ -5492,9 +5484,6 @@ double player_t::composite_spell_haste() const if ( timeofday == NIGHT_TIME ) h *= 1.0 / ( 1.0 + racials.touch_of_elune->effectN( 1 ).percent() ); - - if ( buffs.power_infusion ) - h *= 1.0 / ( 1.0 + buffs.power_infusion->check_value() ); } return h; @@ -6407,7 +6396,6 @@ void player_t::combat_begin() make_event( *sim, t, [ buff, duration ] { buff->trigger( duration ); } ); }; - add_timed_buff_triggers( external_buffs.power_infusion, buffs.power_infusion ); add_timed_buff_triggers( external_buffs.boon_of_azeroth, buffs.boon_of_azeroth ); add_timed_buff_triggers( external_buffs.boon_of_azeroth_mythic, buffs.boon_of_azeroth_mythic ); add_timed_buff_triggers( external_buffs.tome_of_unstable_power, buffs.tome_of_unstable_power ); diff --git a/engine/player/player.hpp b/engine/player/player.hpp index f28cd5cd993..2b46fb007a3 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -544,7 +544,6 @@ struct player_t : public actor_t // 9.0 class buffs buff_t* focus_magic; // Mage talent - buff_t* power_infusion; // Priest spell // 9.0 Runecarves buff_t* norgannons_sagacity; // consume stacks to allow casting while moving From 388a9d4281e8efe5eadefcef0c1d57e8ab428ff9 Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sun, 24 May 2026 14:10:48 -0700 Subject: [PATCH 12/13] sort initializer by priority then name --- engine/sim/sim.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index 256e988eb6e..b3884e3c201 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -2592,7 +2592,9 @@ void sim_t::init_actors() // sort initializers by priority range::sort( actor_initializer, []( const auto& a, const auto& b ) { - return std::get( a ) < std::get( b ); + auto a_prio = std::get( a ); + auto b_prio = std::get( b ); + return a_prio == b_prio ? std::get( a ) < std::get( b ) : a_prio < b_prio; } ); print_debug( "Initializing actors." ); From b7b90ccbe1c1283354e9ef41f85f60317681105f Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Sun, 24 May 2026 17:18:34 -0700 Subject: [PATCH 13/13] fix typo [skip ci] --- engine/player/unique_gear_dragonflight.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/player/unique_gear_dragonflight.cpp b/engine/player/unique_gear_dragonflight.cpp index c238fe2d358..0baebabb856 100644 --- a/engine/player/unique_gear_dragonflight.cpp +++ b/engine/player/unique_gear_dragonflight.cpp @@ -11741,7 +11741,7 @@ void register_actor_initializers( sim_t& sim ) p->special_effects.push_back( new emerald_coachs_whistle_ally_t( p ) ); } }, - "create_buffs_dragonflight" ); + "create_effects_dragonflight" ); } // check and return multiplier for toxified armor patch