From 87e85932a458fe90d3e23c6b674f2b6f64c1b581 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Fri, 3 Apr 2026 18:24:41 -0700 Subject: [PATCH] switch rare drops to stacked space logic --- src/CommandFormats.hh | 4 +- src/ItemCreator.cc | 85 +++++++++---------- src/ItemCreator.hh | 9 +- src/Lobby.cc | 3 + src/ReplaySession.cc | 3 + src/ServerState.hh | 1 + ...1-DCv2-PCv2-CrossplayPrivateDrops.test.txt | 1 + tests/GC-JungleGame.test.txt | 1 + 8 files changed, 60 insertions(+), 47 deletions(-) diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 0665a511..40f53c79 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1826,13 +1826,13 @@ struct C_Login_DC_PC_GC_9D { /* 08 */ be_uint64_t hardware_id; /* 10 */ le_uint32_t sub_version = 0; /* 14 */ uint8_t is_extended = 0; // If 1, structure has extended format - /* 15 */ Language language = Language::JAPANESE; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES + /* 15 */ Language language = Language::JAPANESE; /* 16 */ parray unused3; // Always zeroes /* 18 */ pstring v1_serial_number; /* 28 */ pstring v1_access_key; /* 38 */ pstring serial_number; // On XB, this is the XBL gamertag /* 48 */ pstring access_key; // On XB, this is the XBL user ID - /* 58 */ pstring serial_number2; // On DCv2, this is the hardware ID; on XB, this is the XBL gamertag + /* 58 */ pstring serial_number2; // DCv2: hardware ID; XB: XBL gamertag /* 88 */ pstring access_key2; // On XB, this is the XBL user ID /* B8 */ pstring login_character_name; /* C8 */ diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index 070edfe4..db97abe9 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -38,6 +38,7 @@ ItemCreator::ItemCreator( shared_ptr restrictions) : log(std::format("[ItemCreator:{}/{}/{}/{}] ", phosg::name_for_enum(stack_limits->version), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id), lobby_log.min_level), logic_version(stack_limits->version), + is_legacy_replay(false), stack_limits(stack_limits), mode(mode), difficulty(difficulty), @@ -286,30 +287,48 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(EnemyType enemy_type, } } +ItemData ItemCreator::check_rare_specs_and_create_rare_item( + const std::vector& specs, uint8_t area, bool force_rare) { + if (specs.empty()) { + return ItemData(); + } + + // This logic differs from the original client logic. This logic "stacks" all rare rates into a single probability + // space, whereas the original client logic chooses a new random number for each rare spec that it checks. The + // stacking logic makes the order of specs irrelevant, whereas the original client logic means that later specs are + // actually more rare than they should be. In the original client, this only matters for boxes, because enemies could + // not have multiple specs. Also, the original code uses 0xFFFFFFFF as the maximum here; we use 0x100000000 instead, + // which makes all rare items SLIGHTLY more rare. + int64_t det = force_rare ? 0 : this->rand_int(0x100000000); + if (this->is_legacy_replay) { + // For some old tests, we waste a few replay values because they used the old (non-stacked) logic. New tests should + // not use this codepath. + for (size_t z = 1; z < specs.size(); z++) { + this->rand_int(0x100000000); + } + } + this->log.info_f("{} specs to check with det={:08X}", specs.size(), det); + for (const auto& spec : specs) { + if (this->log.should_log(phosg::LogLevel::L_INFO)) { + this->log.info_f("Checking spec {:08X} => {} with det={:08X}", spec.probability, spec.data.hex(), det); + } + det -= spec.probability; + if (det < 0) { + return this->create_rare_item(spec.data, area); + } + } + return ItemData(); +} + 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; + return ItemData(); } uint8_t table_index = this->table_index_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); - for (const auto& spec : rare_specs) { - 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(); - this->log.info_f("Box spec {:08X} produced item {}", spec.probability, hex); - } - break; - } - if (this->log.should_log(phosg::LogLevel::L_INFO)) { - auto hex = spec.data.hex(); - this->log.info_f("Box spec {:08X} did not produce item {}", spec.probability, hex); - } - } - return item; + auto specs = this->rare_item_set->get_box_specs(this->mode, episode, this->difficulty, this->section_id, table_index); + return this->check_rare_specs_and_create_rare_item(specs, area, force_rare); } uint32_t ItemCreator::rand_int(uint64_t max) { @@ -351,35 +370,13 @@ ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(EnemyType enemy // can have multiple rare drops if JSONRareItemSet is used (the other RareItemSet implementations never return // multiple drops for an enemy type). Episode episode = episode_for_area(area); - auto rare_specs = this->rare_item_set->get_enemy_specs( + auto specs = this->rare_item_set->get_enemy_specs( 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, force_rare); - if (!item.empty()) { - if (this->log.should_log(phosg::LogLevel::L_INFO)) { - auto hex = spec.data.hex(); - this->log.info_f("Enemy spec {:08X} produced item {}", spec.probability, hex); - } - break; - } - if (this->log.should_log(phosg::LogLevel::L_INFO)) { - auto hex = spec.data.hex(); - this->log.info_f("Enemy spec {:08X} did not produce item {}", spec.probability, hex); - } - } - return item; + return this->check_rare_specs_and_create_rare_item(specs, area, force_rare); } -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 (!force_rare && ((drop.probability == 0) || (this->rand_int(0x100000000) >= drop.probability))) { - return ItemData(); - } - - ItemData item = drop.data; +ItemData ItemCreator::create_rare_item(const ItemData& drop_item, uint8_t area) { + ItemData item = drop_item; if (item.can_be_encoded_in_rel_rare_table()) { switch (item.data1[0]) { case 0: diff --git a/src/ItemCreator.hh b/src/ItemCreator.hh index 321f852e..8e77e43a 100644 --- a/src/ItemCreator.hh +++ b/src/ItemCreator.hh @@ -37,6 +37,10 @@ public: bool is_from_rare_table = false; }; + inline void set_legacy_replay() { + this->is_legacy_replay = true; + } + 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 @@ -70,6 +74,7 @@ private: phosg::PrefixedLogger log; Version logic_version; + bool is_legacy_replay; std::shared_ptr stack_limits; GameMode mode; Difficulty difficulty; @@ -123,7 +128,9 @@ private: 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); + ItemData check_rare_specs_and_create_rare_item( + const std::vector& specs, uint8_t area, bool force_rare); + ItemData create_rare_item(const ItemData& drop_item, uint8_t area); void generate_rare_weapon_bonuses(ItemData& item, Episode episode, uint32_t random_sample); void deduplicate_weapon_bonuses(ItemData& item) const; diff --git a/src/Lobby.cc b/src/Lobby.cc index 3d4d14e9..3d1417f2 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -230,6 +230,9 @@ void Lobby::create_item_creator(Version logic_version) { effective_section_id, rand_crypt, this->quest ? this->quest->meta.battle_rules : nullptr); + if (s->use_legacy_item_random_behavior) { + this->item_creator->set_legacy_replay(); + } } uint8_t Lobby::effective_section_id() const { diff --git a/src/ReplaySession.cc b/src/ReplaySession.cc index a11e3a65..359747ef 100644 --- a/src/ReplaySession.cc +++ b/src/ReplaySession.cc @@ -365,6 +365,9 @@ ReplaySession::ReplaySession(shared_ptr state, FILE* input_log, boo if (line == "### use psov2 crypt") { this->state->use_psov2_rand_crypt = true; } + if (line == "### use legacy item random behavior") { + this->state->use_legacy_item_random_behavior = true; + } if (line.starts_with("### cc ")) { // ### cc $ if (this->clients.size() != 1) { diff --git a/src/ServerState.hh b/src/ServerState.hh index 0531c1ac..f49fedf0 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -157,6 +157,7 @@ struct ServerState : public std::enable_shared_from_this { bool default_switch_assist_enabled = false; bool use_game_creator_section_id = false; bool use_psov2_rand_crypt = false; // Used in some tests + bool use_legacy_item_random_behavior = false; // Used in some tests bool rare_notifs_enabled_for_client_drops = false; bool default_rare_notifs_enabled_v1_v2 = false; bool default_rare_notifs_enabled_v3_v4 = false; diff --git a/tests/DCv1-DCv2-PCv2-CrossplayPrivateDrops.test.txt b/tests/DCv1-DCv2-PCv2-CrossplayPrivateDrops.test.txt index 9086bc67..9772c27c 100644 --- a/tests/DCv1-DCv2-PCv2-CrossplayPrivateDrops.test.txt +++ b/tests/DCv1-DCv2-PCv2-CrossplayPrivateDrops.test.txt @@ -1,4 +1,5 @@ ### use psov2 crypt +### use legacy item random behavior I 35932 2025-05-22 21:56:44 - [C-1] Channel name updated: C-1 @ ip:10.37.129.2:49620 I 35932 2025-05-22 21:56:44 - [C-1] Created I 35932 2025-05-22 21:56:44 - [GameServer] Client connected: C-1 via TG-9300-PC_V2-pc-game_server diff --git a/tests/GC-JungleGame.test.txt b/tests/GC-JungleGame.test.txt index ca42a011..5ca1779b 100644 --- a/tests/GC-JungleGame.test.txt +++ b/tests/GC-JungleGame.test.txt @@ -1,4 +1,5 @@ ### use psov2 crypt +### use legacy item random behavior I 39471 2025-05-21 23:39:20 - [IPStackSimulator] Virtual network N-2 connected via T-IPS-5059 I 39471 2025-05-21 23:39:20 - [IPStackSimulator] Client opened TCP connection 23232323238C062D (10.0.1.535:1581 -> 35.35.35.35:9100) (acked_server_seq=BD656BE7, next_client_seq=4F6748DF) I 39471 2025-05-21 23:39:20 - [C-9] Channel name updated: C-9 @ ipss:N-2:127.0.0.1:57072