diff --git a/engine/report/json/report_json.cpp b/engine/report/json/report_json.cpp index 80db8ebbb11..5d4b6e519cc 100644 --- a/engine/report/json/report_json.cpp +++ b/engine/report/json/report_json.cpp @@ -1269,6 +1269,7 @@ void to_json( const ::report::json::report_configuration_t& report_configuration { auto profileset_root = root[ "profilesets" ]; profileset_json( report_configuration, *sim.profilesets, sim, profileset_root ); + profileset_controller::report_json( sim, root ); } if ( !sim.plot->dps_plot_stats.empty() ) diff --git a/engine/report/report_html_sim.cpp b/engine/report/report_html_sim.cpp index fd0b58d94ab..386b91067da 100644 --- a/engine/report/report_html_sim.cpp +++ b/engine/report/report_html_sim.cpp @@ -1164,6 +1164,8 @@ void print_profilesets( std::ostream& out, const profileset::profilesets_t& prof print_profilesets_chart( out, sim ); + profileset_controller::report_html( sim, out ); + out << ""; out << ""; } diff --git a/engine/sim/profileset_control.cpp b/engine/sim/profileset_control.cpp new file mode 100644 index 00000000000..56e771284f7 --- /dev/null +++ b/engine/sim/profileset_control.cpp @@ -0,0 +1,337 @@ +#include "profileset_control.hpp" + +#include "dbc/dbc.hpp" +#include "dbc/item_set_bonus.hpp" +#include "interfaces/sc_js.hpp" +#include "player/set_bonus.hpp" +#include "profileset.hpp" +#include "sc_enums.hpp" +#include "sim.hpp" + +std::unordered_map profileset_controller_t::factory = { + { "set_bonus_enabled", profileset_controller::create_fn_pair() } }; + +std::atomic_uint profileset_controller_data_wrapper_t::id_generator; + +profileset_controller_data_t::profileset_controller_data_t( std::string_view key, std::string_view options ) + : key( key ), options( options ) +{ +} + +void profileset_controller_data_t::report_html_options( std::ostream& output ) const +{ + output << "" + << "" << util::encode_html( key ) << "" + << "" << exit_reasons.size() << "" + << "" << util::encode_html( options ) << "" + << "\n"; +} + +void profileset_controller_data_t::report_html_profileset( std::ostream& output ) const +{ + bool first = true; + output << fmt::format( "{}", exit_reasons.size(), + util::encode_html( key ) ); + for ( const auto& [ name, call_point, reason ] : exit_reasons ) + { + if ( !first ) + output << ""; + output << "" << util::encode_html( name ) << "" + << "" << util::encode_html( profileset_controller::call_point_string( call_point ) ) << "" + << "" << util::encode_html( reason ) << "" + << "\n"; + first = false; + } +} + +void profileset_controller_data_t::report_json_options( js::JsonOutput& root ) const +{ + auto output = root.add(); + auto splits = util::string_split( options, "," ); + output[ "profileset_controller_name" ] = key; + for ( const auto& split : splits ) + { + auto subsplit = util::string_split( split, "=" ); + assert( subsplit.size() == 2 ); + output[ subsplit[ 0 ] ] = subsplit[ 1 ]; + } +} + +void profileset_controller_data_t::report_json_profileset( js::JsonOutput& root ) const +{ + for ( const auto& [ name, call_point, reason ] : exit_reasons ) + { + auto output = root.add(); + output[ "profileset_name" ] = name; + output[ "interrupted_by" ] = key; + output[ "exit_point" ] = profileset_controller::call_point_string( call_point ); + output[ "exit_reason" ] = reason; + } +} + +profileset_controller_data_wrapper_t::profileset_controller_data_wrapper_t( std::string key, std::string_view options ) + : mutex(), id( id_generator++ ), key( key ), options( options ) +{ + if ( const auto& value = profileset_controller_t::factory.find( key ); + value != profileset_controller_t::factory.end() ) + data = value->second.second( key, options ); + assert( data ); +} + +void profileset_controller_data_wrapper_t::construct_controller( sim_t* sim ) +{ + if ( const auto& value = profileset_controller_t::factory.find( key ); + value != profileset_controller_t::factory.end() ) + { + auto controller = value->second.first( sim, id ); + controller->create_options(); + opts::parse( sim, "profileset_controller", controller->options, options, + [ this, &sim ]( opts::parse_status status, util::string_view name, util::string_view value ) { + // Fail parsing if strict parsing is used and the option is not found + if ( sim->strict_parsing && status == opts::parse_status::NOT_FOUND ) + return opts::parse_status::FAILURE; + // .. otherwise, just warn that there's an unknown option + if ( status == opts::parse_status::NOT_FOUND ) + sim->error( + "Warning: profileset controller '{}' provided unknown option '{}' with value '{}', ignoring.", + key, name, value ); + return status; + } ); + sim->profileset_controller.emplace_back( std::move( controller ) ); + return; + } + assert( false && "No factory fn for key found." ); +} + +bool profileset_controller_t::register_controller( std::string key, profileset_controller_t::factory_fn_pair_t&& value ) +{ + return factory.try_emplace( key, std::move( value ) ).second; +} + +bool profileset_controller_t::controller_exists( std::string key ) +{ + return factory.find( key ) != factory.end(); +} + +void profileset_controller_t::evaluate( sim_t* sim, call_point_e call_point ) +{ + if ( !sim->profileset_enabled || !sim->parent ) + return; + + std::function& )> cb; + switch ( call_point ) + { + case POST_INIT: + cb = []( std::unique_ptr& sc ) { return !sc->evaluate_post_init(); }; + break; + case POST_ITER: + cb = []( std::unique_ptr& sc ) { return !sc->evaluate_post_iter(); }; + break; + default: + assert( false ); + break; + } + auto pc = range::find_if( sim->profileset_controller, cb ); + if ( pc == sim->profileset_controller.end() ) + return; + + auto controller = pc->get(); + assert( controller->sim == sim ); + assert( controller->parent == sim->parent ); + + controller->set_exit_reason( + { sim->parent->profilesets->current_profileset_name(), call_point, controller->reason() } ); + + sim->canceled = true; + sim->error( error_level_e::TRIVIAL, "{}", controller->message( call_point ) ); + sim->interrupt(); +} + +void profileset_controller_t::add_option( std::unique_ptr&& option ) +{ + options.emplace_back( std::move( option ) ); +} + +profileset_controller_t::profileset_controller_t( sim_t* sim, unsigned int id ) + : parent( sim->parent ), sim( sim ), id( id ) +{ + assert( sim && sim->parent ); +} + +const std::string profileset_controller_t::message( call_point_e call_point ) +{ + std::string msg = fmt::format( "Profileset {} was canceled by Profileset Controller {} after {}", + parent->profilesets->current_profileset_name(), name(), + profileset_controller::call_point_string( call_point ) ); + if ( call_point == POST_ITER ) + msg += std::to_string( sim->current_iteration ); + + if ( const auto r = reason(); !r.empty() ) + msg += fmt::format( " because {}.", r ); + else + msg += "."; + + return msg; +} + +void profileset_controller_t::set_exit_reason( exit_reason_t&& exit_reason ) +{ + auto& pcd = parent->profileset_controller_data; + assert( pcd.size() > id ); + pcd[ id ].data->exit_reasons.emplace_back( std::move( exit_reason ) ); +} + +namespace +{ +// how to do this with reference wrapper instead of template? +template +void report_html_table( + std::ostream& out, std::vector keys, const std::deque& data, + T ref, std::function& )> cond = []( const auto& ) { + return true; + } ) +{ + out << "\n" + << ""; + bool first = true; + for ( const auto& key : keys ) + { + out << fmt::format( ""; + first = false; + } + out << "\n"; + for ( const auto& datum_wrapper : data ) + if ( const auto& datum = datum_wrapper.data; datum && cond( datum ) ) + std::invoke( ref, datum, out ); + out << "
", first ? "left" : "center" ) << key << "
"; +} +} // namespace + +namespace profileset_controller +{ +const std::string call_point_string( call_point_e call_point ) +{ + switch ( call_point ) + { + case POST_INIT: + return "simulation initialization"; + case POST_ITER: + return "iteration"; + default: + assert( false ); + return "no matching call point"; + } +} + +void report_html( const sim_t& sim, std::ostream& out ) +{ + if ( sim.profileset_controller_data.empty() ) + return; + + out << "

Profileset Sim Control

\n"; + out << "
\n"; + + out << "
Profileset Controllers\n"; + report_html_table( out, { "Type", "Count", "Options" }, sim.profileset_controller_data, + &profileset_controller_data_t::report_html_options ); + out << "
\n"; + + // report source, location, and reason of interrupt for + // all registered profileset profileset controllers + bool has_culled_profileset = range::any_of( sim.profileset_controller_data, + []( const auto& datum ) { return datum.data->exit_reasons.size(); } ); + + if ( has_culled_profileset ) + { + out << "
Cancelled Profilesets\n"; + report_html_table( out, { "Type", "Profileset Name", "Cancellation Point", "Reason" }, + sim.profileset_controller_data, &profileset_controller_data_t::report_html_profileset, + []( const auto& datum ) { return datum->exit_reasons.size(); } ); + out << "
\n"; + } + out << "
"; +} + +void report_json( const sim_t& sim, js::JsonOutput& output ) +{ + if ( sim.profileset_controller_data.empty() ) + return; + + auto root = output[ "profileset_controller" ]; + + auto exits = root[ "cancelled_profilesets" ].make_array(); + for ( const auto& datum_wrapper : sim.profileset_controller_data ) + if ( const auto& datum = datum_wrapper.data; datum ) + datum->report_json_profileset( exits ); + + auto controllers = root[ "enabled_controllers" ].make_array(); + for ( const auto& datum_wrapper : sim.profileset_controller_data ) + if ( const auto& datum = datum_wrapper.data; datum ) + datum->report_json_options( controllers ); +} +} // namespace profileset_controller + +bool min_player_stat_t::evaluate_post_init() +{ + return true; +} + +const std::string min_player_stat_t::reason() const +{ + return fmt::format( "player {} does not exceed {} rating for {}", target_player->name(), min_rating, + util::stat_type_string( rating ) ); +} + +bool set_bonus_enabled_t::evaluate_post_init() +{ + if ( target_player ) + return target_player->sets->has_set_bonus( target_player->specialization(), tier, count ); + return true; +} + +const std::string set_bonus_enabled_t::reason() const +{ + // no to string for set bonus tier or count... + // that should definitely exist :) + auto set_bonuses = item_set_bonus_t::data( target_player ? target_player->dbc->ptr : false ); + std::string tier_name{}; + for ( const auto& set_bonus : set_bonuses ) + if ( set_bonus.enum_id == static_cast( tier ) ) + tier_name = set_bonus.tier; + return fmt::format( "player {} does not have set {} {}pc active", target_player->name(), tier_name, + static_cast( count + 1 ) ); +} + +void set_bonus_enabled_t::create_options() +{ + add_option( opt_func( "tier", [ this ]( sim_t*, util::string_view, util::string_view value ) { + auto set_bonuses = item_set_bonus_t::data( target_player ? target_player->dbc->ptr : false ); + for ( const auto& set_bonus : set_bonuses ) + { + if ( util::str_compare_ci( set_bonus.tier, value ) ) + { + this->tier = static_cast( set_bonus.enum_id ); + return true; + } + } + return false; + } ) ); + add_option( opt_func( "pc", [ this ]( sim_t*, util::string_view, util::string_view value ) { + auto bonus_value = util::to_unsigned( value ); + if ( bonus_value > B_MAX ) + return false; + this->count = static_cast( bonus_value - 1 ); + return true; + } ) ); + add_option( opt_func( "player", [ this ]( sim_t* sim, util::string_view, util::string_view value ) { + for ( auto& player : sim->player_list ) + { + if ( util::str_compare_ci( player->name(), value ) ) + { + this->target_player = player; + return true; + } + } + return false; + } ) ); +} diff --git a/engine/sim/profileset_control.hpp b/engine/sim/profileset_control.hpp new file mode 100644 index 00000000000..6bf7fb337ef --- /dev/null +++ b/engine/sim/profileset_control.hpp @@ -0,0 +1,182 @@ +#pragma once + +#include "player/player.hpp" +#include "player/rating.hpp" +#include "sc_enums.hpp" +#include "util/generic.hpp" + +#include +#include +#include + +struct sim_t; + +enum call_point_e +{ + CALL_POINT_NONE, + POST_INIT, + POST_ITER +}; + +template +struct data_wrapper_t +{ +private: + std::scoped_lock lock; + +public: + const T& data; + + data_wrapper_t( const T& data, std::recursive_mutex& m ) : lock( m ), data( data ) + { + } +}; + +struct exit_reason_t +{ + const std::string profileset_name; + const call_point_e exit_point; + const std::string exit_reason; +}; + +struct profileset_controller_data_t : private noncopyable +{ + const std::string key; + std::string_view options; + std::vector exit_reasons; + + profileset_controller_data_t( std::string_view, std::string_view ); + virtual ~profileset_controller_data_t() = default; + + virtual void report_html_options( std::ostream& ) const; + virtual void report_html_profileset( std::ostream& ) const; + virtual void report_json_options( js::JsonOutput& ) const; + virtual void report_json_profileset( js::JsonOutput& ) const; +}; + +struct profileset_controller_data_wrapper_t : private noncopyable +{ +private: + static std::atomic_uint id_generator; + +public: + std::recursive_mutex mutex; + + const unsigned int id; + const std::string key; + std::string_view options; + std::unique_ptr data; + + profileset_controller_data_wrapper_t( std::string, std::string_view ); + + void construct_controller( sim_t* ); +}; + +struct profileset_controller_t : private noncopyable +{ + using controller_factory_t = std::function( sim_t*, unsigned int )>; + using data_factory_t = + std::function( std::string_view, std::string_view )>; + using factory_fn_pair_t = std::pair; + +protected: + friend profileset_controller_data_wrapper_t; + static std::unordered_map factory; + +public: + static bool register_controller( std::string, factory_fn_pair_t&& ); + static bool controller_exists( std::string ); + + using data_t = profileset_controller_data_t; + static void evaluate( sim_t* sim, call_point_e call_point ); + + sim_t* parent; + sim_t* sim; + const unsigned int id; + std::vector> options; + + profileset_controller_t( sim_t*, unsigned int ); + virtual ~profileset_controller_t() = default; + + const std::string message( call_point_e ); + void add_option( std::unique_ptr&& ); + + virtual const std::string name() const = 0; + virtual const std::string reason() const = 0; + virtual void create_options() + { + } + virtual bool evaluate_post_init() + { + return true; + } + virtual bool evaluate_post_iter() + { + return true; + } + +protected: + template + data_wrapper_t get_data(); + template + void set_data( T&& data ); + void set_exit_reason( exit_reason_t&& ); +}; + +namespace profileset_controller +{ +const std::string call_point_string( call_point_e call_point ); +void report_html( const sim_t&, std::ostream& ); +void report_json( const sim_t&, js::JsonOutput& output ); + +template +profileset_controller_t::factory_fn_pair_t create_fn_pair() +{ + return { + []( sim_t* sim, unsigned int id ) { return std::make_unique( sim, id ); }, + []( std::string_view key, std::string_view options ) { return std::make_unique( key, options ); } }; +} +}; // namespace profileset_controller + +struct min_player_stat_t : profileset_controller_t +{ + /* + * This sim controller doesn't work, as at all controller evaluation points + * only have base rating provided by the class/spec. If gear stats were to + * be set once on actor init and preserved between iterations, this would be + * fixed. + */ + using data_t = profileset_controller_data_t; + + player_t* target_player; + stat_e rating; + double min_rating; + + const std::string name() const override + { + return "min_player_stat"; + } + bool evaluate_post_init() override; + const std::string reason() const override; +}; + +struct set_bonus_enabled_t : profileset_controller_t +{ + using data_t = profileset_controller_data_t; + + player_t* target_player; + set_bonus_type_e tier; + set_bonus_e count; + + set_bonus_enabled_t( sim_t* sim, unsigned int id ) : profileset_controller_t( sim, id ) + { + } + + const std::string name() const override + { + return "set_bonus_enabled"; + } + bool evaluate_post_init() override; + const std::string reason() const override; + void create_options() override; +}; diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index 31b8fbdd0bf..98b1b6bc594 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -1552,6 +1552,8 @@ sim_t::sim_t() count_overheal_as_heal( false ), scaling_normalized( 1.0 ), merge_enemy_priority_dmg( false ), + profileset_controller(), + profileset_controller_data(), // Multi-Threading threads( 0 ), thread_index( 0 ), @@ -2886,6 +2888,23 @@ void sim_t::init() } init_mutex.lock(); + + // TODO: convert to new init registry system + if ( !parent && !profileset_controller_options.empty() && !profileset_map.empty() ) + { + for ( const auto& [ key, values ] : profileset_controller_options ) + { + if ( profileset_controller_t::controller_exists( key ) ) + for ( const auto& value : values ) + profileset_controller_data.emplace_back( key, value ); + else + throw sc_invalid_sim_argument( fmt::format( "Unknown profileset controller option with name '{}'.", key ) ); + } + } + if ( parent && profileset_enabled ) + for ( auto& profileset_controller_datum : parent->profileset_controller_data ) + profileset_controller_datum.construct_controller( this ); + initialized = true; init_mutex.unlock(); @@ -3110,6 +3129,8 @@ bool sim_t::iterate() progress_bar.init(); + profileset_controller_t::evaluate( this, POST_INIT ); + try { activate_actors(); @@ -3127,6 +3148,8 @@ bool sim_t::iterate() progress_bar.output( false ); } + profileset_controller_t::evaluate( this, POST_ITER ); + do_pause(); auto old_active = current_index; if ( !canceled ) @@ -3947,6 +3970,8 @@ void sim_t::create_options() add_option( opt_bool( "merge_enemy_priority_dmg", merge_enemy_priority_dmg ) ); add_option( opt_int( "decorated_tooltips", decorated_tooltips ) ); add_option( opt_uint( "spell_query_wrap", spell_query_wrap, 50, UINT_MAX ) ); + // Sim Controller Options + add_option( opt_map_list( "profileset_controller.", profileset_controller_options ) ); // Charts add_option( opt_bool( "chart_show_relative_difference", chart_show_relative_difference ) ); add_option( opt_bool( "chart_show_relative_difference_percent", chart_show_relative_difference_percent ) ); diff --git a/engine/sim/sim.hpp b/engine/sim/sim.hpp index ef5e29e25e5..b7e3ea4dfed 100644 --- a/engine/sim/sim.hpp +++ b/engine/sim/sim.hpp @@ -9,6 +9,7 @@ #include "event_manager.hpp" #include "player/gear_stats.hpp" #include "progress_bar.hpp" +#include "profileset_control.hpp" #include "sim_ostream.hpp" #include "sim/option.hpp" #include "util/concurrency.hpp" @@ -17,8 +18,10 @@ #include "util/util.hpp" #include "util/vector_with_callback.hpp" +#include #include #include +#include #include struct actor_target_data_t; @@ -581,6 +584,11 @@ struct sim_t : private sc_thread_t double scaling_normalized; bool merge_enemy_priority_dmg; + // sim control + std::vector> profileset_controller; + std::deque profileset_controller_data; + opts::map_list_t profileset_controller_options; + // Multi-Threading mutex_t merge_mutex; int threads; @@ -833,3 +841,20 @@ struct sim_t : private sc_thread_t void disable_debug_seed(); bool requires_cleanup() const; }; + +template +data_wrapper_t profileset_controller_t::get_data() +{ + auto& pcd = parent->profileset_controller_data; + assert( pcd.size() > id ); + auto& data = pcd[ id ]; + return { *data.data.get(), data.mutex }; +} + +template +void profileset_controller_t::set_data( T&& data ) +{ + auto& pcd = parent->profileset_controller_data; + assert( pcd.size() > id ); + pcd[ id ].data = std::make_unique( data ); +} diff --git a/source_files/QT_engine.pri b/source_files/QT_engine.pri index eb2d8b61f9a..67ceaa40024 100644 --- a/source_files/QT_engine.pri +++ b/source_files/QT_engine.pri @@ -159,6 +159,7 @@ HEADERS += engine/sim/plot.hpp HEADERS += engine/sim/proc.hpp HEADERS += engine/sim/proc_rng.hpp HEADERS += engine/sim/profileset.hpp +HEADERS += engine/sim/profileset_control.hpp HEADERS += engine/sim/progress_bar.hpp HEADERS += engine/sim/raid_event.hpp HEADERS += engine/sim/reforge_plot.hpp @@ -350,6 +351,7 @@ SOURCES += engine/sim/plot.cpp SOURCES += engine/sim/proc.cpp SOURCES += engine/sim/proc_rng.cpp SOURCES += engine/sim/profileset.cpp +SOURCES += engine/sim/profileset_control.cpp SOURCES += engine/sim/progress_bar.cpp SOURCES += engine/sim/raid_event.cpp SOURCES += engine/sim/reforge_plot.cpp diff --git a/source_files/VS_engine.props b/source_files/VS_engine.props index 13c40bd2177..7cf4f81370b 100644 --- a/source_files/VS_engine.props +++ b/source_files/VS_engine.props @@ -163,6 +163,7 @@ To change the list of source files run synchronize.py + @@ -353,6 +354,7 @@ To change the list of source files run synchronize.py + diff --git a/source_files/cmake_engine.txt b/source_files/cmake_engine.txt index cccd4bfe0e8..7cc4e9d8867 100644 --- a/source_files/cmake_engine.txt +++ b/source_files/cmake_engine.txt @@ -157,6 +157,7 @@ sim/plot.hpp sim/proc.hpp sim/proc_rng.hpp sim/profileset.hpp +sim/profileset_control.hpp sim/progress_bar.hpp sim/raid_event.hpp sim/reforge_plot.hpp @@ -347,6 +348,7 @@ sim/plot.cpp sim/proc.cpp sim/proc_rng.cpp sim/profileset.cpp +sim/profileset_control.cpp sim/progress_bar.cpp sim/raid_event.cpp sim/reforge_plot.cpp diff --git a/source_files/engine_make b/source_files/engine_make index 9b0871db0a9..415f966e10a 100644 --- a/source_files/engine_make +++ b/source_files/engine_make @@ -158,6 +158,7 @@ SRC += \ sim$(PATHSEP)proc.cpp \ sim$(PATHSEP)proc_rng.cpp \ sim$(PATHSEP)profileset.cpp \ + sim$(PATHSEP)profileset_control.cpp \ sim$(PATHSEP)progress_bar.cpp \ sim$(PATHSEP)raid_event.cpp \ sim$(PATHSEP)reforge_plot.cpp \