add $allrare debug command; closes #739

This commit is contained in:
Martin Michelsen
2026-03-22 21:37:43 -07:00
parent 983753f840
commit 5724fb9a12
8 changed files with 48 additions and 33 deletions
+1
View File
@@ -603,6 +603,7 @@ Some commands only work for clients not in proxy sessions. The chat commands are
* `$qsyncall <reg-num> <value>`: Set a quest register's value for everyone in the game. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer. * `$qsyncall <reg-num> <value>`: Set a quest register's value for everyone in the game. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
* `$swset [floor] <flag-num>` and `$swclear [floor] <flag-num>`: Set or clear a switch flag. If floor is not given, sets or clears the flag on your current floor. * `$swset [floor] <flag-num>` and `$swclear [floor] <flag-num>`: 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. * `$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. * `$gc` (non-proxy only): Send your own Guild Card to yourself.
* `$sc <data>`: Send a command to yourself. * `$sc <data>`: Send a command to yourself.
* `$scp <data>`: Send a protected command to yourself. * `$scp <data>`: Send a protected command to yourself.
+9
View File
@@ -2227,6 +2227,15 @@ ChatCommandDefinition cc_fastkill(
} }
co_return; co_return;
}); });
ChatCommandDefinition cc_allrare(
{"$allrare"},
+[](const Args& a) -> asio::awaitable<void> {
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( ChatCommandDefinition cc_rand(
{"$rand"}, {"$rand"},
+1
View File
@@ -78,6 +78,7 @@ public:
INFINITE_HP_ENABLED = 0x0000000040000000, INFINITE_HP_ENABLED = 0x0000000040000000,
INFINITE_TP_ENABLED = 0x0000000080000000, INFINITE_TP_ENABLED = 0x0000000080000000,
FAST_KILLS_ENABLED = 0x0000000100000000, FAST_KILLS_ENABLED = 0x0000000100000000,
ALL_RARES_ENABLED = 0x0000100000000000,
DEBUG_ENABLED = 0x0000000200000000, DEBUG_ENABLED = 0x0000000200000000,
ITEM_DROP_NOTIFICATIONS_1 = 0x0000000400000000, ITEM_DROP_NOTIFICATIONS_1 = 0x0000000400000000,
ITEM_DROP_NOTIFICATIONS_2 = 0x0000000800000000, ITEM_DROP_NOTIFICATIONS_2 = 0x0000000800000000,
+13 -14
View File
@@ -136,13 +136,13 @@ uint8_t ItemCreator::table_index_for_area(uint8_t area) const {
return data[area]; 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 { try {
uint8_t table_index = this->table_index_for_area(area); 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); this->log.info_f("Box drop checks for area {:02X} (table index {:02X})", area, table_index);
DropResult res; 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()) { if (!res.item.empty()) {
res.is_from_rare_table = true; res.is_from_rare_table = true;
} else { } 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 { try {
// Note: The original implementation has a bounds check for enemy_type here, because it uses rt_index instead // 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) { // if (enemy_type >= NUM_RT_INDEXES_V4) {
@@ -205,6 +205,7 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(EnemyType enemy_type,
this->log.info_f("No drop probability is set for this enemy type"); this->log.info_f("No drop probability is set for this enemy type");
return DropResult(); return DropResult();
} }
if (!force_rare) {
uint8_t drop_sample = this->rand_int(100); uint8_t drop_sample = this->rand_int(100);
if (drop_sample >= type_drop_prob) { if (drop_sample >= type_drop_prob) {
this->log.info_f("Drop not chosen ({} >= {})", drop_sample, type_drop_prob); this->log.info_f("Drop not chosen ({} >= {})", drop_sample, type_drop_prob);
@@ -212,9 +213,10 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(EnemyType enemy_type,
} else { } else {
this->log.info_f("Drop chosen ({} < {})", drop_sample, type_drop_prob); this->log.info_f("Drop chosen ({} < {})", drop_sample, type_drop_prob);
} }
}
DropResult res; 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()) { if (!res.item.empty()) {
res.is_from_rare_table = true; res.is_from_rare_table = true;
} else { } 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; ItemData item;
if (!this->are_rare_drops_allowed()) { if (!this->are_rare_drops_allowed()) {
return item; 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); 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); 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) { 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 (!item.empty()) {
if (this->log.should_log(phosg::LogLevel::L_INFO)) { if (this->log.should_log(phosg::LogLevel::L_INFO)) {
auto hex = spec.data.hex(); auto hex = spec.data.hex();
@@ -338,7 +340,7 @@ bool ItemCreator::should_allow_meseta_drops() const {
return (this->mode != GameMode::CHALLENGE); 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. // 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 ((enemy_type <= 0) || (enemy_type >= NUM_RT_INDEXES_V4)) return ItemData{};
if (!this->are_rare_drops_allowed()) { 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); this->mode, episode, this->difficulty, this->section_id, enemy_type);
ItemData item; ItemData item;
for (const auto& spec : rare_specs) { 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 (!item.empty()) {
if (this->log.should_log(phosg::LogLevel::L_INFO)) { if (this->log.should_log(phosg::LogLevel::L_INFO)) {
auto hex = spec.data.hex(); auto hex = spec.data.hex();
@@ -369,14 +371,11 @@ ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(EnemyType enemy
return item; return item;
} }
ItemData ItemCreator::check_rate_and_create_rare_item(const RareItemSet::ExpandedDrop& drop, uint8_t area) { ItemData ItemCreator::check_rate_and_create_rare_item(
if (drop.probability == 0) { const RareItemSet::ExpandedDrop& drop, uint8_t area, bool force_rare) {
return ItemData();
}
// Note: The original code uses 0xFFFFFFFF as the maximum here. We use 0x100000000 instead, which makes all rare // Note: The original code uses 0xFFFFFFFF as the maximum here. We use 0x100000000 instead, which makes all rare
// items SLIGHTLY more 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(); return ItemData();
} }
+7 -6
View File
@@ -37,10 +37,11 @@ public:
bool is_from_rare_table = false; bool is_from_rare_table = false;
}; };
DropResult on_monster_item_drop(EnemyType enemy_type, 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); DropResult on_box_item_drop(uint8_t area, bool force_rare);
// Note: param3-6 refer to the corresponding fields of the object definition // 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; ItemData base_item_for_specialized_box(uint32_t param4, uint32_t param5, uint32_t param6) const;
std::vector<ItemData> generate_armor_shop_contents(Episode episode, size_t player_level); std::vector<ItemData> generate_armor_shop_contents(Episode episode, size_t player_level);
@@ -120,9 +121,9 @@ private:
bool should_allow_meseta_drops() const; bool should_allow_meseta_drops() const;
ItemData check_rare_spec_and_create_rare_enemy_item(EnemyType enemy_type, 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); 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); 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 generate_rare_weapon_bonuses(ItemData& item, Episode episode, uint32_t random_sample);
void deduplicate_weapon_bonuses(ItemData& item) const; void deduplicate_weapon_bonuses(ItemData& item) const;
+1
View File
@@ -3484,6 +3484,7 @@ const array<uint32_t, 41> MapFile::RAND_ENEMY_BASE_TYPES = {
0x00DD, /* TObjEneDolmOlm */ 0x00DD, /* TObjEneDolmOlm */
0x00DE, /* TObjEneMorfos */ 0x00DE, /* TObjEneMorfos */
0x00DF, /* TObjEneRecobox */ 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) */
0x00E0, /* TObjEneMe3SinowZoaReal/TObjEneEpsilonBody (depending on area) */ 0x00E0, /* TObjEneMe3SinowZoaReal/TObjEneEpsilonBody (depending on area) */
0x00E1, /* TObjEneIllGill */ 0x00E1, /* TObjEneIllGill */
+4 -2
View File
@@ -960,7 +960,8 @@ static asio::awaitable<HandlerResult> SC_6x60_6xA2(shared_ptr<Client> c, Channel
if (rec.obj_st) { if (rec.obj_st) {
if (rec.ignore_def) { if (rec.ignore_def) {
c->log.info_f("Creating item from box {:04X} (area {:02X})", cmd.entity_index, cmd.effective_area); 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 { } else {
c->log.info_f("Creating item from box {:04X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})", c->log.info_f("Creating item from box {:04X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})",
cmd.entity_index, cmd.effective_area, cmd.entity_index, cmd.effective_area,
@@ -970,7 +971,8 @@ static asio::awaitable<HandlerResult> SC_6x60_6xA2(shared_ptr<Client> c, Channel
} }
} else { } else {
c->log.info_f("Creating item from enemy {:04X} (area {:02X})", cmd.entity_index, cmd.effective_area); 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()) { if (res.item.empty()) {
+6 -5
View File
@@ -2964,12 +2964,13 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
} }
if (rec.should_drop) { if (rec.should_drop) {
auto generate_item = [&]() -> ItemCreator::DropResult { auto generate_item_for_client = [&](std::shared_ptr<Client> c) -> ItemCreator::DropResult {
bool force_rare = c->check_flag(Client::Flag::ALL_RARES_ENABLED);
if (rec.obj_st) { if (rec.obj_st) {
if (rec.ignore_def) { if (rec.ignore_def) {
l->log.info_f("Creating item from box {:04X} => K-{:03X} (area {:02X})", l->log.info_f("Creating item from box {:04X} => K-{:03X} (area {:02X})",
cmd.entity_index, rec.obj_st->k_id, cmd.effective_area); 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 { } else {
l->log.info_f( l->log.info_f(
"Creating item from box {:04X} => K-{:03X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})", "Creating item from box {:04X} => K-{:03X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})",
@@ -2980,7 +2981,7 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
} else if (rec.target_ene_st) { } else if (rec.target_ene_st) {
l->log.info_f("Creating item from enemy {:04X} => E-{:03X} (area {:02X})", 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); 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 { } else {
throw runtime_error("neither object nor enemy were present"); throw runtime_error("neither object nor enemy were present");
} }
@@ -3002,7 +3003,7 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
throw logic_error("unhandled simple drop mode"); throw logic_error("unhandled simple drop mode");
case ServerDropMode::SERVER_SHARED: case ServerDropMode::SERVER_SHARED:
case ServerDropMode::SERVER_DUPLICATE: { case ServerDropMode::SERVER_DUPLICATE: {
auto res = generate_item(); auto res = generate_item_for_client(c);
if (res.item.empty()) { if (res.item.empty()) {
l->log.info_f("No item was created"); l->log.info_f("No item was created");
} else { } else {
@@ -3040,7 +3041,7 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
case ServerDropMode::SERVER_PRIVATE: { case ServerDropMode::SERVER_PRIVATE: {
for (const auto& lc : l->clients) { for (const auto& lc : l->clients) {
if (lc && (rec.obj_st || (lc->floor == cmd.floor))) { if (lc && (rec.obj_st || (lc->floor == cmd.floor))) {
auto res = generate_item(); auto res = generate_item_for_client(lc);
if (res.item.empty()) { if (res.item.empty()) {
l->log.info_f("No item was created for {}", lc->channel->name); l->log.info_f("No item was created for {}", lc->channel->name);
} else { } else {