Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4a46c40
[sim_controllers] Initial implementation.
renanthera Oct 21, 2025
693448a
[sim_controllers] Fix a few implementation details and implement basi…
renanthera Oct 21, 2025
48857ae
[sim_controllers] Switch return type of `sim_controller_t::set_data<T…
renanthera Oct 21, 2025
59da2e3
[sim_controllers] Fix action list merge issue by guaranteeing `sim_t:…
renanthera Oct 25, 2025
a31a998
[sim_controllers] Tier set bonus constraint sample.
renanthera Oct 25, 2025
497a367
[sim_controllers] Rework implementation a bit to provide more default…
renanthera Oct 26, 2025
e66055e
[sim_controllers] Move the last of the implementation details out of …
renanthera Oct 26, 2025
1aec79f
[sim_controllers] Move more implementation out of `sim.[h/c]pp`. Log …
renanthera Oct 26, 2025
c654db2
[sim_controllers] `sim_controller_t::message` should probably not be …
renanthera Oct 26, 2025
e8a3d85
[sim_controllers] Replace `std::shared_ptr` with `std::unique_ptr` to…
renanthera Oct 27, 2025
2cddf0e
for real this time?
renanthera Oct 27, 2025
4331563
[sim_controllers] Initial reporting.
renanthera Oct 31, 2025
49c0c68
[sim_controllers] Fix HTML reporting and rearrange data so the lifeti…
renanthera Oct 31, 2025
f363d03
[sim_controllers] Start implementing options.
renanthera Oct 31, 2025
0d2252d
checkpoint
renanthera Dec 4, 2025
b764a10
options are working and profileset controllers can be created with no…
renanthera Dec 5, 2025
342eb20
refactor a few methods and implement html reporting
renanthera Dec 6, 2025
11c903b
implement json reporting
renanthera Dec 6, 2025
c494dd5
rename files
renanthera Dec 6, 2025
643290b
rename files
renanthera Dec 6, 2025
669aa1d
add helper function to create factory map function pairs.
renanthera Dec 7, 2025
cbf601f
clean up some notes
renanthera Jun 16, 2026
d5a13a7
Remove non-changes to monk module.
renanthera Jun 16, 2026
d63707f
revert some whitespace changes and incorrect rebase resolution
renanthera Jun 16, 2026
61bb16a
move include, clear whitespace change.
renanthera Jun 16, 2026
df99267
remove profileset control from monk files
renanthera Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions engine/report/json/report_json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() )
Expand Down
2 changes: 2 additions & 0 deletions engine/report/report_html_sim.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 << "</div>";
out << "</div>";
}
Expand Down
337 changes: 337 additions & 0 deletions engine/sim/profileset_control.cpp
Original file line number Diff line number Diff line change
@@ -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<std::string, profileset_controller_t::factory_fn_pair_t> profileset_controller_t::factory = {
{ "set_bonus_enabled", profileset_controller::create_fn_pair<set_bonus_enabled_t>() } };

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 << "<tr>"
<< "<td>" << util::encode_html( key ) << "</td>"
<< "<td class=\"center\">" << exit_reasons.size() << "</td>"
<< "<td>" << util::encode_html( options ) << "</td>"
<< "</tr>\n";
}

void profileset_controller_data_t::report_html_profileset( std::ostream& output ) const
{
bool first = true;
output << fmt::format( "<tr><td rowspan=\"{}\" class=\"dark\">{}</td>", exit_reasons.size(),
util::encode_html( key ) );
for ( const auto& [ name, call_point, reason ] : exit_reasons )
{
if ( !first )
output << "<tr>";
output << "<td class=\"center\">" << util::encode_html( name ) << "</td>"
<< "<td>" << util::encode_html( profileset_controller::call_point_string( call_point ) ) << "</td>"
<< "<td>" << util::encode_html( reason ) << "</td>"
<< "</tr>\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<bool( std::unique_ptr<profileset_controller_t>& )> cb;
switch ( call_point )
{
case POST_INIT:
cb = []( std::unique_ptr<profileset_controller_t>& sc ) { return !sc->evaluate_post_init(); };
break;
case POST_ITER:
cb = []( std::unique_ptr<profileset_controller_t>& 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_t>&& 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 <typename T>
void report_html_table(
std::ostream& out, std::vector<std::string> keys, const std::deque<profileset_controller_data_wrapper_t>& data,
T ref, std::function<bool( const std::unique_ptr<profileset_controller_data_t>& )> cond = []( const auto& ) {
return true;
} )
{
out << "<table class=\"details nowrap\" style=\"width:min-content\">\n"
<< "<tr>";
bool first = true;
for ( const auto& key : keys )
{
out << fmt::format( "<th class=\"small {}\">", first ? "left" : "center" ) << key << "</th>";
first = false;
}
out << "</tr>\n";
for ( const auto& datum_wrapper : data )
if ( const auto& datum = datum_wrapper.data; datum && cond( datum ) )
std::invoke( ref, datum, out );
out << "</table>";
}
} // 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 << "<h3 class=\"toggle\">Profileset Sim Control</h3>\n";
out << "<div class=\"toggle-content hide\">\n";

out << "<div class=\"note\" style=\"margin:6px 0;\"><strong>Profileset Controllers</strong>\n";
report_html_table( out, { "Type", "Count", "Options" }, sim.profileset_controller_data,
&profileset_controller_data_t::report_html_options );
out << "</div>\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 << "<div class=\"note\" style=\"margin:6px 0;\"><strong>Cancelled Profilesets</strong>\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 << "</div>\n";
}
out << "</div>";
}

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<unsigned int>( tier ) )
tier_name = set_bonus.tier;
return fmt::format( "player {} does not have set {} {}pc active", target_player->name(), tier_name,
static_cast<int>( 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_type_e>( 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<set_bonus_e>( 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;
} ) );
}
Loading
Loading