diff --git a/src/CommonItemSet.cc b/src/CommonItemSet.cc index ccdfff53..5b0b0f98 100644 --- a/src/CommonItemSet.cc +++ b/src/CommonItemSet.cc @@ -129,3 +129,82 @@ WeaponRandomSet::get_favored_grind_range(size_t index) const { return &this->r.pget( this->offsets->favored_grind_range_table + sizeof(RangeTableEntry) * index); } + +TekkerAdjustmentSet::TekkerAdjustmentSet(std::shared_ptr data) + : data(data), + r(*data) { + this->offsets = &this->r.pget(this->r.pget_u32b(this->r.size() - 0x10)); +} + +const ProbabilityTable& TekkerAdjustmentSet::get_table( + std::array, 10>& tables_default, + std::array, 10>& tables_favored, + uint32_t offset_and_count_offset, + bool favored, + uint8_t section_id) const { + if (section_id >= 10) { + throw runtime_error("invalid section ID"); + } + ProbabilityTable& table = favored ? tables_favored[section_id] : tables_default[section_id]; + if (table.count == 0) { + uint32_t offset = r.pget_u32b(offset_and_count_offset); + uint32_t count_per_section_id = r.pget_u32b(offset_and_count_offset + 4); + auto* entries = &r.pget(offset, sizeof(DeltaProbabilityEntry) * count_per_section_id * 10); + for (size_t z = count_per_section_id * section_id; z < count_per_section_id * (section_id + 1); z++) { + size_t count = favored ? entries[z].count_favored : entries[z].count_default; + for (size_t w = 0; w < count; w++) { + table.push(entries[z].delta_index); + } + } + } + return table; +} + +const ProbabilityTable& TekkerAdjustmentSet::get_special_upgrade_prob_table(uint8_t section_id, bool favored) const { + return this->get_table( + this->special_upgrade_prob_tables_default, + this->special_upgrade_prob_tables_favored, + this->offsets->special_upgrade_prob_table_offset, + favored, section_id); +} + +const ProbabilityTable& TekkerAdjustmentSet::get_grind_delta_prob_table(uint8_t section_id, bool favored) const { + return this->get_table( + this->grind_delta_prob_tables_default, + this->grind_delta_prob_tables_favored, + this->offsets->grind_delta_prob_table_offset, + favored, section_id); +} + +const ProbabilityTable& TekkerAdjustmentSet::get_bonus_delta_prob_table(uint8_t section_id, bool favored) const { + return this->get_table( + this->bonus_delta_prob_tables_default, + this->bonus_delta_prob_tables_favored, + this->offsets->bonus_delta_prob_table_offset, + favored, section_id); +} + +int8_t TekkerAdjustmentSet::get_luck(uint32_t start_offset, uint8_t delta_index) const { + StringReader sub_r = r.sub(start_offset); + while (!sub_r.eof()) { + const auto& entry = sub_r.get(); + if (entry.delta_index == 0xFF) { + return 0; + } else if (entry.delta_index == delta_index) { + return entry.luck; + } + } + return 0; +} + +int8_t TekkerAdjustmentSet::get_luck_for_special_upgrade(uint8_t delta_index) const { + return this->get_luck(this->offsets->special_upgrade_luck_table_offset, delta_index); +} + +int8_t TekkerAdjustmentSet::get_luck_for_grind_delta(uint8_t delta_index) const { + return this->get_luck(this->offsets->grind_delta_luck_table_offset, delta_index); +} + +int8_t TekkerAdjustmentSet::get_luck_for_bonus_delta(uint8_t delta_index) const { + return this->get_luck(this->offsets->bonus_delta_luck_offset, delta_index); +} diff --git a/src/CommonItemSet.hh b/src/CommonItemSet.hh index 3d95aeaa..084a7b9a 100644 --- a/src/CommonItemSet.hh +++ b/src/CommonItemSet.hh @@ -396,3 +396,153 @@ private: const Offsets* offsets; }; + +class TekkerAdjustmentSet { +public: + // This class parses and accesses data from JudgeItem.rel + TekkerAdjustmentSet(std::shared_ptr data); + + const ProbabilityTable& get_special_upgrade_prob_table(uint8_t section_id, bool favored) const; + const ProbabilityTable& get_grind_delta_prob_table(uint8_t section_id, bool favored) const; + const ProbabilityTable& get_bonus_delta_prob_table(uint8_t section_id, bool favored) const; + int8_t get_luck_for_special_upgrade(uint8_t delta_index) const; + int8_t get_luck_for_grind_delta(uint8_t delta_index) const; + int8_t get_luck_for_bonus_delta(uint8_t delta_index) const; + +private: + const ProbabilityTable& get_table( + std::array, 10>& tables_default, + std::array, 10>& tables_favored, + uint32_t offset_and_count_offset, + bool favored, + uint8_t section_id) const; + int8_t get_luck(uint32_t start_offset, uint8_t delta_index) const; + + std::shared_ptr data; + StringReader r; + + struct DeltaProbabilityEntry { + uint8_t delta_index; + uint8_t count_default; + uint8_t count_favored; + } __attribute__((packed)); + struct LuckTableEntry { + uint8_t delta_index; + int8_t luck; + } __attribute__((packed)); + + struct Offsets { + // Each section ID's favored weapon class has different probabilities than + // those used for all other weapons. The tables are labeled with (D) for the + // default values and (F) for the favored-class values. + + // Note that the favored bonuses for Redria are all zero; these values are + // unused because Redria does not have a favored weapon type. Curiously, + // Yellowboze also does not have a favored weapon type, but the values for + // Yellowboze are not all zero. + + // This table specifies how likely a special is to be upgraded or + // downgraded by one level. + // In PSO GC, the special upgrade table is: + // Viridia => (D) +1=10%, 0=60%, -1=30% + // Viridia => (F) +1=25%, 0=50%, -1=25% + // Greennill => (D) +1=25%, 0=65%, -1=10% + // Greennill => (F) +1=40%, 0=55%, -1=5% + // Skyly => (D) +1=15%, 0=70%, -1=15% + // Skyly => (F) +1=30%, 0=60%, -1=10% + // Bluefull => (D) +1=10%, 0=60%, -1=30% + // Bluefull => (F) +1=25%, 0=50%, -1=25% + // Purplenum => (D) +1=25%, 0=65%, -1=10% + // Purplenum => (F) +1=40%, 0=55%, -1=5% + // Pinkal => (D) +1=15%, 0=70%, -1=15% + // Pinkal => (F) +1=30%, 0=60%, -1=10% + // Redria => (D) +1=20%, 0=60%, -1=20% + // Redria => (F) +1=0%, 0=0%, -1=0% + // Oran => (D) +1=15%, 0=70%, -1=15% + // Oran => (F) +1=30%, 0=60%, -1=10% + // Yellowboze => (D) +1=25%, 0=65%, -1=10% + // Yellowboze => (F) +1=40%, 0=55%, -1=5% + // Whitill => (D) +1=10%, 0=60%, -1=30% + // Whitill => (F) +1=25%, 0=50%, -1=25% + be_uint32_t special_upgrade_prob_table_offset; // [{c, o -> (DeltaProbabilityEntry)[10][c]}) + + // This table specifies how likely a weapon's grind is to be upgraded or + // downgraded, and by how much. The final grind value is clamped to the + // range between 0 and the weapon's maximum grind from ItemPMT, inclusive. + // In PSO GC, the grind delta table is: + // Viridia => (D) +3=3%, +2=7%, +1=13%, 0=60%, -1=10%, -2=7%, -3=0% + // Viridia => (F) +3=5%, +2=13%, +1=25%, 0=50%, -1=7%, -2=0%, -3=0% + // Greennill => (D) +3=0%, +2=5%, +1=10%, 0=70%, -1=10%, -2=5%, -3=0% + // Greennill => (F) +3=3%, +2=7%, +1=20%, 0=60%, -1=10%, -2=0%, -3=0% + // Skyly => (D) +3=0%, +2=7%, +1=10%, 0=60%, -1=13%, -2=7%, -3=3% + // Skyly => (F) +3=3%, +2=12%, +1=20%, 0=50%, -1=10%, -2=5%, -3=0% + // Bluefull => (D) +3=3%, +2=7%, +1=13%, 0=60%, -1=10%, -2=7%, -3=0% + // Bluefull => (F) +3=5%, +2=13%, +1=25%, 0=50%, -1=7%, -2=0%, -3=0% + // Purplenum => (D) +3=0%, +2=5%, +1=10%, 0=70%, -1=10%, -2=5%, -3=0% + // Purplenum => (F) +3=3%, +2=7%, +1=20%, 0=60%, -1=10%, -2=0%, -3=0% + // Pinkal => (D) +3=0%, +2=7%, +1=10%, 0=60%, -1=13%, -2=7%, -3=3% + // Pinkal => (F) +3=3%, +2=12%, +1=20%, 0=50%, -1=10%, -2=5%, -3=0% + // Redria => (D) +3=0%, +2=7%, +1=10%, 0=60%, -1=13%, -2=7%, -3=3% + // Redria => (F) +3=0%, +2=0%, +1=0%, 0=0%, -1=0%, -2=0%, -3=0% + // Oran => (D) +3=0%, +2=7%, +1=10%, 0=60%, -1=13%, -2=7%, -3=3% + // Oran => (F) +3=3%, +2=12%, +1=20%, 0=50%, -1=10%, -2=5%, -3=0% + // Yellowboze => (D) +3=0%, +2=5%, +1=10%, 0=70%, -1=10%, -2=5%, -3=0% + // Yellowboze => (F) +3=3%, +2=7%, +1=20%, 0=60%, -1=10%, -2=0%, -3=0% + // Whitill => (D) +3=3%, +2=7%, +1=13%, 0=60%, -1=10%, -2=7%, -3=0% + // Whitill => (F) +3=5%, +2=13%, +1=25%, 0=50%, -1=7%, -2=0%, -3=0% + be_uint32_t grind_delta_prob_table_offset; // [{c, o -> (DeltaProbabilityEntry)[10][c]}) + + // This table specifies how likely a weapon's bonuses are to be upgraded + // or downgraded, and by how much. The final bonuses are capped above at + // 100, but there is no lower limit (so negative results are possible). + // In PSO GC, the bonus delta table is: + // Viridia => (D) +10=5%, +5=15%, 0=60%, -5=15%, -10=5% + // Viridia => (F) +10=8%, +5=20%, 0=60%, -5=10%, -10=2% + // Greennill => (D) +10=5%, +5=10%, 0=50%, -5=25%, -10=10% + // Greennill => (F) +10=8%, +5=15%, 0=50%, -5=20%, -10=7% + // Skyly => (D) +10=10%, +5=25%, 0=50%, -5=10%, -10=5% + // Skyly => (F) +10=13%, +5=30%, 0=50%, -5=5%, -10=2% + // Bluefull => (D) +10=5%, +5=15%, 0=60%, -5=15%, -10=5% + // Bluefull => (F) +10=8%, +5=20%, 0=60%, -5=10%, -10=2% + // Purplenum => (D) +10=5%, +5=10%, 0=50%, -5=25%, -10=10% + // Purplenum => (F) +10=8%, +5=15%, 0=50%, -5=20%, -10=7% + // Pinkal => (D) +10=10%, +5=25%, 0=50%, -5=10%, -10=5% + // Pinkal => (F) +10=13%, +5=30%, 0=50%, -5=5%, -10=2% + // Redria => (D) +10=10%, +5=25%, 0=50%, -5=10%, -10=5% + // Redria => (F) +10=0%, +5=0%, 0=0%, -5=0%, -10=0% + // Oran => (D) +10=10%, +5=25%, 0=50%, -5=10%, -10=5% + // Oran => (F) +10=13%, +5=30%, 0=50%, -5=5%, -10=2% + // Yellowboze => (D) +10=5%, +5=10%, 0=50%, -5=25%, -10=10% + // Yellowboze => (F) +10=8%, +5=15%, 0=50%, -5=20%, -10=7% + // Whitill => (D) +10=5%, +5=15%, 0=60%, -5=15%, -10=5% + // Whitill => (F) +10=8%, +5=20%, 0=60%, -5=10%, -10=2% + be_uint32_t bonus_delta_prob_table_offset; // [{c, o -> (DeltaProbabilityEntry)[10][c]}) + + // There is a secondary computation done during weapon adjustment that + // appears to determine how "good" the resulting weapon is compared to its + // original state. If the result of this computation is positive, the game + // plays a jingle when the tekker result is accepted. These tables describe + // how much each delta affects this value, which we call luck. + + // In PSO GC, the special upgrade luck table is: + // +1 => +20, 0 => 0, -1 => -20 + be_uint32_t special_upgrade_luck_table_offset; // LuckTableEntry[...]; ending with FF FF + + // In PSO GC, the grind delta luck table is: + // +3 => +10, +2 => +5, +1 => +3, 0 => 0, -1 => -3, -2 => -5, -3 => -10 + be_uint32_t grind_delta_luck_table_offset; // LuckTableEntry[...]; ending with FF FF + + // In PSO GC, the bonus delta luck table is: + // +10 => +15, +5 => +8, 0 => 0, -5 => -8, -10 => -15 + be_uint32_t bonus_delta_luck_offset; // LuckTableEntry[...]; ending with FF FF + } __attribute__((packed)); + + const Offsets* offsets; + + mutable std::array, 10> special_upgrade_prob_tables_default; + mutable std::array, 10> special_upgrade_prob_tables_favored; + mutable std::array, 10> grind_delta_prob_tables_default; + mutable std::array, 10> grind_delta_prob_tables_favored; + mutable std::array, 10> bonus_delta_prob_tables_default; + mutable std::array, 10> bonus_delta_prob_tables_favored; +}; diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index 08f2db45..3f795336 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -5,12 +5,16 @@ using namespace std; +static const array favored_weapon_by_section_id = { + 0x09, 0x07, 0x02, 0x04, 0x08, 0x0A, 0xFF, 0x03, 0xFF, 0x05}; + ItemCreator::ItemCreator( shared_ptr common_item_set, shared_ptr rare_item_set, shared_ptr armor_random_set, shared_ptr tool_random_set, shared_ptr weapon_random_set, + shared_ptr tekker_adjustment_set, shared_ptr item_parameter_table, Episode episode, GameMode mode, @@ -28,6 +32,7 @@ ItemCreator::ItemCreator( armor_random_set(armor_random_set), tool_random_set(tool_random_set), weapon_random_set(weapon_random_set), + tekker_adjustment_set(tekker_adjustment_set), item_parameter_table(item_parameter_table), pt(&this->common_item_set->get_table( this->episode, this->mode, this->difficulty, this->section_id)), @@ -1449,9 +1454,6 @@ void ItemCreator::generate_weapon_shop_item_grind( table_index = 5; } - static const array favored_weapon_by_section_id = { - 0x09, 0x07, 0x02, 0x04, 0x08, 0x0A, 0xFF, 0x03, 0xFF, 0x05}; - uint8_t favored_weapon = favored_weapon_by_section_id.at(this->section_id); bool is_favored = (favored_weapon != 0xFF) && (item.data1[1] == favored_weapon); const auto* range = is_favored @@ -1670,3 +1672,80 @@ ItemData ItemCreator::on_specialized_box_item_drop(uint32_t def0, uint32_t def1, return item; } + +ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) { + if (item.data1[0] != 0) { + throw runtime_error("tekker deltas can only be applied to weapons"); + } + + static const array delta_table = { + -10, -5, -3, -2, -1, 0, 1, 2, 3, 5, 10}; + + bool favored = item.data1[1] == favored_weapon_by_section_id[section_id]; + ssize_t luck = 0; + + // Adjust the weapon's special + { + const auto& prob_table = this->tekker_adjustment_set->get_special_upgrade_prob_table(section_id, favored); + uint8_t delta_index = prob_table.sample(this->random_crypt); + int8_t delta = delta_table.at(delta_index); + fprintf(stderr, "Special: delta_index=%hhu delta=%hhd\n", delta_index, delta); + // Note: The original code checks specifically for -1 and +1 here, but the + // data files only include delta_indexes 4, 5, and 6 (which correspond to -1, + // 0, and 1) anyway, so we just check for positive and negative numbers + // instead. When using the original JudgeItem.rel file, the behavior should + // be the same, but this feels more correct. + try { + uint8_t new_special; + if (delta < 0) { + new_special = item.data1[4] - 1; + } else if (delta > 0) { + new_special = item.data1[4] + 1; + } else { + new_special = item.data1[4]; + } + if ((new_special != item.data1[4]) && + (this->item_parameter_table->get_special(item.data1[4]).type == + this->item_parameter_table->get_special(new_special).type)) { + item.data1[4] = new_special; + } + } catch (const runtime_error&) { + // Invalid special number passed to get_special; just ignore it + } + luck += this->tekker_adjustment_set->get_luck_for_special_upgrade(delta_index); + } + + // Adjust the weapon's grind if it's not rare + if (!this->item_parameter_table->is_item_rare(item)) { + const auto& weapon_def = this->item_parameter_table->get_weapon(item.data1[1], item.data1[2]); + const auto& prob_table = this->tekker_adjustment_set->get_grind_delta_prob_table(section_id, favored); + uint8_t delta_index = prob_table.sample(this->random_crypt); + int8_t delta = delta_table.at(delta_index); + fprintf(stderr, "Grind: delta_index=%hhu delta=%hhd\n", delta_index, delta); + int16_t new_grind = static_cast(item.data1[3]) + static_cast(delta); + item.data1[3] = clamp(new_grind, 0, weapon_def.max_grind); + luck += this->tekker_adjustment_set->get_luck_for_grind_delta(delta_index); + } + + // Adjust the weapon's bonuses + { + const auto& prob_table = this->tekker_adjustment_set->get_bonus_delta_prob_table(section_id, favored); + // Note: The original code really does use the same delta for all three + // bonuses. + uint8_t delta_index = prob_table.sample(this->random_crypt); + int8_t delta = delta_table.at(delta_index); + fprintf(stderr, "Bonus: delta_index=%hhu delta=%hhd\n", delta_index, delta); + // Note: The original code doesn't check if there's actually a bonus in each + // slot before incrementing the values. Presumably there's a check later + // that will clear any invalid bonuses, but we don't have such a check, so + // we need to check here if each bonus is actually present. + for (size_t z = 6; z <= 10; z += 2) { + if (item.data1[z] >= 1 && item.data1[z] <= 5) { + item.data1[z + 1] = min(item.data1[z + 1] + delta, 100); + } + } + luck += this->tekker_adjustment_set->get_luck_for_bonus_delta(delta_index); + } + + return luck; +} diff --git a/src/ItemCreator.hh b/src/ItemCreator.hh index da33e4f4..cfabf312 100644 --- a/src/ItemCreator.hh +++ b/src/ItemCreator.hh @@ -56,6 +56,7 @@ public: std::shared_ptr armor_random_set, std::shared_ptr tool_random_set, std::shared_ptr weapon_random_set, + std::shared_ptr tekker_adjustment_set, std::shared_ptr item_parameter_table, Episode episode, GameMode mode, @@ -73,6 +74,10 @@ public: std::vector generate_tool_shop_contents(size_t player_level); std::vector generate_weapon_shop_contents(size_t player_level); + // This function adjusts the item in-place, and returns the luck value. + // See the comments in TekkerAdjustmentSet for what this value means. + ssize_t apply_tekker_deltas(ItemData& item, uint8_t section_id); + private: PrefixedLogger log; Episode episode; @@ -84,6 +89,7 @@ private: std::shared_ptr armor_random_set; std::shared_ptr tool_random_set; std::shared_ptr weapon_random_set; + std::shared_ptr tekker_adjustment_set; std::shared_ptr item_parameter_table; const CommonItemSet::Table* pt; std::shared_ptr restrictions; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index baa41c5c..c532cf6c 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -3236,6 +3236,7 @@ shared_ptr create_game_generic( s->armor_random_set, s->tool_random_set, s->weapon_random_sets.at(game->difficulty), + s->tekker_adjustment_set, s->item_parameter_table, game->episode, (game->mode == GameMode::SOLO) ? GameMode::NORMAL : game->mode, diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 9f5f2fc0..79536f14 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -1643,15 +1643,21 @@ static void on_identify_item_bb(shared_ptr, if (!(l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) { throw logic_error("item tracking not enabled in BB game"); } + if (!l->item_creator.get()) { + throw logic_error("received item identify subcommand without item creator present"); + } size_t x = c->game_data.player()->inventory.find_item(cmd.item_id); if (c->game_data.player()->inventory.items[x].data.data1[0] != 0) { return; // Only weapons can be identified } - c->game_data.player()->disp.stats.meseta -= 100; + auto p = c->game_data.player(); + p->disp.stats.meseta -= 100; c->game_data.identify_result = c->game_data.player()->inventory.items[x]; c->game_data.identify_result.data.data1[4] &= 0x7F; + l->item_creator->apply_tekker_deltas( + c->game_data.identify_result.data, p->disp.visual.section_id); send_item_identify_result(l, c); } else { diff --git a/src/ServerState.cc b/src/ServerState.cc index 0ed8ecbc..dd58c128 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -871,6 +871,11 @@ void ServerState::load_item_tables() { this->weapon_random_sets[z].reset(new WeaponRandomSet(weapon_data)); } + config_log.info("Loading tekker adjustment table"); + shared_ptr tekker_data(new string(load_file( + "system/blueburst/JudgeItem_GC.rel"))); + this->tekker_adjustment_set.reset(new TekkerAdjustmentSet(tekker_data)); + config_log.info("Loading item definition table"); shared_ptr pmt_data(new string(prs_decompress(load_file( "system/blueburst/ItemPMT.prs")))); diff --git a/src/ServerState.hh b/src/ServerState.hh index 2f075802..85ad351a 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -81,6 +81,7 @@ struct ServerState { std::shared_ptr armor_random_set; std::shared_ptr tool_random_set; std::array, 4> weapon_random_sets; + std::shared_ptr tekker_adjustment_set; std::shared_ptr item_parameter_table; std::shared_ptr mag_evolution_table;