From efe7401d7b0e173e346084a335cc8a0db1360e49 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 3 Jun 2026 22:17:14 -0700 Subject: [PATCH] convert TekkerAdjustmentSet to JSON --- CMakeLists.txt | 1 + src/CommonItemSet.cc | 77 ---- src/CommonItemSet.hh | 193 +--------- src/ItemCreator.cc | 131 ++++--- src/ItemCreator.hh | 1 + src/Main.cc | 23 ++ src/ServerState.cc | 4 +- src/ServerState.hh | 1 + src/TekkerAdjustmentSet.cc | 357 ++++++++++++++++++ src/TekkerAdjustmentSet.hh | 45 +++ system/tables/tekker-adjustment-set.json | 77 ++++ tests/game-tables.test.sh | 5 + .../tekker-adjustment-set.expected.bin | Bin 13 files changed, 600 insertions(+), 315 deletions(-) create mode 100644 src/TekkerAdjustmentSet.cc create mode 100644 src/TekkerAdjustmentSet.hh create mode 100644 system/tables/tekker-adjustment-set.json rename system/tables/JudgeItem-gc-v3.rel => tests/game-tables/tekker-adjustment-set.expected.bin (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index cadde791..d4775cce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,6 +128,7 @@ set(SOURCES src/SignalWatcher.cc src/StaticGameData.cc src/TeamIndex.cc + src/TekkerAdjustmentSet.cc src/Text.cc src/TextIndex.cc src/Version.cc diff --git a/src/CommonItemSet.cc b/src/CommonItemSet.cc index 1ca71db5..47c16f10 100644 --- a/src/CommonItemSet.cc +++ b/src/CommonItemSet.cc @@ -1041,80 +1041,3 @@ const WeaponRandomSet::RangeTableEntry* 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 std::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 { - phosg::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 a3b3c1aa..b1c5e894 100644 --- a/src/CommonItemSet.hh +++ b/src/CommonItemSet.hh @@ -285,55 +285,18 @@ public: explicit JSONCommonItemSet(const phosg::JSON& json); }; -// Note: There are clearly better ways of doing this, but this implementation closely follows what the original code in -// the client does. -template -struct ProbabilityTable { - ItemT items[MaxCount]; - size_t count; - - ProbabilityTable() : count(0) {} - - void push(ItemT item) { - if (this->count == MaxCount) { - throw std::runtime_error("push to full probability table"); - } - this->items[this->count++] = item; - } - - ItemT pop() { - if (this->count == 0) { - throw std::runtime_error("pop from empty probability table"); - } - return this->items[--this->count]; - } - - void shuffle(std::shared_ptr rand_crypt) { - for (size_t z = 1; z < this->count; z++) { - size_t other_z = rand_crypt->next() % (z + 1); - ItemT t = this->items[z]; - this->items[z] = this->items[other_z]; - this->items[other_z] = t; - } - } - - ItemT sample(std::shared_ptr rand_crypt) const { - if (this->count == 0) { - throw std::runtime_error("sample from empty probability table"); - } else if (this->count == 1) { - return this->items[0]; - } else { - return this->items[rand_crypt->next() % this->count]; - } - } -}; - class RELFileSet { public: template struct WeightTableEntry { ValueT value; WeightT weight; + phosg::JSON json() const { + return phosg::JSON::dict({{"Weight", this->weight}, {"Value", this->value}}); + } + static WeightTableEntry from_json(const phosg::JSON& json) { + return WeightTableEntry{json.get_int("Weight"), json.get_int("Value")}; + } } __attribute__((packed)); using WeightTableEntry8 = WeightTableEntry; using WeightTableEntry32 = WeightTableEntry; @@ -432,147 +395,3 @@ 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; - phosg::StringReader r; - - struct DeltaProbabilityEntry { - uint8_t delta_index; - uint8_t count_default; - uint8_t count_favored; - } __packed_ws__(DeltaProbabilityEntry, 3); - struct LuckTableEntry { - uint8_t delta_index; - int8_t luck; - } __packed_ws__(LuckTableEntry, 2); - - 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 V3, 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 V3, 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 V3, 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 V3, 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 V3, 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 V3, 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 - } __packed_ws__(Offsets, 0x18); - - 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 6b1e1ada..81b6b765 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -6,19 +6,48 @@ #include "EnemyType.hh" #include "Loggers.hh" -// The favored weapon type table is hardcoded in the game client. The table is: -// Viridia shots -// Greennill rifles -// Skyly swords -// Bluefull partisans -// Purplenum mechguns -// Pinkal canes -// Redria (none) -// Oran daggers -// Yellowboze (none) -// Whitill slicers -static const std::array favored_weapon_by_section_id = { - 0x09, 0x07, 0x02, 0x04, 0x08, 0x0A, 0xFF, 0x03, 0xFF, 0x05}; +// Note: There are clearly better ways of doing this, but this implementation closely follows what the original code in +// the client does. +template +struct ProbabilityTable { + ItemT items[MaxCount]; + size_t count; + + ProbabilityTable() : count(0) {} + + void push(ItemT item) { + if (this->count == MaxCount) { + throw std::runtime_error("push to full probability table"); + } + this->items[this->count++] = item; + } + + ItemT pop() { + if (this->count == 0) { + throw std::runtime_error("pop from empty probability table"); + } + return this->items[--this->count]; + } + + void shuffle(std::shared_ptr rand_crypt) { + for (size_t z = 1; z < this->count; z++) { + size_t other_z = rand_crypt->next() % (z + 1); + ItemT t = this->items[z]; + this->items[z] = this->items[other_z]; + this->items[other_z] = t; + } + } + + ItemT sample(std::shared_ptr rand_crypt) const { + if (this->count == 0) { + throw std::runtime_error("sample from empty probability table"); + } else if (this->count == 1) { + return this->items[0]; + } else { + return this->items[rand_crypt->next() % this->count]; + } + } +}; ItemCreator::ItemCreator( std::shared_ptr common_item_set, @@ -1511,7 +1540,7 @@ void ItemCreator::generate_weapon_shop_item_grind(ItemData& item, size_t player_ table_index = 5; } - uint8_t favored_weapon = favored_weapon_by_section_id.at(this->section_id); + uint8_t favored_weapon = TekkerAdjustmentSet::favored_weapon_type_for_section_id(this->section_id); bool is_favored = (favored_weapon != 0xFF) && (item.data1[1] == favored_weapon); const auto* range = is_favored ? this->weapon_random_set->get_favored_grind_range(table_index) @@ -1716,56 +1745,61 @@ ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) { throw std::runtime_error("tekker deltas can only be applied to weapons"); } - static const std::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]; + bool favored = (item.data1[1] == TekkerAdjustmentSet::favored_weapon_type_for_section_id(section_id)); ssize_t luck = 0; this->log.info_f("Applying tekker deltas for {} weapon", favored ? "favored" : "non-favored"); + auto sample_prob_table = [this](const TekkerAdjustmentSet::Table& table) -> int8_t { + size_t sample = this->rand_crypt->next() % table.total; + for (const auto& [k, v] : table.probs) { + if (sample < v) { + return k; + } + sample -= v; + } + throw std::logic_error("Table total is incorrect"); + }; + // 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->rand_crypt); - int8_t delta = delta_table.at(delta_index); - this->log.info_f("(Special) Delta index {}, delta {}", 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]) { + int8_t delta = sample_prob_table(favored + ? this->tekker_adjustment_set->favored_special_delta_table[section_id] + : this->tekker_adjustment_set->default_special_delta_table[section_id]); + this->log.info_f("(Special) Delta {} chosen", delta); + for (; delta != 0; delta += (delta < 0) - (0 < delta)) { + try { + // Note: The original code checks specifically for -1 and +1 here and only increments or decrements the special + // by 1, and the data files only include delta_indexes 4, 5, and 6 (which correspond to -1, 0, and 1). But we + // want to support other levels of delta indexes, so we simply add delta instead. When using the original + // JudgeItem.rel file, the behavior should be the same, but this logic feels more correct. + uint8_t new_special = item.data1[4] + delta; if (this->item_parameter_table->get_special(item.data1[4]).type == this->item_parameter_table->get_special(new_special).type) { item.data1[4] = new_special; + this->log.info_f("(Special) Delta {} applied", delta); + break; } else { - this->log.info_f("(Special) Delta canceled because it would change special category"); + this->log.info_f("(Special) Delta {} canceled because it would change special category", delta); } + } catch (const std::out_of_range&) { + // Invalid special number passed to get_special; treat it as if delta == 0 } - } catch (const std::out_of_range&) { - // Invalid special number passed to get_special; just ignore it } - luck += this->tekker_adjustment_set->get_luck_for_special_upgrade(delta_index); + luck += this->tekker_adjustment_set->special_luck_table.at(delta); this->log.info_f("(Special) Luck is now {}", luck); } // 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->rand_crypt); - int8_t delta = delta_table.at(delta_index); - this->log.info_f("(Grind) Delta index {}, delta {}", delta_index, delta); + int8_t delta = sample_prob_table(favored + ? this->tekker_adjustment_set->favored_grind_delta_table[section_id] + : this->tekker_adjustment_set->default_grind_delta_table[section_id]); + this->log.info_f("(Grind) Delta {} chosen", delta); int16_t new_grind = static_cast(item.data1[3]) + static_cast(delta); item.data1[3] = std::clamp(new_grind, 0, weapon_def.max_grind); - luck += this->tekker_adjustment_set->get_luck_for_grind_delta(delta_index); + luck += this->tekker_adjustment_set->grind_luck_table.at(delta); this->log.info_f("(Grind) Luck is now {}", luck); } else { this->log.info_f("(Grind) Item is rare; skipping grind adjustment"); @@ -1773,11 +1807,10 @@ ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) { // 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->rand_crypt); - int8_t delta = delta_table.at(delta_index); - this->log.info_f("(Bonuses) Delta index {}, delta {}", delta_index, delta); + int8_t delta = sample_prob_table(favored + ? this->tekker_adjustment_set->favored_bonus_delta_table[section_id] + : this->tekker_adjustment_set->default_bonus_delta_table[section_id]); + this->log.info_f("(Bonuses) Delta {} chosen", 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. @@ -1786,7 +1819,7 @@ ssize_t ItemCreator::apply_tekker_deltas(ItemData& item, uint8_t section_id) { item.data1[z + 1] = std::min(item.data1[z + 1] + delta, 100); } } - luck += this->tekker_adjustment_set->get_luck_for_bonus_delta(delta_index); + luck += this->tekker_adjustment_set->bonus_luck_table.at(delta); this->log.info_f("(Bonuses) Luck is now {}", luck); } diff --git a/src/ItemCreator.hh b/src/ItemCreator.hh index 8e77e43a..d01de519 100644 --- a/src/ItemCreator.hh +++ b/src/ItemCreator.hh @@ -8,6 +8,7 @@ #include "PlayerSubordinates.hh" #include "RareItemSet.hh" #include "StaticGameData.hh" +#include "TekkerAdjustmentSet.hh" // This file and ItemCreator.cc are essentially a direct reverse-engineering of the item creation algorithm in PSO GC. // Only minor changes have been made to support BB (as described in the comments in the implementation) and to support diff --git a/src/Main.cc b/src/Main.cc index de8b13bc..47f69374 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -2431,6 +2431,29 @@ Action a_encode_mag_metadata_table( write_output_data(args, data, nullptr); }); +Action a_decode_tekker_adjustment_set( + "decode-tekker-adjustment-set", "\ + decode-tekker-adjustment-set [INPUT-FILENAME [OUTPUT-FILENAME]] [OPTIONS...]\n\ + Converts a JudgeItem.rel file into a JSON tekker adjustment set. Use\n\ + --big-endian if the .rel file is from PSO GC.\n", + +[](phosg::Arguments& args) { + auto input_data = read_input_data(args); + TekkerAdjustmentSet table(input_data, args.get("big-endian")); + auto json = table.json(); + auto serialized = json.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::SORT_DICT_KEYS); + write_output_data(args, serialized, nullptr); + }); + +Action a_encode_tekker_adjustment_set( + "encode-tekker-adjustment-set", "\ + encode-tekker-adjustment-set [INPUT-FILENAME [OUTPUT-FILENAME]] [OPTIONS...]\n\ + Converts a JSON tekker adjustment set into a JudgeItem.rel file compatible\n\ + with the game client. Use --big-endian if the .rel file is for PSO GC.\n", + +[](phosg::Arguments& args) { + TekkerAdjustmentSet table(phosg::JSON::parse(read_input_data(args))); + write_output_data(args, table.serialize_binary(args.get("big-endian")), nullptr); + }); + Action a_decode_level_table( "decode-level-table", nullptr, +[](phosg::Arguments& args) { diff --git a/src/ServerState.cc b/src/ServerState.cc index c9e03e29..03eff3f9 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -2128,8 +2128,8 @@ void ServerState::load_drop_tables() { new_weapon_random_sets[z] = std::make_shared(weapon_data); } - config_log.info_f("Loading tekker adjustment table"); - auto tekker_data = std::make_shared(phosg::load_file("system/tables/JudgeItem-gc-v3.rel")); + config_log.info_f("Loading tekker adjustment set"); + auto tekker_data = phosg::JSON::parse(phosg::load_file("system/tables/tekker-adjustment-set.json")); auto new_tekker_adjustment_set = std::make_shared(tekker_data); this->rare_item_sets = std::move(new_rare_item_sets); diff --git a/src/ServerState.hh b/src/ServerState.hh index 3dabb2e1..ad2145e1 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -28,6 +28,7 @@ #include "Menu.hh" #include "Quest.hh" #include "TeamIndex.hh" +#include "TekkerAdjustmentSet.hh" #include "WordSelectTable.hh" // Forward declarations due to reference cycles diff --git a/src/TekkerAdjustmentSet.cc b/src/TekkerAdjustmentSet.cc new file mode 100644 index 00000000..0358a24d --- /dev/null +++ b/src/TekkerAdjustmentSet.cc @@ -0,0 +1,357 @@ +#include "TekkerAdjustmentSet.hh" + +#include + +#include "CommonFileFormats.hh" +#include "StaticGameData.hh" +#include "Types.hh" + +static const std::array delta_table = {-10, -5, -3, -2, -1, 0, 1, 2, 3, 5, 10}; +static const std::unordered_map reverse_delta_table = { + {-10, 0}, {-5, 1}, {-3, 2}, {-2, 3}, {-1, 4}, {0, 5}, {1, 6}, {2, 7}, {3, 8}, {5, 9}, {10, 10}}; + +struct DeltaProbabilityEntry { + uint8_t delta_index; + uint8_t count_default; + uint8_t count_favored; +} __packed_ws__(DeltaProbabilityEntry, 3); + +struct LuckTableEntry { + uint8_t delta_index; + int8_t luck; +} __packed_ws__(LuckTableEntry, 2); + +template +struct ProbTableRefT { + U32T offset; + U32T count; +} __packed_ws_be__(ProbTableRefT, 8); + +template +struct RootT { + // 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 V3, 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% + U32T special_delta_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 V3, 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% + U32T grind_delta_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 V3, 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% + U32T bonus_delta_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 V3, the special upgrade luck table is: + // +1 => +20, 0 => 0, -1 => -20 + U32T special_luck_table_offset; // LuckTableEntry[...]; ending with FF FF + + // In PSO V3, the grind delta luck table is: + // +3 => +10, +2 => +5, +1 => +3, 0 => 0, -1 => -3, -2 => -5, -3 => -10 + U32T grind_luck_table_offset; // LuckTableEntry[...]; ending with FF FF + + // In PSO V3, the bonus delta luck table is: + // +10 => +15, +5 => +8, 0 => 0, -5 => -8, -10 => -15 + U32T bonus_luck_table_offset; // LuckTableEntry[...]; ending with FF FF +} __packed_ws_be__(RootT, 0x18); + +uint8_t TekkerAdjustmentSet::favored_weapon_type_for_section_id(uint8_t section_id) { + // The favored weapon type table is hardcoded in the game client. The table is: + // Viridia shots + // Greennill rifles + // Skyly swords + // Bluefull partisans + // Purplenum mechguns + // Pinkal canes + // Redria (none) + // Oran daggers + // Yellowboze (none) + // Whitill slicers + static const std::array data{0x09, 0x07, 0x02, 0x04, 0x08, 0x0A, 0xFF, 0x03, 0xFF, 0x05}; + return data.at(section_id); +} + +TekkerAdjustmentSet::TekkerAdjustmentSet(const void* data, size_t size, bool big_endian) { + if (big_endian) { + this->parse_t(data, size); + } else { + this->parse_t(data, size); + } +} + +TekkerAdjustmentSet::TekkerAdjustmentSet(const std::string& data, bool big_endian) + : TekkerAdjustmentSet(data.data(), data.size(), big_endian) {} + +TekkerAdjustmentSet::TekkerAdjustmentSet(const phosg::JSON& json) { + auto parse_delta_table = [](const phosg::JSON& json) -> std::array { + if (!json.is_dict() || json.size() != 10) { + throw std::runtime_error("Invalid structure for TekkerAdjustmentSet JSON delta table"); + } + std::array ret; + for (size_t section_id = 0; section_id < 10; section_id++) { + auto& table = ret[section_id]; + for (const auto& [k, v] : json.at(name_for_section_id(section_id)).as_dict()) { + auto prob = v->as_int(); + table.probs.emplace(stoll(k), prob); + table.total += prob; + } + } + return ret; + }; + + this->favored_special_delta_table = parse_delta_table(json.at("FavoredSpecialDeltaTable")); + this->default_special_delta_table = parse_delta_table(json.at("DefaultSpecialDeltaTable")); + this->favored_grind_delta_table = parse_delta_table(json.at("FavoredGrindDeltaTable")); + this->default_grind_delta_table = parse_delta_table(json.at("DefaultGrindDeltaTable")); + this->favored_bonus_delta_table = parse_delta_table(json.at("FavoredBonusDeltaTable")); + this->default_bonus_delta_table = parse_delta_table(json.at("DefaultBonusDeltaTable")); + + auto parse_luck_table = [](const phosg::JSON& json) -> std::unordered_map { + std::unordered_map ret; + for (const auto& [k, v] : json.as_dict()) { + ret.emplace(stoll(k), v->as_int()); + } + return ret; + }; + + this->special_luck_table = parse_luck_table(json.at("SpecialLuckTable")); + this->grind_luck_table = parse_luck_table(json.at("GrindLuckTable")); + this->bonus_luck_table = parse_luck_table(json.at("BonusLuckTable")); +} + +template +void TekkerAdjustmentSet::parse_t(const void* data, size_t size) { + phosg::StringReader r(data, size); + const auto& root = r.pget>(r.pget>(size - 0x10)); + + auto parse_delta_table = [&r](std::array& favored_tables, std::array& default_tables, uint32_t ref_offset) -> void { + const auto& ref = r.pget>(ref_offset); + auto* entries = &r.pget(ref.offset, sizeof(DeltaProbabilityEntry) * ref.count * 10); + for (size_t section_id = 0; section_id < 10; section_id++) { + auto& favored_table = favored_tables[section_id]; + auto& default_table = default_tables[section_id]; + for (size_t z = 0; z < ref.count; z++) { + const auto& entry = entries[section_id * ref.count + z]; + int8_t delta = delta_table.at(entry.delta_index); + favored_table.probs.emplace(delta, entry.count_favored); + favored_table.total += entry.count_favored; + default_table.probs.emplace(delta, entry.count_default); + default_table.total += entry.count_default; + } + } + }; + + parse_delta_table(this->favored_special_delta_table, this->default_special_delta_table, root.special_delta_table_offset); + parse_delta_table(this->favored_grind_delta_table, this->default_grind_delta_table, root.grind_delta_table_offset); + parse_delta_table(this->favored_bonus_delta_table, this->default_bonus_delta_table, root.bonus_delta_table_offset); + + auto parse_luck_table = [&r](uint32_t offset) -> std::unordered_map { + auto sub_r = r.sub(offset); + std::unordered_map ret; + for (;;) { + const auto& entry = sub_r.get(); + if (entry.delta_index == 0xFF) { + break; + } + ret.emplace(delta_table.at(entry.delta_index), entry.luck); + } + return ret; + }; + + this->special_luck_table = parse_luck_table(root.special_luck_table_offset); + this->grind_luck_table = parse_luck_table(root.grind_luck_table_offset); + this->bonus_luck_table = parse_luck_table(root.bonus_luck_table_offset); +} + +template +std::string TekkerAdjustmentSet::serialize_binary_t() const { + RELFileWriter rel; + + auto serialize_delta_tables = [&rel](const std::array& favored_tables, const std::array& default_tables) -> ProbTableRefT { + std::set all_deltas; + for (size_t section_id = 0; section_id < 10; section_id++) { + for (const auto& [delta, _] : favored_tables[section_id].probs) { + all_deltas.emplace(delta); + } + for (const auto& [delta, _] : default_tables[section_id].probs) { + all_deltas.emplace(delta); + } + } + + ProbTableRefT ret{rel.w.size(), all_deltas.size()}; + + for (size_t section_id = 0; section_id < 10; section_id++) { + for (auto delta_it = all_deltas.rbegin(); delta_it != all_deltas.rend(); delta_it++) { + DeltaProbabilityEntry entry; + entry.delta_index = reverse_delta_table.at(*delta_it); + try { + entry.count_favored = favored_tables[section_id].probs.at(*delta_it); + } catch (const std::out_of_range&) { + } + try { + entry.count_default = default_tables[section_id].probs.at(*delta_it); + } catch (const std::out_of_range&) { + } + rel.template put(entry); + } + } + + return ret; + }; + + auto special_delta_ref = serialize_delta_tables(this->favored_special_delta_table, this->default_special_delta_table); + auto grind_delta_ref = serialize_delta_tables(this->favored_grind_delta_table, this->default_grind_delta_table); + auto bonus_delta_ref = serialize_delta_tables(this->favored_bonus_delta_table, this->default_bonus_delta_table); + + auto serialize_luck_table = [&rel](const std::unordered_map& table) -> uint32_t { + uint32_t ret = rel.w.size(); + + std::vector> entries; + for (const auto& [delta, luck] : table) { + entries.emplace_back(std::make_pair(reverse_delta_table.at(delta), luck)); + } + std::sort(entries.begin(), entries.end(), std::greater>()); + + for (const auto& [delta_index, luck] : entries) { + rel.w.put_u8(delta_index); + rel.w.put_s8(luck); + } + rel.w.put_u16(0xFFFF); + + return ret; + }; + + RootT root; + root.special_luck_table_offset = serialize_luck_table(this->special_luck_table); + root.grind_luck_table_offset = serialize_luck_table(this->grind_luck_table); + root.bonus_luck_table_offset = serialize_luck_table(this->bonus_luck_table); + + rel.align(4); + rel.relocations.emplace(rel.w.size()); + root.special_delta_table_offset = rel.template put>(special_delta_ref); + rel.relocations.emplace(rel.w.size()); + root.grind_delta_table_offset = rel.template put>(grind_delta_ref); + rel.relocations.emplace(rel.w.size()); + root.bonus_delta_table_offset = rel.template put>(bonus_delta_ref); + + uint32_t root_offset = rel.template put>(root); + for (size_t z = 1; z <= sizeof(RootT) / 4; z++) { + rel.relocations.emplace(rel.w.size() - (z * 4)); + } + + return rel.finalize(root_offset); +} + +std::string TekkerAdjustmentSet::serialize_binary(bool big_endian) const { + return big_endian ? this->serialize_binary_t() : this->serialize_binary_t(); +} + +phosg::JSON TekkerAdjustmentSet::json() const { + auto ret = phosg::JSON::dict(); + + auto serialize_delta_table = [](const std::array& table) -> phosg::JSON { + auto ret = phosg::JSON::dict(); + for (size_t section_id = 0; section_id < 10; section_id++) { + auto secid_ret = phosg::JSON::dict(); + for (const auto& [k, v] : table[section_id].probs) { + secid_ret.emplace(std::format("{}", k), v); + } + ret.emplace(name_for_section_id(section_id), std::move(secid_ret)); + } + return ret; + }; + + ret.emplace("FavoredSpecialDeltaTable", serialize_delta_table(this->favored_special_delta_table)); + ret.emplace("DefaultSpecialDeltaTable", serialize_delta_table(this->default_special_delta_table)); + ret.emplace("FavoredGrindDeltaTable", serialize_delta_table(this->favored_grind_delta_table)); + ret.emplace("DefaultGrindDeltaTable", serialize_delta_table(this->default_grind_delta_table)); + ret.emplace("FavoredBonusDeltaTable", serialize_delta_table(this->favored_bonus_delta_table)); + ret.emplace("DefaultBonusDeltaTable", serialize_delta_table(this->default_bonus_delta_table)); + + auto serialize_luck_table = [](const std::unordered_map& table) -> phosg::JSON { + auto ret = phosg::JSON::dict(); + for (const auto& [k, v] : table) { + ret.emplace(std::format("{}", k), v); + } + return ret; + }; + + ret.emplace("SpecialLuckTable", serialize_luck_table(this->special_luck_table)); + ret.emplace("GrindLuckTable", serialize_luck_table(this->grind_luck_table)); + ret.emplace("BonusLuckTable", serialize_luck_table(this->bonus_luck_table)); + + return ret; +} diff --git a/src/TekkerAdjustmentSet.hh b/src/TekkerAdjustmentSet.hh new file mode 100644 index 00000000..11c34be5 --- /dev/null +++ b/src/TekkerAdjustmentSet.hh @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include "EnemyType.hh" +#include "GSLArchive.hh" +#include "PSOEncryption.hh" +#include "StaticGameData.hh" +#include "Text.hh" +#include "Types.hh" + +struct TekkerAdjustmentSet { + // This struct parses and accesses data from JudgeItem.rel + + TekkerAdjustmentSet(const void* data, size_t size, bool big_endian); + TekkerAdjustmentSet(const std::string& data, bool big_endian); + explicit TekkerAdjustmentSet(const phosg::JSON& json); + + template + void parse_t(const void* data, size_t size); + template + std::string serialize_binary_t() const; + + std::string serialize_binary(bool big_endian) const; + phosg::JSON json() const; + + static uint8_t favored_weapon_type_for_section_id(uint8_t section_id); + + struct Table { + std::unordered_map probs; + size_t total; + }; + + std::array favored_special_delta_table; + std::array default_special_delta_table; + std::array favored_grind_delta_table; + std::array default_grind_delta_table; + std::array favored_bonus_delta_table; + std::array default_bonus_delta_table; + std::unordered_map special_luck_table; + std::unordered_map grind_luck_table; + std::unordered_map bonus_luck_table; +}; diff --git a/system/tables/tekker-adjustment-set.json b/system/tables/tekker-adjustment-set.json new file mode 100644 index 00000000..95ae2039 --- /dev/null +++ b/system/tables/tekker-adjustment-set.json @@ -0,0 +1,77 @@ +{ + "BonusLuckTable": {"-10": -15, "-5": -8, "0": 0, "10": 15, "5": 8}, + "DefaultBonusDeltaTable": { + "Bluefull": {"-10": 5, "-5": 15, "0": 60, "10": 5, "5": 15}, + "Greennill": {"-10": 10, "-5": 25, "0": 50, "10": 5, "5": 10}, + "Oran": {"-10": 5, "-5": 10, "0": 50, "10": 10, "5": 25}, + "Pinkal": {"-10": 5, "-5": 10, "0": 50, "10": 10, "5": 25}, + "Purplenum": {"-10": 10, "-5": 25, "0": 50, "10": 5, "5": 10}, + "Redria": {"-10": 5, "-5": 10, "0": 50, "10": 10, "5": 25}, + "Skyly": {"-10": 5, "-5": 10, "0": 50, "10": 10, "5": 25}, + "Viridia": {"-10": 5, "-5": 15, "0": 60, "10": 5, "5": 15}, + "Whitill": {"-10": 5, "-5": 15, "0": 60, "10": 5, "5": 15}, + "Yellowboze": {"-10": 10, "-5": 25, "0": 50, "10": 5, "5": 10} + }, + "DefaultGrindDeltaTable": { + "Bluefull": {"-1": 10, "-2": 7, "-3": 0, "0": 60, "1": 13, "2": 7, "3": 3}, + "Greennill": {"-1": 10, "-2": 5, "-3": 0, "0": 70, "1": 10, "2": 5, "3": 0}, + "Oran": {"-1": 13, "-2": 7, "-3": 3, "0": 60, "1": 10, "2": 7, "3": 0}, + "Pinkal": {"-1": 13, "-2": 7, "-3": 3, "0": 60, "1": 10, "2": 7, "3": 0}, + "Purplenum": {"-1": 10, "-2": 5, "-3": 0, "0": 70, "1": 10, "2": 5, "3": 0}, + "Redria": {"-1": 13, "-2": 7, "-3": 3, "0": 60, "1": 10, "2": 7, "3": 0}, + "Skyly": {"-1": 13, "-2": 7, "-3": 3, "0": 60, "1": 10, "2": 7, "3": 0}, + "Viridia": {"-1": 10, "-2": 7, "-3": 0, "0": 60, "1": 13, "2": 7, "3": 3}, + "Whitill": {"-1": 10, "-2": 7, "-3": 0, "0": 60, "1": 13, "2": 7, "3": 3}, + "Yellowboze": {"-1": 10, "-2": 5, "-3": 0, "0": 70, "1": 10, "2": 5, "3": 0} + }, + "DefaultSpecialDeltaTable": { + "Bluefull": {"-1": 30, "0": 60, "1": 10}, + "Greennill": {"-1": 10, "0": 65, "1": 25}, + "Oran": {"-1": 15, "0": 70, "1": 15}, + "Pinkal": {"-1": 15, "0": 70, "1": 15}, + "Purplenum": {"-1": 10, "0": 65, "1": 25}, + "Redria": {"-1": 20, "0": 60, "1": 20}, + "Skyly": {"-1": 15, "0": 70, "1": 15}, + "Viridia": {"-1": 30, "0": 60, "1": 10}, + "Whitill": {"-1": 30, "0": 60, "1": 10}, + "Yellowboze": {"-1": 10, "0": 65, "1": 25} + }, + "FavoredBonusDeltaTable": { + "Bluefull": {"-10": 2, "-5": 10, "0": 60, "10": 8, "5": 20}, + "Greennill": {"-10": 7, "-5": 20, "0": 50, "10": 8, "5": 15}, + "Oran": {"-10": 2, "-5": 5, "0": 50, "10": 13, "5": 30}, + "Pinkal": {"-10": 2, "-5": 5, "0": 50, "10": 13, "5": 30}, + "Purplenum": {"-10": 7, "-5": 20, "0": 50, "10": 8, "5": 15}, + "Redria": {"-10": 0, "-5": 0, "0": 0, "10": 0, "5": 0}, + "Skyly": {"-10": 2, "-5": 5, "0": 50, "10": 13, "5": 30}, + "Viridia": {"-10": 2, "-5": 10, "0": 60, "10": 8, "5": 20}, + "Whitill": {"-10": 2, "-5": 10, "0": 60, "10": 8, "5": 20}, + "Yellowboze": {"-10": 7, "-5": 20, "0": 50, "10": 8, "5": 15} + }, + "FavoredGrindDeltaTable": { + "Bluefull": {"-1": 7, "-2": 0, "-3": 0, "0": 50, "1": 25, "2": 13, "3": 5}, + "Greennill": {"-1": 10, "-2": 0, "-3": 0, "0": 60, "1": 20, "2": 7, "3": 3}, + "Oran": {"-1": 10, "-2": 5, "-3": 0, "0": 50, "1": 20, "2": 12, "3": 3}, + "Pinkal": {"-1": 10, "-2": 5, "-3": 0, "0": 50, "1": 20, "2": 12, "3": 3}, + "Purplenum": {"-1": 10, "-2": 0, "-3": 0, "0": 60, "1": 20, "2": 7, "3": 3}, + "Redria": {"-1": 0, "-2": 0, "-3": 0, "0": 0, "1": 0, "2": 0, "3": 0}, + "Skyly": {"-1": 10, "-2": 5, "-3": 0, "0": 50, "1": 20, "2": 12, "3": 3}, + "Viridia": {"-1": 7, "-2": 0, "-3": 0, "0": 50, "1": 25, "2": 13, "3": 5}, + "Whitill": {"-1": 7, "-2": 0, "-3": 0, "0": 50, "1": 25, "2": 13, "3": 5}, + "Yellowboze": {"-1": 10, "-2": 0, "-3": 0, "0": 60, "1": 20, "2": 7, "3": 3} + }, + "FavoredSpecialDeltaTable": { + "Bluefull": {"-1": 25, "0": 50, "1": 25}, + "Greennill": {"-1": 5, "0": 55, "1": 40}, + "Oran": {"-1": 10, "0": 60, "1": 30}, + "Pinkal": {"-1": 10, "0": 60, "1": 30}, + "Purplenum": {"-1": 5, "0": 55, "1": 40}, + "Redria": {"-1": 0, "0": 0, "1": 0}, + "Skyly": {"-1": 10, "0": 60, "1": 30}, + "Viridia": {"-1": 25, "0": 50, "1": 25}, + "Whitill": {"-1": 25, "0": 50, "1": 25}, + "Yellowboze": {"-1": 5, "0": 55, "1": 40} + }, + "GrindLuckTable": {"-1": -3, "-2": -5, "-3": -10, "0": 0, "1": 3, "2": 5, "3": 10}, + "SpecialLuckTable": {"-1": -20, "0": 0, "1": 20} +} \ No newline at end of file diff --git a/tests/game-tables.test.sh b/tests/game-tables.test.sh index f630c250..9861e345 100755 --- a/tests/game-tables.test.sh +++ b/tests/game-tables.test.sh @@ -21,6 +21,11 @@ bindiff tests/game-tables/battle-params-ep1-off.dat tests/game-tables/battle-par bindiff tests/game-tables/battle-params-ep2-off.dat tests/game-tables/battle-params-encoded_lab.dat bindiff tests/game-tables/battle-params-ep4-off.dat tests/game-tables/battle-params-encoded_ep4.dat +echo "... (tekker-adjustment-set)" +$EXECUTABLE decode-tekker-adjustment-set --big-endian $DIR/tekker-adjustment-set.expected.bin $DIR/tekker-adjustment-set.json +$EXECUTABLE encode-tekker-adjustment-set --big-endian $DIR/tekker-adjustment-set.json $DIR/tekker-adjustment-set.encoded.bin +bindiff $DIR/tekker-adjustment-set.expected.bin $DIR/tekker-adjustment-set.encoded.bin + echo "... (level-table) BB" $EXECUTABLE decode-level-table --bb-v4 $DIR/level-table-bb-v4.expected.bin --decompressed $DIR/level-table-bb-v4.json --hex $EXECUTABLE encode-level-table-v4 $DIR/level-table-bb-v4.json $DIR/level-table-bb-v4.encoded.bin --decompressed diff --git a/system/tables/JudgeItem-gc-v3.rel b/tests/game-tables/tekker-adjustment-set.expected.bin similarity index 100% rename from system/tables/JudgeItem-gc-v3.rel rename to tests/game-tables/tekker-adjustment-set.expected.bin