From 5724fb9a12c92ce7f4823bcb30463a33b75212ab Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 22 Mar 2026 21:37:43 -0700 Subject: [PATCH] add $allrare debug command; closes #739 --- README.md | 1 + src/ChatCommands.cc | 9 +++++++++ src/Client.hh | 1 + src/ItemCreator.cc | 39 +++++++++++++++++++-------------------- src/ItemCreator.hh | 13 +++++++------ src/Map.cc | 1 + src/ProxyCommands.cc | 6 ++++-- src/ReceiveSubcommands.cc | 11 ++++++----- 8 files changed, 48 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 2808ec2c..c11917dd 100644 --- a/README.md +++ b/README.md @@ -603,6 +603,7 @@ Some commands only work for clients not in proxy sessions. The chat commands are * `$qsyncall `: Set a quest register's value for everyone in the game. `` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `` is parsed as a floating-point value instead of as an integer. * `$swset [floor] ` and `$swclear [floor] `: Set or clear a switch flag. If floor is not given, sets or clears the flag on your current floor. * `$swsetall`: Set all switch flags on your current floor. This unlocks all doors, disables all laser fences, triggers all light/poison switches, etc. + * `$allrare`: Make all enemies and boxes drop their rare items every time. * `$gc` (non-proxy only): Send your own Guild Card to yourself. * `$sc `: Send a command to yourself. * `$scp `: Send a protected command to yourself. diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 3469eafe..d393b391 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -2227,6 +2227,15 @@ ChatCommandDefinition cc_fastkill( } co_return; }); +ChatCommandDefinition cc_allrare( + {"$allrare"}, + +[](const Args& a) -> asio::awaitable { + a.check_debug_enabled(); + a.c->toggle_flag(Client::Flag::ALL_RARES_ENABLED); + send_text_message_fmt( + a.c, "$C6All-rares {}", a.c->check_flag(Client::Flag::ALL_RARES_ENABLED) ? "enabled" : "disabled"); + co_return; + }); ChatCommandDefinition cc_rand( {"$rand"}, diff --git a/src/Client.hh b/src/Client.hh index 5e7e86e5..5ce7030a 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -78,6 +78,7 @@ public: INFINITE_HP_ENABLED = 0x0000000040000000, INFINITE_TP_ENABLED = 0x0000000080000000, FAST_KILLS_ENABLED = 0x0000000100000000, + ALL_RARES_ENABLED = 0x0000100000000000, DEBUG_ENABLED = 0x0000000200000000, ITEM_DROP_NOTIFICATIONS_1 = 0x0000000400000000, ITEM_DROP_NOTIFICATIONS_2 = 0x0000000800000000, diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index 58bee51b..070edfe4 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -136,13 +136,13 @@ uint8_t ItemCreator::table_index_for_area(uint8_t area) const { return data[area]; } -ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) { +ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area, bool force_rare) { try { uint8_t table_index = this->table_index_for_area(area); this->log.info_f("Box drop checks for area {:02X} (table index {:02X})", area, table_index); DropResult res; - res.item = this->check_rare_specs_and_create_rare_box_item(area); + res.item = this->check_rare_specs_and_create_rare_box_item(area, force_rare); if (!res.item.empty()) { res.is_from_rare_table = true; } else { @@ -188,7 +188,7 @@ ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) { } } -ItemCreator::DropResult ItemCreator::on_monster_item_drop(EnemyType enemy_type, uint8_t area) { +ItemCreator::DropResult ItemCreator::on_monster_item_drop(EnemyType enemy_type, uint8_t area, bool force_rare) { try { // Note: The original implementation has a bounds check for enemy_type here, because it uses rt_index instead // if (enemy_type >= NUM_RT_INDEXES_V4) { @@ -205,16 +205,18 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(EnemyType enemy_type, this->log.info_f("No drop probability is set for this enemy type"); return DropResult(); } - uint8_t drop_sample = this->rand_int(100); - if (drop_sample >= type_drop_prob) { - this->log.info_f("Drop not chosen ({} >= {})", drop_sample, type_drop_prob); - return DropResult(); - } else { - this->log.info_f("Drop chosen ({} < {})", drop_sample, type_drop_prob); + if (!force_rare) { + uint8_t drop_sample = this->rand_int(100); + if (drop_sample >= type_drop_prob) { + this->log.info_f("Drop not chosen ({} >= {})", drop_sample, type_drop_prob); + return DropResult(); + } else { + this->log.info_f("Drop chosen ({} < {})", drop_sample, type_drop_prob); + } } DropResult res; - res.item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area); + res.item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area, force_rare); if (!res.item.empty()) { res.is_from_rare_table = true; } else { @@ -284,7 +286,7 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(EnemyType enemy_type, } } -ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area) { +ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area, bool force_rare) { ItemData item; if (!this->are_rare_drops_allowed()) { return item; @@ -294,7 +296,7 @@ ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area) { Episode episode = episode_for_area(area); auto rare_specs = this->rare_item_set->get_box_specs(this->mode, episode, this->difficulty, this->section_id, table_index); for (const auto& spec : rare_specs) { - item = this->check_rate_and_create_rare_item(spec, area); + item = this->check_rate_and_create_rare_item(spec, area, force_rare); if (!item.empty()) { if (this->log.should_log(phosg::LogLevel::L_INFO)) { auto hex = spec.data.hex(); @@ -338,7 +340,7 @@ bool ItemCreator::should_allow_meseta_drops() const { return (this->mode != GameMode::CHALLENGE); } -ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(EnemyType enemy_type, uint8_t area) { +ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(EnemyType enemy_type, uint8_t area, bool force_rare) { // Note: The original implementation has a bounds check for enemy_type here, since it uses rt_index instead. // if ((enemy_type <= 0) || (enemy_type >= NUM_RT_INDEXES_V4)) return ItemData{}; if (!this->are_rare_drops_allowed()) { @@ -353,7 +355,7 @@ ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(EnemyType enemy this->mode, episode, this->difficulty, this->section_id, enemy_type); ItemData item; for (const auto& spec : rare_specs) { - item = this->check_rate_and_create_rare_item(spec, area); + item = this->check_rate_and_create_rare_item(spec, area, force_rare); if (!item.empty()) { if (this->log.should_log(phosg::LogLevel::L_INFO)) { auto hex = spec.data.hex(); @@ -369,14 +371,11 @@ ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(EnemyType enemy return item; } -ItemData ItemCreator::check_rate_and_create_rare_item(const RareItemSet::ExpandedDrop& drop, uint8_t area) { - if (drop.probability == 0) { - return ItemData(); - } - +ItemData ItemCreator::check_rate_and_create_rare_item( + const RareItemSet::ExpandedDrop& drop, uint8_t area, bool force_rare) { // Note: The original code uses 0xFFFFFFFF as the maximum here. We use 0x100000000 instead, which makes all rare // items SLIGHTLY more rare. - if (this->rand_int(0x100000000) >= drop.probability) { + if (!force_rare && ((drop.probability == 0) || (this->rand_int(0x100000000) >= drop.probability))) { return ItemData(); } diff --git a/src/ItemCreator.hh b/src/ItemCreator.hh index bc621f5e..321f852e 100644 --- a/src/ItemCreator.hh +++ b/src/ItemCreator.hh @@ -37,10 +37,11 @@ public: bool is_from_rare_table = false; }; - DropResult on_monster_item_drop(EnemyType enemy_type, uint8_t area); - DropResult on_box_item_drop(uint8_t area); + DropResult on_monster_item_drop(EnemyType enemy_type, uint8_t area, bool force_rare); + DropResult on_box_item_drop(uint8_t area, bool force_rare); // Note: param3-6 refer to the corresponding fields of the object definition - DropResult on_specialized_box_item_drop(uint8_t area, float param3, uint32_t param4, uint32_t param5, uint32_t param6); + DropResult on_specialized_box_item_drop( + uint8_t area, float param3, uint32_t param4, uint32_t param5, uint32_t param6); ItemData base_item_for_specialized_box(uint32_t param4, uint32_t param5, uint32_t param6) const; std::vector generate_armor_shop_contents(Episode episode, size_t player_level); @@ -120,9 +121,9 @@ private: bool should_allow_meseta_drops() const; - ItemData check_rare_spec_and_create_rare_enemy_item(EnemyType enemy_type, uint8_t area); - ItemData check_rare_specs_and_create_rare_box_item(uint8_t area); - ItemData check_rate_and_create_rare_item(const RareItemSet::ExpandedDrop& drop, uint8_t area); + ItemData check_rare_spec_and_create_rare_enemy_item(EnemyType enemy_type, uint8_t area, bool force_rare); + ItemData check_rare_specs_and_create_rare_box_item(uint8_t area, bool force_rare); + ItemData check_rate_and_create_rare_item(const RareItemSet::ExpandedDrop& drop, uint8_t area, bool force_rare); void generate_rare_weapon_bonuses(ItemData& item, Episode episode, uint32_t random_sample); void deduplicate_weapon_bonuses(ItemData& item) const; diff --git a/src/Map.cc b/src/Map.cc index f2503723..f27bce93 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -3484,6 +3484,7 @@ const array MapFile::RAND_ENEMY_BASE_TYPES = { 0x00DD, /* TObjEneDolmOlm */ 0x00DE, /* TObjEneMorfos */ 0x00DF, /* TObjEneRecobox */ + // This is not a bug; 0x00E0 really does appear twice in this list on the client. 0x00E0, /* TObjEneMe3SinowZoaReal/TObjEneEpsilonBody (depending on area) */ 0x00E0, /* TObjEneMe3SinowZoaReal/TObjEneEpsilonBody (depending on area) */ 0x00E1, /* TObjEneIllGill */ diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index a6ecc76c..e739f9bb 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -960,7 +960,8 @@ static asio::awaitable SC_6x60_6xA2(shared_ptr c, Channel if (rec.obj_st) { if (rec.ignore_def) { c->log.info_f("Creating item from box {:04X} (area {:02X})", cmd.entity_index, cmd.effective_area); - res = c->proxy_session->item_creator->on_box_item_drop(cmd.effective_area); + res = c->proxy_session->item_creator->on_box_item_drop( + cmd.effective_area, c->check_flag(Client::Flag::ALL_RARES_ENABLED)); } else { c->log.info_f("Creating item from box {:04X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})", cmd.entity_index, cmd.effective_area, @@ -970,7 +971,8 @@ static asio::awaitable SC_6x60_6xA2(shared_ptr c, Channel } } else { c->log.info_f("Creating item from enemy {:04X} (area {:02X})", cmd.entity_index, cmd.effective_area); - res = c->proxy_session->item_creator->on_monster_item_drop(rec.effective_enemy_type, cmd.effective_area); + res = c->proxy_session->item_creator->on_monster_item_drop( + rec.effective_enemy_type, cmd.effective_area, c->check_flag(Client::Flag::ALL_RARES_ENABLED)); } if (res.item.empty()) { diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 341bac53..2fb601a2 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2964,12 +2964,13 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S } if (rec.should_drop) { - auto generate_item = [&]() -> ItemCreator::DropResult { + auto generate_item_for_client = [&](std::shared_ptr c) -> ItemCreator::DropResult { + bool force_rare = c->check_flag(Client::Flag::ALL_RARES_ENABLED); if (rec.obj_st) { if (rec.ignore_def) { l->log.info_f("Creating item from box {:04X} => K-{:03X} (area {:02X})", cmd.entity_index, rec.obj_st->k_id, cmd.effective_area); - return l->item_creator->on_box_item_drop(cmd.effective_area); + return l->item_creator->on_box_item_drop(cmd.effective_area, force_rare); } else { l->log.info_f( "Creating item from box {:04X} => K-{:03X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})", @@ -2980,7 +2981,7 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S } else if (rec.target_ene_st) { l->log.info_f("Creating item from enemy {:04X} => E-{:03X} (area {:02X})", cmd.entity_index, rec.target_ene_st->e_id, cmd.effective_area); - return l->item_creator->on_monster_item_drop(rec.effective_enemy_type, cmd.effective_area); + return l->item_creator->on_monster_item_drop(rec.effective_enemy_type, cmd.effective_area, force_rare); } else { throw runtime_error("neither object nor enemy were present"); } @@ -3002,7 +3003,7 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S throw logic_error("unhandled simple drop mode"); case ServerDropMode::SERVER_SHARED: case ServerDropMode::SERVER_DUPLICATE: { - auto res = generate_item(); + auto res = generate_item_for_client(c); if (res.item.empty()) { l->log.info_f("No item was created"); } else { @@ -3040,7 +3041,7 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S case ServerDropMode::SERVER_PRIVATE: { for (const auto& lc : l->clients) { if (lc && (rec.obj_st || (lc->floor == cmd.floor))) { - auto res = generate_item(); + auto res = generate_item_for_client(lc); if (res.item.empty()) { l->log.info_f("No item was created for {}", lc->channel->name); } else {