switch rare drops to stacked space logic

This commit is contained in:
Martin Michelsen
2026-04-03 18:24:41 -07:00
parent b704d827ed
commit 87e85932a4
8 changed files with 60 additions and 47 deletions
+2 -2
View File
@@ -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<uint8_t, 0x2> unused3; // Always zeroes
/* 18 */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
/* 28 */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
/* 38 */ pstring<TextEncoding::ASCII, 0x10> serial_number; // On XB, this is the XBL gamertag
/* 48 */ pstring<TextEncoding::ASCII, 0x10> access_key; // On XB, this is the XBL user ID
/* 58 */ pstring<TextEncoding::ASCII, 0x30> serial_number2; // On DCv2, this is the hardware ID; on XB, this is the XBL gamertag
/* 58 */ pstring<TextEncoding::ASCII, 0x30> serial_number2; // DCv2: hardware ID; XB: XBL gamertag
/* 88 */ pstring<TextEncoding::ASCII, 0x30> access_key2; // On XB, this is the XBL user ID
/* B8 */ pstring<TextEncoding::ASCII, 0x10> login_character_name;
/* C8 */
+41 -44
View File
@@ -38,6 +38,7 @@ ItemCreator::ItemCreator(
shared_ptr<const BattleRules> 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<RareItemSet::ExpandedDrop>& 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:
+8 -1
View File
@@ -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<const ItemData::StackLimits> 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<RareItemSet::ExpandedDrop>& 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;
+3
View File
@@ -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 {
+3
View File
@@ -365,6 +365,9 @@ ReplaySession::ReplaySession(shared_ptr<ServerState> 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 $<chat command>
if (this->clients.size() != 1) {
+1
View File
@@ -157,6 +157,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
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;
@@ -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
+1
View File
@@ -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