From 0383dc90b8413db1ffcf9ddf96635545f72d5441 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Thu, 22 Feb 2024 00:05:40 -0800 Subject: [PATCH] allow overriding stack sizes --- src/ItemCreator.cc | 11 +- src/ItemCreator.hh | 3 +- src/ItemData.cc | 73 +++++-- src/ItemData.hh | 42 +++- src/ItemNameIndex.cc | 20 +- src/ItemNameIndex.hh | 4 +- src/Items.cc | 6 +- src/Lobby.cc | 2 +- src/Main.cc | 28 +-- src/PlayerSubordinates.cc | 10 +- src/PlayerSubordinates.hh | 4 +- src/ProxyServer.cc | 2 +- src/ReceiveCommands.cc | 11 +- src/ReceiveSubcommands.cc | 130 +++++++----- src/SaveFileFormats.cc | 10 +- src/SaveFileFormats.hh | 4 +- src/ServerShell.cc | 5 +- src/ServerState.cc | 414 ++++++++++++++++++++----------------- src/ServerState.hh | 16 +- src/StaticGameData.cc | 17 -- src/StaticGameData.hh | 2 - src/Text.hh | 8 +- system/config.example.json | 21 ++ tests/config.json | 15 ++ 24 files changed, 504 insertions(+), 354 deletions(-) diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index 48fdf704..aa201238 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -18,15 +18,16 @@ ItemCreator::ItemCreator( shared_ptr weapon_random_set, shared_ptr tekker_adjustment_set, shared_ptr item_parameter_table, - Version version, + std::shared_ptr stack_limits, Episode episode, GameMode mode, uint8_t difficulty, uint8_t section_id, uint32_t random_seed, shared_ptr restrictions) - : log(string_printf("[ItemCreator:%s/%s/%s/%c/%hhu] ", name_for_enum(version), abbreviation_for_episode(episode), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id), lobby_log.min_level), - version(version), + : log(string_printf("[ItemCreator:%s/%s/%s/%c/%hhu] ", name_for_enum(stack_limits->version), abbreviation_for_episode(episode), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id), lobby_log.min_level), + version(stack_limits->version), + stack_limits(stack_limits), episode(episode), mode(mode), difficulty(difficulty), @@ -474,7 +475,7 @@ void ItemCreator::set_item_unidentified_flag_if_not_challenge(ItemData& item) co void ItemCreator::set_tool_item_amount_to_1(ItemData& item) const { if (item.data1[0] == 0x03) { - item.set_tool_item_amount(this->version, 1); + item.set_tool_item_amount(*this->stack_limits, 1); } } @@ -1723,7 +1724,7 @@ ItemData ItemCreator::base_item_for_specialized_box(uint32_t def0, uint32_t def1 if (item.data1[1] == 0x02) { item.data1[4] = def0 & 0xFF; } - item.set_tool_item_amount(this->version, 1); + item.set_tool_item_amount(*this->stack_limits, 1); break; case 0x04: item.data2d = ((def1 >> 0x10) & 0xFFFF) * 10; diff --git a/src/ItemCreator.hh b/src/ItemCreator.hh index 33872202..20cfe37b 100644 --- a/src/ItemCreator.hh +++ b/src/ItemCreator.hh @@ -19,7 +19,7 @@ public: std::shared_ptr weapon_random_set, std::shared_ptr tekker_adjustment_set, std::shared_ptr item_parameter_table, - Version version, + std::shared_ptr stack_limits, Episode episode, GameMode mode, uint8_t difficulty, @@ -56,6 +56,7 @@ public: private: PrefixedLogger log; Version version; + std::shared_ptr stack_limits; Episode episode; GameMode mode; uint8_t difficulty; diff --git a/src/ItemData.cc b/src/ItemData.cc index 0b181756..f9b8501c 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -8,6 +8,39 @@ using namespace std; +const vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_11_2000( + {10}); +const vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2( + {10, 10, 1, 10, 10, 10, 10, 10, 10, 1}); +const vector ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V3_V4( + {10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1}); + +ItemData::StackLimits::StackLimits( + Version version, const vector& max_tool_stack_sizes_by_data1_1, uint32_t max_meseta_stack_size) + : version(version), + max_tool_stack_sizes_by_data1_1(max_tool_stack_sizes_by_data1_1), + max_meseta_stack_size(max_meseta_stack_size) {} + +ItemData::StackLimits::StackLimits(Version version, const JSON& json) + : version(version) { + this->max_tool_stack_sizes_by_data1_1.clear(); + for (const auto& limit_json : json.at("ToolLimits").as_list()) { + this->max_tool_stack_sizes_by_data1_1.emplace_back(limit_json->as_int()); + } + this->max_meseta_stack_size = json.at("MesetaLimit").as_int(); +} + +uint8_t ItemData::StackLimits::get(uint8_t data1_0, uint8_t data1_1) const { + if (data1_0 == 4) { + return this->max_meseta_stack_size; + } + if (data1_0 == 3) { + const auto& vec = this->max_tool_stack_sizes_by_data1_1; + return vec.at(min(data1_1, vec.size() - 1)); + } + return 1; +} + ItemData::ItemData() { this->clear(); } @@ -85,7 +118,7 @@ uint32_t ItemData::primary_identifier() const { } } -bool ItemData::is_wrapped(Version version) const { +bool ItemData::is_wrapped(const StackLimits& limits) const { switch (this->data1[0]) { case 0: case 1: @@ -93,7 +126,7 @@ bool ItemData::is_wrapped(Version version) const { case 2: return this->data2[2] & 0x40; case 3: - return !this->is_stackable(version) && (this->data1[3] & 0x40); + return !this->is_stackable(limits) && (this->data1[3] & 0x40); case 4: return false; default: @@ -101,7 +134,7 @@ bool ItemData::is_wrapped(Version version) const { } } -void ItemData::wrap(Version version) { +void ItemData::wrap(const StackLimits& limits) { switch (this->data1[0]) { case 0: case 1: @@ -111,7 +144,7 @@ void ItemData::wrap(Version version) { this->data2[2] |= 0x40; break; case 3: - if (!this->is_stackable(version)) { + if (!this->is_stackable(limits)) { this->data1[3] |= 0x40; } break; @@ -122,7 +155,7 @@ void ItemData::wrap(Version version) { } } -void ItemData::unwrap(Version version) { +void ItemData::unwrap(const StackLimits& limits) { switch (this->data1[0]) { case 0: case 1: @@ -132,7 +165,7 @@ void ItemData::unwrap(Version version) { this->data2[2] &= 0xBF; break; case 3: - if (!this->is_stackable(version)) { + if (!this->is_stackable(limits)) { this->data1[3] &= 0xBF; } break; @@ -143,23 +176,23 @@ void ItemData::unwrap(Version version) { } } -bool ItemData::is_stackable(Version version) const { - return this->max_stack_size(version) > 1; +bool ItemData::is_stackable(const StackLimits& limits) const { + return this->max_stack_size(limits) > 1; } -size_t ItemData::stack_size(Version version) const { - if (max_stack_size_for_item(version, this->data1[0], this->data1[1]) > 1) { +size_t ItemData::stack_size(const StackLimits& limits) const { + if (this->max_stack_size(limits) > 1) { return this->data1[5]; } return 1; } -size_t ItemData::max_stack_size(Version version) const { - return max_stack_size_for_item(version, this->data1[0], this->data1[1]); +size_t ItemData::max_stack_size(const StackLimits& limits) const { + return limits.get(this->data1[0], this->data1[1]); } -void ItemData::enforce_min_stack_size(Version version) { - if (this->stack_size(version) == 0) { +void ItemData::enforce_min_stack_size(const StackLimits& limits) { + if (this->stack_size(limits) == 0) { this->data1[5] = 1; } } @@ -502,12 +535,12 @@ void ItemData::set_sealed_item_kill_count(uint16_t v) { } } -uint8_t ItemData::get_tool_item_amount(Version version) const { - return this->is_stackable(version) ? this->data1[5] : 1; +uint8_t ItemData::get_tool_item_amount(const StackLimits& limits) const { + return this->is_stackable(limits) ? this->data1[5] : 1; } -void ItemData::set_tool_item_amount(Version version, uint8_t amount) { - if (this->is_stackable(version)) { +void ItemData::set_tool_item_amount(const StackLimits& limits, uint8_t amount) { + if (this->is_stackable(limits)) { this->data1[5] = amount; } else if (this->data1[0] == 0x03) { this->data1[5] = 0x00; @@ -667,7 +700,7 @@ ItemData ItemData::from_data(const string& data) { return ret; } -ItemData ItemData::from_primary_identifier(Version version, uint32_t primary_identifier) { +ItemData ItemData::from_primary_identifier(const StackLimits& limits, uint32_t primary_identifier) { ItemData ret; if (primary_identifier > 0x04000000) { throw runtime_error("invalid item class"); @@ -680,7 +713,7 @@ ItemData ItemData::from_primary_identifier(Version version, uint32_t primary_ide } else { ret.data1[2] = (primary_identifier >> 8) & 0xFF; } - ret.set_tool_item_amount(version, 1); + ret.set_tool_item_amount(limits, 1); return ret; } diff --git a/src/ItemData.hh b/src/ItemData.hh index 535e03b9..19ddd55b 100644 --- a/src/ItemData.hh +++ b/src/ItemData.hh @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "Text.hh" @@ -54,7 +55,26 @@ struct ItemMagStats { } }; -struct ItemData { // 0x14 bytes +struct ItemData { + struct StackLimits { + Version version; + std::vector max_tool_stack_sizes_by_data1_1; + uint32_t max_meseta_stack_size; + + StackLimits(Version version, const std::vector& max_tool_stack_sizes_by_data1_1, uint32_t max_meseta_stack_size); + StackLimits(Version version, const JSON& json); + StackLimits(const StackLimits& other) = default; + StackLimits(StackLimits&& other) = default; + StackLimits& operator=(const StackLimits& other) = default; + StackLimits& operator=(StackLimits&& other) = default; + + uint8_t get(uint8_t data1_0, uint8_t data1_1) const; + + static const std::vector DEFAULT_TOOL_LIMITS_DC_11_2000; + static const std::vector DEFAULT_TOOL_LIMITS_V1_V2; + static const std::vector DEFAULT_TOOL_LIMITS_V3_V4; + }; + // QUICK ITEM FORMAT REFERENCE // data1/0 data1/4 data1/8 data2 // Weapon: 00ZZZZGG SS00AABB AABBAABB 00000000 @@ -124,18 +144,18 @@ struct ItemData { // 0x14 bytes void clear(); static ItemData from_data(const std::string& data); - static ItemData from_primary_identifier(Version version, uint32_t primary_identifier); + static ItemData from_primary_identifier(const StackLimits& limits, uint32_t primary_identifier); std::string hex() const; uint32_t primary_identifier() const; - bool is_wrapped(Version version) const; - void wrap(Version version); - void unwrap(Version version); + bool is_wrapped(const StackLimits& limits) const; + void wrap(const StackLimits& limits); + void unwrap(const StackLimits& limits); - bool is_stackable(Version version) const; - size_t stack_size(Version version) const; - size_t max_stack_size(Version version) const; - void enforce_min_stack_size(Version version); + bool is_stackable(const StackLimits& limits) const; + size_t stack_size(const StackLimits& limits) const; + size_t max_stack_size(const StackLimits& limits) const; + void enforce_min_stack_size(const StackLimits& limits); static bool is_common_consumable(uint32_t primary_identifier); bool is_common_consumable() const; @@ -154,8 +174,8 @@ struct ItemData { // 0x14 bytes uint16_t get_sealed_item_kill_count() const; void set_sealed_item_kill_count(uint16_t v); - uint8_t get_tool_item_amount(Version version) const; - void set_tool_item_amount(Version version, uint8_t amount); + uint8_t get_tool_item_amount(const StackLimits& limits) const; + void set_tool_item_amount(const StackLimits& limits, uint8_t amount); int16_t get_armor_or_shield_defense_bonus() const; void set_armor_or_shield_defense_bonus(int16_t bonus); int16_t get_common_armor_evasion_bonus() const; diff --git a/src/ItemNameIndex.cc b/src/ItemNameIndex.cc index 360d3046..3a62d744 100644 --- a/src/ItemNameIndex.cc +++ b/src/ItemNameIndex.cc @@ -5,16 +5,16 @@ using namespace std; ItemNameIndex::ItemNameIndex( - Version version, std::shared_ptr item_parameter_table, + std::shared_ptr limits, const std::vector& name_coll) - : version(version), - item_parameter_table(item_parameter_table) { + : item_parameter_table(item_parameter_table), + limits(limits) { for (uint32_t primary_identifier : item_parameter_table->compute_all_valid_primary_identifiers()) { const string* name = nullptr; try { - ItemData item = ItemData::from_primary_identifier(this->version, primary_identifier); + ItemData item = ItemData::from_primary_identifier(*this->limits, primary_identifier); name = &name_coll.at(item_parameter_table->get_item_id(item)); } catch (const out_of_range&) { } @@ -138,7 +138,7 @@ std::string ItemNameIndex::describe_item( // flags in a different location. if (((item.data1[1] == 0x01) && (item.data1[4] & 0x40)) || ((item.data1[0] == 0x02) && (item.data2[2] & 0x40)) || - ((item.data1[0] == 0x03) && !item.is_stackable(this->version) && (item.data1[3] & 0x40))) { + ((item.data1[0] == 0x03) && !item.is_stackable(*this->limits) && (item.data1[3] & 0x40))) { ret_tokens.emplace_back("Wrapped"); } @@ -318,7 +318,7 @@ std::string ItemNameIndex::describe_item( // For tools, add the amount (if applicable) } else if (item.data1[0] == 0x03) { - if (item.max_stack_size(this->version) > 1) { + if (item.max_stack_size(*this->limits) > 1) { ret_tokens.emplace_back(string_printf("x%hhu", item.data1[5])); } } @@ -360,7 +360,7 @@ ItemData ItemNameIndex::parse_item_description(const std::string& desc) const { } } } - ret.enforce_min_stack_size(this->version); + ret.enforce_min_stack_size(*this->limits); return ret; } @@ -596,7 +596,7 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript ret.data2[2] |= 0x40; } } else if (ret.data1[0] == 0x03) { - if (ret.max_stack_size(this->version) > 1) { + if (ret.max_stack_size(*this->limits) > 1) { if (starts_with(desc, "x")) { ret.data1[5] = stoul(desc.substr(1), nullptr, 10); } else { @@ -607,7 +607,7 @@ ItemData ItemNameIndex::parse_item_description_phase(const std::string& descript } if (is_wrapped) { - if (ret.is_stackable(this->version)) { + if (ret.is_stackable(*this->limits)) { throw runtime_error("stackable items cannot be wrapped"); } else { ret.data1[3] |= 0x40; @@ -816,7 +816,7 @@ void ItemNameIndex::print_table(FILE* stream) const { item.data1[0] = 0x03; item.data1[1] = data1_1; item.data1[(data1_1 == 0x02) ? 4 : 2] = data1_2; - item.set_tool_item_amount(this->version, 1); + item.set_tool_item_amount(*this->limits, 1); string name = this->describe_item(item); fprintf(stream, "03%02zX%02zX => %08" PRIX32 " %04hX %04hX %6" PRIu32 " %5hu %04hX %6" PRId32 " %08" PRIX32 " %2hhu* %s %s\n", diff --git a/src/ItemNameIndex.hh b/src/ItemNameIndex.hh index 27138a7a..e424cf56 100644 --- a/src/ItemNameIndex.hh +++ b/src/ItemNameIndex.hh @@ -20,8 +20,8 @@ public: }; ItemNameIndex( - Version version, std::shared_ptr pmt, + std::shared_ptr limits, const std::vector& name_coll); inline size_t entry_count() const { @@ -43,8 +43,8 @@ public: private: ItemData parse_item_description_phase(const std::string& description, bool skip_special) const; - Version version; std::shared_ptr item_parameter_table; + std::shared_ptr limits; std::unordered_map> primary_identifier_index; std::map> name_index; diff --git a/src/Items.cc b/src/Items.cc index 9ef22c26..4fe646ee 100644 --- a/src/Items.cc +++ b/src/Items.cc @@ -111,9 +111,9 @@ void player_use_item(shared_ptr c, size_t item_index, shared_ptrversion())) { + } else if (item.data.is_wrapped(*s->item_stack_limits(c->version()))) { // Unwrap present - item.data.unwrap(c->version()); + item.data.unwrap(*s->item_stack_limits(c->version())); should_delete_item = false; } else if (primary_identifier == 0x00330000) { @@ -250,7 +250,7 @@ void player_use_item(shared_ptr c, size_t item_index, shared_ptrremove_item(item.data.id, 1, c->version()); + player->remove_item(item.data.id, 1, *s->item_stack_limits(c->version())); } } diff --git a/src/Lobby.cc b/src/Lobby.cc index 153c552e..a6a3d61d 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -252,7 +252,7 @@ void Lobby::create_item_creator() { s->weapon_random_sets.at(this->difficulty), s->tekker_adjustment_set, s->item_parameter_table(this->base_version), - this->base_version, + s->item_stack_limits(this->base_version), this->episode, (this->mode == GameMode::SOLO) ? GameMode::NORMAL : this->mode, this->difficulty, diff --git a/src/Main.cc b/src/Main.cc index 4f353bc5..6b4068a9 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1739,11 +1739,13 @@ Action a_describe_item( Action a_name_all_items( "name-all-items", nullptr, +[](Arguments&) { - auto s = make_shared(); + auto s = make_shared("system/config.json"); + s->load_config_early(); s->load_patch_indexes(false); s->load_text_index(false); s->load_item_definitions(false); s->load_item_name_indexes(false); + s->load_config_early(); set all_primary_identifiers; for (const auto& index : s->item_name_indexes) { @@ -1771,7 +1773,7 @@ Action a_name_all_items( if (index) { Version version = static_cast(v_s); auto pmt = s->item_parameter_table(version); - ItemData item = ItemData::from_primary_identifier(version, primary_identifier); + ItemData item = ItemData::from_primary_identifier(*s->item_stack_limits(version), primary_identifier); string name = index->describe_item(item); try { bool is_rare = pmt->is_item_rare(item); @@ -2091,12 +2093,12 @@ Action a_find_rare_enemy_seeds( size_t min_count = args.get("min-count", 1); string quest_name = args.get("quest", false); - ServerState s("system/config.json"); + auto s = make_shared("system/config.json"); shared_ptr vq; if (!quest_name.empty()) { - s.load_config(); - s.load_quest_index(false); - auto q = s.quest_index(version)->get(quest_name); + s->load_config_early(); + s->load_quest_index(false); + auto q = s->quest_index(version)->get(quest_name); if (!q) { throw runtime_error("quest does not exist"); } @@ -2105,20 +2107,20 @@ Action a_find_rare_enemy_seeds( throw runtime_error("quest version does not exist"); } } else if (version == Version::BB_V4) { - s.load_config(); + s->load_config_early(); } else if (version == Version::PC_V2) { - s.load_patch_indexes(false); + s->load_patch_indexes(false); } else { - s.clear_map_file_caches(); + s->clear_map_file_caches(); } shared_ptr rare_rates; if (version != Version::BB_V4) { rare_rates = Map::DEFAULT_RARE_ENEMIES; } else if (mode == GameMode::CHALLENGE) { - rare_rates = s.rare_enemy_rates_challenge; + rare_rates = s->rare_enemy_rates_challenge; } else { - rare_rates = s.rare_enemy_rates_by_difficulty[difficulty]; + rare_rates = s->rare_enemy_rates_by_difficulty[difficulty]; } mutex output_lock; @@ -2142,8 +2144,8 @@ Action a_find_rare_enemy_seeds( difficulty, 0, 0, - s.set_data_table(version, episode, mode, difficulty), - bind(&ServerState::load_map_file, &s, placeholders::_1, placeholders::_2), + s->set_data_table(version, episode, mode, difficulty), + bind(&ServerState::load_map_file, s.get(), placeholders::_1, placeholders::_2), rare_rates, random_crypt, variations); diff --git a/src/PlayerSubordinates.cc b/src/PlayerSubordinates.cc index 510a7d5a..7a580eb4 100644 --- a/src/PlayerSubordinates.cc +++ b/src/PlayerSubordinates.cc @@ -444,7 +444,7 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge() const { return ret; } -void PlayerBank::add_item(const ItemData& item, Version version) { +void PlayerBank::add_item(const ItemData& item, const ItemData::StackLimits& limits) { uint32_t primary_identifier = item.primary_identifier(); if (primary_identifier == 0x04000000) { @@ -455,7 +455,7 @@ void PlayerBank::add_item(const ItemData& item, Version version) { return; } - size_t combine_max = item.max_stack_size(version); + size_t combine_max = item.max_stack_size(limits); if (combine_max > 1) { size_t y; for (y = 0; y < this->num_items; y++) { @@ -480,17 +480,17 @@ void PlayerBank::add_item(const ItemData& item, Version version) { } auto& last_item = this->items[this->num_items]; last_item.data = item; - last_item.amount = (item.max_stack_size(version) > 1) ? item.data1[5] : 1; + last_item.amount = (item.max_stack_size(limits) > 1) ? item.data1[5] : 1; last_item.present = 1; this->num_items++; } -ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount, Version version) { +ItemData PlayerBank::remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) { size_t index = this->find_item(item_id); auto& bank_item = this->items[index]; ItemData ret; - if (amount && (bank_item.data.stack_size(version) > 1) && (amount < bank_item.data.data1[5])) { + if (amount && (bank_item.data.stack_size(limits) > 1) && (amount < bank_item.data.data1[5])) { ret = bank_item.data; ret.data1[5] = amount; bank_item.data.data1[5] -= amount; diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index 71362c6b..3c718231 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -101,8 +101,8 @@ struct PlayerBank { /* 0008 */ parray items; /* 12C8 */ - void add_item(const ItemData& item, Version version); - ItemData remove_item(uint32_t item_id, uint32_t amount, Version version); + void add_item(const ItemData& item, const ItemData::StackLimits& limits); + ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits); size_t find_item(uint32_t item_id); void sort(); diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index a3a9b83c..f1cc75e1 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -782,7 +782,7 @@ void ProxyServer::LinkedSession::set_drop_mode(DropMode new_mode) { s->weapon_random_sets.at(this->lobby_difficulty), s->tekker_adjustment_set, s->item_parameter_table(version), - version, + s->item_stack_limits(version), this->lobby_episode, (this->lobby_mode == GameMode::SOLO) ? GameMode::NORMAL : this->lobby_mode, this->lobby_difficulty, diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index dc4bd59b..49953cd0 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -3740,7 +3740,7 @@ static void on_DF_BB(shared_ptr c, uint16_t command, uint32_t, string& d ? p->challenge_records.ep2_online_award_state : p->challenge_records.ep1_online_award_state; award_state.rank_award_flags |= cmd.rank_bitmask; - p->add_item(cmd.item, c->version()); + p->add_item(cmd.item, *s->item_stack_limits(c->version())); l->on_item_id_generated_externally(cmd.item.id); string desc = s->describe_item(Version::BB_V4, cmd.item, false); l->log.info("(Challenge mode) Item awarded to player %hhu: %s", c->lobby_client_id, desc.c_str()); @@ -4611,6 +4611,7 @@ static void on_D2_V3_BB(shared_ptr c, uint16_t, uint32_t, string& data) throw runtime_error("player executed a trade with no other side pending"); } + auto s = c->require_server_state(); auto complete_trade_for_side = [&](shared_ptr to_c, shared_ptr from_c) { if (c->version() == Version::BB_V4) { // On BB, the server is expected to generate the delete item and create @@ -4618,9 +4619,9 @@ static void on_D2_V3_BB(shared_ptr c, uint16_t, uint32_t, string& data) auto to_p = to_c->character(); auto from_p = from_c->character(); for (const auto& trade_item : from_c->pending_item_trade->items) { - size_t amount = trade_item.stack_size(from_c->version()); + size_t amount = trade_item.stack_size(*s->item_stack_limits(from_c->version())); - auto item = from_p->remove_item(trade_item.id, amount, from_c->version()); + auto item = from_p->remove_item(trade_item.id, amount, *s->item_stack_limits(from_c->version())); // This is a special case: when the trade is executed, the client // deletes the traded items from its own inventory automatically, so we // should NOT send the 6x29 to that client; we should only send it to @@ -4632,7 +4633,7 @@ static void on_D2_V3_BB(shared_ptr c, uint16_t, uint32_t, string& data) } } - to_p->add_item(trade_item, to_c->version()); + to_p->add_item(trade_item, *s->item_stack_limits(to_c->version())); send_create_inventory_item_to_lobby(to_c, to_c->lobby_client_id, item); } send_command(to_c, 0xD3, 0x00); @@ -5066,7 +5067,7 @@ static void on_EA_BB(shared_ptr c, uint16_t command, uint32_t flag, stri } } if (!reward.reward_item.empty()) { - c->current_bank().add_item(reward.reward_item, c->version()); + c->current_bank().add_item(reward.reward_item, *s->item_stack_limits(c->version())); } } break; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 201a00c8..5a4bb048 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -1385,9 +1385,10 @@ static void on_player_drop_item(shared_ptr c, uint8_t command, uint8_t f return; } + auto s = c->require_server_state(); auto l = c->require_lobby(); auto p = c->character(); - auto item = p->remove_item(cmd.item_id, 0, c->version()); + auto item = p->remove_item(cmd.item_id, 0, *s->item_stack_limits(c->version())); l->add_item(cmd.floor, item, cmd.x, cmd.z, 0x00F); if (l->log.should_log(LogLevel::INFO)) { @@ -1444,12 +1445,13 @@ static void on_create_inventory_item_t(shared_ptr c, uint8_t command, ui return; } + auto s = c->require_server_state(); auto l = c->require_lobby(); auto p = c->character(); ItemData item = cmd.item_data; item.decode_for_version(c->version()); l->on_item_id_generated_externally(item.id); - p->add_item(item, c->version()); + p->add_item(item, *s->item_stack_limits(c->version())); if (l->log.should_log(LogLevel::INFO)) { auto s = c->require_server_state(); @@ -1516,13 +1518,15 @@ static void on_drop_partial_stack_bb(shared_ptr c, uint8_t command, uint auto l = c->require_lobby(); if (l->base_version == Version::BB_V4) { const auto& cmd = check_size_t(data, size); + auto s = c->require_server_state(); if (!l->is_game() || (cmd.header.client_id != c->lobby_client_id)) { return; } auto p = c->character(); - auto item = p->remove_item(cmd.item_id, cmd.amount, c->version()); + const auto& limits = *s->item_stack_limits(c->version()); + auto item = p->remove_item(cmd.item_id, cmd.amount, limits); // If a stack was split, the original item still exists, so the dropped item // needs a new ID. remove_item signals this by returning an item with an ID @@ -1534,7 +1538,7 @@ static void on_drop_partial_stack_bb(shared_ptr c, uint8_t command, uint // PSOBB sends a 6x29 command after it receives the 6x5D, so we need to add // the item back to the player's inventory to correct for this (it will get // removed again by the 6x29 handler) - p->add_item(item, c->version()); + p->add_item(item, limits); l->add_item(cmd.floor, item, cmd.x, cmd.z, 0x00F); send_drop_stacked_item_to_lobby(l, item, cmd.floor, cmd.x, cmd.z); @@ -1569,7 +1573,7 @@ static void on_buy_shop_item(shared_ptr c, uint8_t command, uint8_t flag item.data2d = 0; // Clear the price field item.decode_for_version(c->version()); l->on_item_id_generated_externally(item.id); - p->add_item(item, c->version()); + p->add_item(item, *s->item_stack_limits(c->version())); size_t price = s->item_parameter_table(c->version())->price_for_item(item); p->remove_meseta(price, c->version() != Version::BB_V4); @@ -1696,6 +1700,7 @@ static void on_pick_up_item_generic( // logic here instead of forwarding the 6x5A to the leader. auto p = c->character(); + auto s = c->require_server_state(); auto fi = l->remove_item(floor, item_id, c->lobby_client_id); if (!fi->visible_to_client(c->lobby_client_id)) { l->log.warning("Player %hu requests to pick up %08" PRIX32 ", but is it not visible to them; dropping command", @@ -1705,7 +1710,7 @@ static void on_pick_up_item_generic( } try { - p->add_item(fi->data, c->version()); + p->add_item(fi->data, *s->item_stack_limits(c->version())); } catch (const out_of_range&) { // Inventory is full; put the item back where it was l->log.warning("Player %hu requests to pick up %08" PRIX32 ", but their inventory is full; dropping command", @@ -1721,7 +1726,6 @@ static void on_pick_up_item_generic( c->print_inventory(stderr); } - auto s = c->require_server_state(); for (size_t z = 0; z < 12; z++) { auto lc = l->clients[z]; if ((!lc) || (!is_request && (lc == c))) { @@ -1821,8 +1825,8 @@ static void on_feed_mag( return; } - auto l = c->require_lobby(); auto s = c->require_server_state(); + auto l = c->require_lobby(); auto p = c->character(); size_t mag_index = p->inventory.find_item(cmd.mag_item_id); @@ -1843,7 +1847,7 @@ static void on_feed_mag( // remove the fed item here, but on other versions, we allow the following // 6x29 command to do that. if (c->version() == Version::BB_V4) { - p->remove_item(cmd.fed_item_id, 1, c->version()); + p->remove_item(cmd.fed_item_id, 1, *s->item_stack_limits(c->version())); } if (l->log.should_log(LogLevel::INFO)) { @@ -1984,7 +1988,8 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr c, uint } } else { // Deposit item - auto item = p->remove_item(cmd.item_id, cmd.item_amount, c->version()); + const auto& limits = *s->item_stack_limits(c->version()); + auto item = p->remove_item(cmd.item_id, cmd.item_amount, limits); // If a stack was split, the bank item retains the same item ID as the // inventory item. This is annoying but doesn't cause any problems // because we always generate a new item ID when withdrawing from the @@ -1992,7 +1997,7 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr c, uint if (item.id == 0xFFFFFFFF) { item.id = cmd.item_id; } - bank.add_item(item, c->version()); + bank.add_item(item, limits); send_destroy_item_to_lobby(c, cmd.item_id, cmd.item_amount, true); if (l->log.should_log(LogLevel::INFO)) { @@ -2019,9 +2024,10 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr c, uint } } else { // Take item - auto item = bank.remove_item(cmd.item_id, cmd.item_amount, c->version()); + const auto& limits = *s->item_stack_limits(c->version()); + auto item = bank.remove_item(cmd.item_id, cmd.item_amount, limits); item.id = l->generate_item_id(c->lobby_client_id); - p->add_item(item, c->version()); + p->add_item(item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); if (l->log.should_log(LogLevel::INFO)) { @@ -2841,26 +2847,29 @@ void on_adjust_player_meseta_bb(shared_ptr c, uint8_t, uint8_t, void* da p->disp.stats.meseta += cmd.amount; } } else if (cmd.amount > 0) { + auto s = c->require_server_state(); auto l = c->require_lobby(); ItemData item; item.data1[0] = 0x04; item.data2d = cmd.amount.load(); item.id = l->generate_item_id(c->lobby_client_id); - p->add_item(item, c->version()); + p->add_item(item, *s->item_stack_limits(c->version())); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); } } void on_item_reward_request_bb(shared_ptr c, uint8_t, uint8_t, void* data, size_t size) { const auto& cmd = check_size_t(data, size); + auto s = c->require_server_state(); auto l = c->require_lobby(); + const auto& limits = *s->item_stack_limits(c->version()); ItemData item; item = cmd.item_data; - item.enforce_min_stack_size(c->version()); + item.enforce_min_stack_size(limits); item.id = l->generate_item_id(c->lobby_client_id); - c->character()->add_item(item, c->version()); + c->character()->add_item(item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); } @@ -2885,7 +2894,8 @@ void on_transfer_item_via_mail_message_bb(shared_ptr c, uint8_t command, auto s = c->require_server_state(); auto p = c->character(); - auto item = p->remove_item(cmd.item_id, cmd.amount, c->version()); + const auto& limits = *s->item_stack_limits(c->version()); + auto item = p->remove_item(cmd.item_id, cmd.amount, limits); if (l->log.should_log(LogLevel::INFO)) { auto name = s->describe_item(c->version(), item, false); @@ -2904,7 +2914,7 @@ void on_transfer_item_via_mail_message_bb(shared_ptr c, uint8_t command, (target_c->character(false) != nullptr) && !target_c->config.check_flag(Client::Flag::AT_BANK_COUNTER)) { try { - target_c->current_bank().add_item(item, target_c->version()); + target_c->current_bank().add_item(item, limits); item_sent = true; } catch (const runtime_error&) { } @@ -2917,7 +2927,7 @@ void on_transfer_item_via_mail_message_bb(shared_ptr c, uint8_t command, send_command(c, 0x16EA, 0x00000000); // If the item failed to send, add it back to the sender's inventory item.id = l->generate_item_id(0xFF); - p->add_item(item, c->version()); + p->add_item(item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); } } @@ -2943,7 +2953,7 @@ void on_exchange_item_for_team_points_bb(shared_ptr c, uint8_t command, auto s = c->require_server_state(); auto p = c->character(); - auto item = p->remove_item(cmd.item_id, cmd.amount, c->version()); + auto item = p->remove_item(cmd.item_id, cmd.amount, *s->item_stack_limits(c->version())); size_t points = s->item_parameter_table(Version::BB_V4)->get_item_team_points(item); s->team_index->add_member_points(c->license->serial_number, points); @@ -2971,7 +2981,7 @@ static void on_destroy_inventory_item(shared_ptr c, uint8_t command, uin auto s = c->require_server_state(); auto p = c->character(); - auto item = p->remove_item(cmd.item_id, cmd.amount, c->version()); + auto item = p->remove_item(cmd.item_id, cmd.amount, *s->item_stack_limits(c->version())); if (l->log.should_log(LogLevel::INFO)) { auto name = s->describe_item(c->version(), item, false); @@ -3075,7 +3085,8 @@ static void on_accept_identify_item_bb(shared_ptr c, uint8_t command, ui if (c->bb_identify_result.id != cmd.item_id) { throw runtime_error("accepted item ID does not match previous identify request"); } - c->character()->add_item(c->bb_identify_result, c->version()); + auto s = c->require_server_state(); + c->character()->add_item(c->bb_identify_result, *s->item_stack_limits(c->version())); send_create_inventory_item_to_lobby(c, c->lobby_client_id, c->bb_identify_result); c->bb_identify_result.clear(); @@ -3092,7 +3103,7 @@ static void on_sell_item_at_shop_bb(shared_ptr c, uint8_t command, uint8 auto s = c->require_server_state(); auto p = c->character(); - auto item = p->remove_item(cmd.item_id, cmd.amount, c->version()); + auto item = p->remove_item(cmd.item_id, cmd.amount, *s->item_stack_limits(c->version())); size_t price = (s->item_parameter_table(c->version())->price_for_item(item) >> 3) * cmd.amount; p->add_meseta(price); @@ -3111,10 +3122,12 @@ static void on_buy_shop_item_bb(shared_ptr c, uint8_t, uint8_t, void* da auto l = c->require_lobby(); if (l->base_version == Version::BB_V4) { const auto& cmd = check_size_t(data, size); + auto s = c->require_server_state(); + const auto& limits = *s->item_stack_limits(c->version()); ItemData item; item = c->bb_shop_contents.at(cmd.shop_type).at(cmd.item_index); - if (item.is_stackable(c->version())) { + if (item.is_stackable(limits)) { item.data1[5] = cmd.amount; } else if (cmd.amount != 1) { throw runtime_error("item is not stackable"); @@ -3127,7 +3140,7 @@ static void on_buy_shop_item_bb(shared_ptr c, uint8_t, uint8_t, void* da item.id = cmd.shop_item_id; l->on_item_id_generated_externally(item.id); - p->add_item(item, c->version()); + p->add_item(item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item, true); if (l->log.should_log(LogLevel::INFO)) { @@ -3375,20 +3388,22 @@ static void on_quest_exchange_item_bb(shared_ptr c, uint8_t, uint8_t, vo (l->base_version == Version::BB_V4) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) { const auto& cmd = check_size_t(data, size); + auto s = c->require_server_state(); try { auto p = c->character(); + const auto& limits = *s->item_stack_limits(c->version()); size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier()); - auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, c->version()); + auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, limits); send_destroy_item_to_lobby(c, found_item.id, 1); // TODO: We probably should use an allow-list here to prevent the client // from creating arbitrary items if cheat mode is disabled. ItemData new_item = cmd.replace_item; - new_item.enforce_min_stack_size(c->version()); + new_item.enforce_min_stack_size(limits); new_item.id = l->generate_item_id(c->lobby_client_id); - p->add_item(new_item, c->version()); + p->add_item(new_item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item); send_quest_function_call(c, cmd.success_function_id); @@ -3404,12 +3419,13 @@ static void on_wrap_item_bb(shared_ptr c, uint8_t, uint8_t, void* data, auto l = c->require_lobby(); if (l->is_game() && (l->base_version == Version::BB_V4)) { const auto& cmd = check_size_t(data, size); + auto s = c->require_server_state(); auto p = c->character(); - auto item = p->remove_item(cmd.item.id, 1, c->version()); + auto item = p->remove_item(cmd.item.id, 1, *s->item_stack_limits(c->version())); send_destroy_item_to_lobby(c, item.id, 1); - item.wrap(c->version()); - p->add_item(item, c->version()); + item.wrap(*s->item_stack_limits(c->version())); + p->add_item(item, *s->item_stack_limits(c->version())); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); } } @@ -3418,20 +3434,22 @@ static void on_photon_drop_exchange_for_item_bb(shared_ptr c, uint8_t, u auto l = c->require_lobby(); if (l->is_game() && (l->base_version == Version::BB_V4)) { const auto& cmd = check_size_t(data, size); + auto s = c->require_server_state(); try { auto p = c->character(); + const auto& limits = *s->item_stack_limits(c->version()); size_t found_index = p->inventory.find_item_by_primary_identifier(0x03100000); - auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 0, c->version()); - send_destroy_item_to_lobby(c, found_item.id, found_item.stack_size(c->version())); + auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 0, limits); + send_destroy_item_to_lobby(c, found_item.id, found_item.stack_size(limits)); // TODO: We probably should use an allow-list here to prevent the client // from creating arbitrary items if cheat mode is disabled. ItemData new_item = cmd.new_item; - new_item.enforce_min_stack_size(c->version()); + new_item.enforce_min_stack_size(limits); new_item.id = l->generate_item_id(c->lobby_client_id); - p->add_item(new_item, c->version()); + p->add_item(new_item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item); send_quest_function_call(c, cmd.success_function_id); @@ -3447,6 +3465,8 @@ static void on_photon_drop_exchange_for_s_rank_special_bb(shared_ptr c, auto l = c->require_lobby(); if (l->is_game() && (l->base_version == Version::BB_V4)) { const auto& cmd = check_size_t(data, size); + auto s = c->require_server_state(); + const auto& limits = *s->item_stack_limits(c->version()); try { auto p = c->character(); @@ -3459,13 +3479,13 @@ static void on_photon_drop_exchange_for_s_rank_special_bb(shared_ptr c, // consistent in case of error p->inventory.find_item(cmd.item_id); - auto payment_item = p->remove_item(p->inventory.items[payment_item_index].data.id, cost, c->version()); + auto payment_item = p->remove_item(p->inventory.items[payment_item_index].data.id, cost, limits); send_destroy_item_to_lobby(c, payment_item.id, cost); - auto item = p->remove_item(cmd.item_id, 1, c->version()); + auto item = p->remove_item(cmd.item_id, 1, limits); send_destroy_item_to_lobby(c, item.id, cost); item.data1[2] = cmd.special_type; - p->add_item(item, c->version()); + p->add_item(item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); send_quest_function_call(c, cmd.success_function_id); @@ -3495,6 +3515,7 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr c, uint8_t, } if (slt_index >= 0) { + const auto& limits = *s->item_stack_limits(c->version()); uint32_t slt_item_id = p->inventory.items[slt_index].data.id; G_ExchangeItemInQuest_BB_6xDB exchange_cmd; @@ -3506,14 +3527,14 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr c, uint8_t, exchange_cmd.amount = 1; send_command_t(c, 0x60, 0x00, exchange_cmd); - p->remove_item(slt_item_id, 1, c->version()); + p->remove_item(slt_item_id, 1, limits); ItemData item = (s->secret_lottery_results.size() == 1) ? s->secret_lottery_results[0] : s->secret_lottery_results[l->random_crypt->next() % s->secret_lottery_results.size()]; - item.enforce_min_stack_size(c->version()); + item.enforce_min_stack_size(limits); item.id = l->generate_item_id(c->lobby_client_id); - p->add_item(item, c->version()); + p->add_item(item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); } @@ -3537,9 +3558,10 @@ static void on_photon_crystal_exchange_bb(shared_ptr c, uint8_t, uint8_t auto l = c->require_lobby(); if (l->is_game() && (l->base_version == Version::BB_V4) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) { check_size_t(data, size); + auto s = c->require_server_state(); auto p = c->character(); size_t index = p->inventory.find_item_by_primary_identifier(0x03100200); - auto item = p->remove_item(p->inventory.items[index].data.id, 1, c->version()); + auto item = p->remove_item(p->inventory.items[index].data.id, 1, *s->item_stack_limits(c->version())); send_destroy_item_to_lobby(c, item.id, 1); } } @@ -3565,7 +3587,7 @@ static void on_quest_F95E_result_bb(shared_ptr c, uint8_t, uint8_t, void } else if (item.data1[0] == 0x00) { item.data1[4] |= 0x80; // Unidentified } else { - item.enforce_min_stack_size(c->version()); + item.enforce_min_stack_size(*s->item_stack_limits(c->version())); } item.id = l->generate_item_id(0xFF); @@ -3588,8 +3610,9 @@ static void on_quest_F95F_result_bb(shared_ptr c, uint8_t, uint8_t, void throw runtime_error("invalid result index"); } + const auto& limits = *s->item_stack_limits(c->version()); size_t index = p->inventory.find_item_by_primary_identifier(0x03100400); // Photon Ticket - auto ticket_item = p->remove_item(p->inventory.items[index].data.id, result.first, c->version()); + auto ticket_item = p->remove_item(p->inventory.items[index].data.id, result.first, limits); // TODO: Shouldn't we send a 6x29 here? Check if this causes desync in an // actual game @@ -3601,9 +3624,9 @@ static void on_quest_F95F_result_bb(shared_ptr c, uint8_t, uint8_t, void send_command_t(c, 0x60, 0x00, cmd_6xDB); ItemData new_item = result.second; - new_item.enforce_min_stack_size(c->version()); + new_item.enforce_min_stack_size(limits); new_item.id = l->generate_item_id(c->lobby_client_id); - p->add_item(new_item, c->version()); + p->add_item(new_item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item); S_GallonPlanResult_BB_25 out_cmd; @@ -3667,7 +3690,7 @@ static void on_quest_F960_result_bb(shared_ptr c, uint8_t, uint8_t, void send_command_t(c, 0x60, 0x00, cmd_6xE3); try { - p->add_item(item, c->version()); + p->add_item(item, *s->item_stack_limits(c->version())); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); if (c->log.should_log(LogLevel::INFO)) { string name = s->describe_item(c->version(), item, false); @@ -3686,10 +3709,12 @@ static void on_momoka_item_exchange_bb(shared_ptr c, uint8_t, uint8_t, v auto l = c->require_lobby(); if (l->is_game() && (l->base_version == Version::BB_V4) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) { const auto& cmd = check_size_t(data, size); + auto s = c->require_server_state(); auto p = c->character(); try { + const auto& limits = *s->item_stack_limits(c->version()); size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier()); - auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, c->version()); + auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, limits); G_ExchangeItemInQuest_BB_6xDB cmd_6xDB = {{0xDB, 0x04, c->lobby_client_id}, 1, found_item.id, 1}; send_command_t(c, 0x60, 0x00, cmd_6xDB); @@ -3699,9 +3724,9 @@ static void on_momoka_item_exchange_bb(shared_ptr c, uint8_t, uint8_t, v // TODO: We probably should use an allow-list here to prevent the client // from creating arbitrary items if cheat mode is disabled. ItemData new_item = cmd.replace_item; - new_item.enforce_min_stack_size(c->version()); + new_item.enforce_min_stack_size(limits); new_item.id = l->generate_item_id(c->lobby_client_id); - p->add_item(new_item, c->version()); + p->add_item(new_item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item); send_command(c, 0x23, 0x00); @@ -3716,6 +3741,7 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr c, uint8_t, uint8_ auto l = c->require_lobby(); if (l->is_game() && (l->base_version == Version::BB_V4) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) { const auto& cmd = check_size_t(data, size); + auto s = c->require_server_state(); auto p = c->character(); try { size_t item_index = p->inventory.find_item(cmd.item_id); @@ -3724,10 +3750,10 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr c, uint8_t, uint8_ uint32_t payment_primary_identifier = cmd.payment_type ? 0x03100100 : 0x03100000; size_t payment_index = p->inventory.find_item_by_primary_identifier(payment_primary_identifier); auto& payment_item = p->inventory.items[payment_index].data; - if (payment_item.stack_size(c->version()) < cmd.payment_count) { + if (payment_item.stack_size(*s->item_stack_limits(c->version())) < cmd.payment_count) { throw runtime_error("not enough payment items present"); } - p->remove_item(payment_item.id, cmd.payment_count, c->version()); + p->remove_item(payment_item.id, cmd.payment_count, *s->item_stack_limits(c->version())); send_destroy_item_to_lobby(c, payment_item.id, cmd.payment_count); uint8_t attribute_amount = 0; diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 3a74f6bc..fa350bda 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -408,7 +408,7 @@ PSOBBCharacterFile::SymbolChatEntry PSOBBCharacterFile::DefaultSymbolChatEntry:: // TODO: Eliminate duplication between this function and the parallel function // in PlayerBank -void PSOBBCharacterFile::add_item(const ItemData& item, Version version) { +void PSOBBCharacterFile::add_item(const ItemData& item, const ItemData::StackLimits& limits) { uint32_t primary_identifier = item.primary_identifier(); // Annoyingly, meseta is in the disp data, not in the inventory struct. If the @@ -419,7 +419,7 @@ void PSOBBCharacterFile::add_item(const ItemData& item, Version version) { } // Handle combinable items - size_t combine_max = item.max_stack_size(version); + size_t combine_max = item.max_stack_size(limits); if (combine_max > 1) { // Get the item index if there's already a stack of the same item in the // player's inventory @@ -456,13 +456,13 @@ void PSOBBCharacterFile::add_item(const ItemData& item, Version version) { // TODO: Eliminate code duplication between this function and the parallel // function in PlayerBank -ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, Version version) { +ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits) { ItemData ret; // If we're removing meseta (signaled by an invalid item ID), then create a // meseta item. if (item_id == 0xFFFFFFFF) { - this->remove_meseta(amount, !is_v4(version)); + this->remove_meseta(amount, !is_v4(limits.version)); ret.data1[0] = 0x04; ret.data2d = amount; return ret; @@ -476,7 +476,7 @@ ItemData PSOBBCharacterFile::remove_item(uint32_t item_id, uint32_t amount, Vers // then create a new item and reduce the amount of the existing stack. Note // that passing amount == 0 means to remove the entire stack, so this only // applies if amount is nonzero. - if (amount && (inventory_item.data.stack_size(version) > 1) && + if (amount && (inventory_item.data.stack_size(limits) > 1) && (amount < inventory_item.data.data1[5])) { if (is_equipped) { throw runtime_error("character has a combine item equipped"); diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 6d2eeb89..e3bc8168 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -233,8 +233,8 @@ struct PSOBBCharacterFile { const PlayerDispDataBBPreview& preview, std::shared_ptr level_table); - void add_item(const ItemData& item, Version version); - ItemData remove_item(uint32_t item_id, uint32_t amount, Version version); + void add_item(const ItemData& item, const ItemData::StackLimits& limits); + ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits); void add_meseta(uint32_t amount); void remove_meseta(uint32_t amount, bool allow_overdraft); diff --git a/src/ServerShell.cc b/src/ServerShell.cc index 517370da..c4f5c542 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -259,7 +259,10 @@ CommandDefinition c_reload( } else if (type == "drop-tables") { args.s->load_drop_tables(true); } else if (type == "config") { - args.s->forward_to_event_thread([s = args.s]() { s->load_config(); }); + args.s->forward_to_event_thread([s = args.s]() { + s->load_config_early(); + s->load_config_late(); + }); } else if (type == "teams") { args.s->load_teams(true); } else if (type == "quests") { diff --git a/src/ServerState.cc b/src/ServerState.cc index 9ed6ab50..4a1a3fc0 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -385,6 +385,14 @@ shared_ptr ServerState::item_parameter_table_for_encod return this->item_parameter_table(is_v1(version) ? Version::PC_V2 : version); } +shared_ptr ServerState::item_stack_limits(Version version) const { + auto ret = this->item_stack_limits_tables.at(static_cast(version)); + if (ret == nullptr) { + throw runtime_error("no item stack limits table exists for this version"); + } + return ret; +} + shared_ptr ServerState::item_name_index(Version version) const { auto ret = this->item_name_indexes.at(static_cast(version)); if (ret == nullptr) { @@ -401,8 +409,7 @@ ItemData ServerState::parse_item_description(Version version, const string& desc return this->item_name_index(version)->parse_item_description(description); } -void ServerState::set_port_configuration( - const vector& port_configs) { +void ServerState::set_port_configuration(const vector& port_configs) { this->name_to_port_config.clear(); this->number_to_port_config.clear(); @@ -546,17 +553,17 @@ void ServerState::collect_network_addresses() { } } -void ServerState::load_config() { +void ServerState::load_config_early() { if (this->config_filename.empty()) { throw logic_error("configuration filename is missing"); } config_log.info("Loading configuration"); - auto json = JSON::parse(load_file(this->config_filename)); + this->config_json = JSON::parse(load_file(this->config_filename)); auto parse_behavior_switch = [&](const string& json_key, BehaviorSwitch default_value) -> ServerState::BehaviorSwitch { try { - string behavior = json.get_string(json_key); + string behavior = this->config_json.get_string(json_key); if (behavior == "Off") { return ServerState::BehaviorSwitch::OFF; } else if (behavior == "OffByDefault") { @@ -573,11 +580,11 @@ void ServerState::load_config() { } }; - this->name = json.at("ServerName").as_string(); + this->name = this->config_json.at("ServerName").as_string(); - if (!this->config_loaded) { + if (!this->one_time_config_loaded) { try { - this->username = json.at("User").as_string(); + this->username = this->config_json.at("User").as_string(); if (this->username == "$SUDO_USER") { const char* user_from_env = getenv("SUDO_USER"); if (!user_from_env) { @@ -588,15 +595,15 @@ void ServerState::load_config() { } catch (const out_of_range&) { } - this->set_port_configuration(parse_port_configuration(json.at("PortConfiguration"))); + this->set_port_configuration(parse_port_configuration(this->config_json.at("PortConfiguration"))); try { - auto spec = this->parse_port_spec(json.at("DNSServerPort")); + auto spec = this->parse_port_spec(this->config_json.at("DNSServerPort")); this->dns_server_addr = std::move(spec.first); this->dns_server_port = spec.second; } catch (const out_of_range&) { } try { - for (const auto& item : json.at("IPStackListen").as_list()) { + for (const auto& item : this->config_json.at("IPStackListen").as_list()) { if (item->is_int()) { this->ip_stack_addresses.emplace_back(string_printf("0.0.0.0:%" PRId64, item->as_int())); } else { @@ -606,7 +613,7 @@ void ServerState::load_config() { } catch (const out_of_range&) { } try { - for (const auto& item : json.at("PPPStackListen").as_list()) { + for (const auto& item : this->config_json.at("PPPStackListen").as_list()) { if (item->is_int()) { this->ppp_stack_addresses.emplace_back(string_printf("0.0.0.0:%" PRId64, item->as_int())); } else { @@ -616,7 +623,7 @@ void ServerState::load_config() { } catch (const out_of_range&) { } try { - for (const auto& item : json.at("HTTPListen").as_list()) { + for (const auto& item : this->config_json.at("HTTPListen").as_list()) { if (item->is_int()) { this->http_addresses.emplace_back(string_printf("0.0.0.0:%" PRId64, item->as_int())); } else { @@ -625,9 +632,11 @@ void ServerState::load_config() { } } catch (const out_of_range&) { } + + this->one_time_config_loaded = true; } - auto local_address_str = json.at("LocalAddress").as_string(); + auto local_address_str = this->config_json.at("LocalAddress").as_string(); try { this->local_address = this->all_addresses.at(local_address_str); string addr_str = string_for_address(this->local_address); @@ -640,7 +649,7 @@ void ServerState::load_config() { this->all_addresses.erase(""); this->all_addresses.emplace("", this->local_address); - auto external_address_str = json.at("ExternalAddress").as_string(); + auto external_address_str = this->config_json.at("ExternalAddress").as_string(); try { this->external_address = this->all_addresses.at(external_address_str); string addr_str = string_for_address(this->external_address); @@ -653,32 +662,32 @@ void ServerState::load_config() { this->all_addresses.erase(""); this->all_addresses.emplace("", this->external_address); - this->client_ping_interval_usecs = json.get_int("ClientPingInterval", 30000000); - this->client_idle_timeout_usecs = json.get_int("ClientIdleTimeout", 60000000); - this->patch_client_idle_timeout_usecs = json.get_int("PatchClientIdleTimeout", 300000000); + this->client_ping_interval_usecs = this->config_json.get_int("ClientPingInterval", 30000000); + this->client_idle_timeout_usecs = this->config_json.get_int("ClientIdleTimeout", 60000000); + this->patch_client_idle_timeout_usecs = this->config_json.get_int("PatchClientIdleTimeout", 300000000); - this->ip_stack_debug = json.get_bool("IPStackDebug", false); - this->allow_unregistered_users = json.get_bool("AllowUnregisteredUsers", false); - this->allow_pc_nte = json.get_bool("AllowPCNTE", false); - this->use_temp_licenses_for_prototypes = json.get_bool("UseTemporaryLicensesForPrototypes", true); - this->allowed_drop_modes_v1_v2_normal = json.get_int("AllowedDropModesV1V2Normal", 0x1F); - this->allowed_drop_modes_v1_v2_battle = json.get_int("AllowedDropModesV1V2Battle", 0x07); - this->allowed_drop_modes_v1_v2_challenge = json.get_int("AllowedDropModesV1V2Challenge", 0x07); - this->allowed_drop_modes_v3_normal = json.get_int("AllowedDropModesV3Normal", 0x1F); - this->allowed_drop_modes_v3_battle = json.get_int("AllowedDropModesV3Battle", 0x07); - this->allowed_drop_modes_v3_challenge = json.get_int("AllowedDropModesV3Challenge", 0x07); - this->allowed_drop_modes_v4_normal = json.get_int("AllowedDropModesV4Normal", 0x1D); - this->allowed_drop_modes_v4_battle = json.get_int("AllowedDropModesV4Battle", 0x05); - this->allowed_drop_modes_v4_challenge = json.get_int("AllowedDropModesV4Challenge", 0x05); - this->default_drop_mode_v1_v2_normal = json.get_enum("DefaultDropModeV1V2Normal", Lobby::DropMode::CLIENT); - this->default_drop_mode_v1_v2_battle = json.get_enum("DefaultDropModeV1V2Battle", Lobby::DropMode::CLIENT); - this->default_drop_mode_v1_v2_challenge = json.get_enum("DefaultDropModeV1V2Challenge", Lobby::DropMode::CLIENT); - this->default_drop_mode_v3_normal = json.get_enum("DefaultDropModeV3Normal", Lobby::DropMode::CLIENT); - this->default_drop_mode_v3_battle = json.get_enum("DefaultDropModeV3Battle", Lobby::DropMode::CLIENT); - this->default_drop_mode_v3_challenge = json.get_enum("DefaultDropModeV3Challenge", Lobby::DropMode::CLIENT); - this->default_drop_mode_v4_normal = json.get_enum("DefaultDropModeV4Normal", Lobby::DropMode::SERVER_SHARED); - this->default_drop_mode_v4_battle = json.get_enum("DefaultDropModeV4Battle", Lobby::DropMode::SERVER_SHARED); - this->default_drop_mode_v4_challenge = json.get_enum("DefaultDropModeV4Challenge", Lobby::DropMode::SERVER_SHARED); + this->ip_stack_debug = this->config_json.get_bool("IPStackDebug", false); + this->allow_unregistered_users = this->config_json.get_bool("AllowUnregisteredUsers", false); + this->allow_pc_nte = this->config_json.get_bool("AllowPCNTE", false); + this->use_temp_licenses_for_prototypes = this->config_json.get_bool("UseTemporaryLicensesForPrototypes", true); + this->allowed_drop_modes_v1_v2_normal = this->config_json.get_int("AllowedDropModesV1V2Normal", 0x1F); + this->allowed_drop_modes_v1_v2_battle = this->config_json.get_int("AllowedDropModesV1V2Battle", 0x07); + this->allowed_drop_modes_v1_v2_challenge = this->config_json.get_int("AllowedDropModesV1V2Challenge", 0x07); + this->allowed_drop_modes_v3_normal = this->config_json.get_int("AllowedDropModesV3Normal", 0x1F); + this->allowed_drop_modes_v3_battle = this->config_json.get_int("AllowedDropModesV3Battle", 0x07); + this->allowed_drop_modes_v3_challenge = this->config_json.get_int("AllowedDropModesV3Challenge", 0x07); + this->allowed_drop_modes_v4_normal = this->config_json.get_int("AllowedDropModesV4Normal", 0x1D); + this->allowed_drop_modes_v4_battle = this->config_json.get_int("AllowedDropModesV4Battle", 0x05); + this->allowed_drop_modes_v4_challenge = this->config_json.get_int("AllowedDropModesV4Challenge", 0x05); + this->default_drop_mode_v1_v2_normal = this->config_json.get_enum("DefaultDropModeV1V2Normal", Lobby::DropMode::CLIENT); + this->default_drop_mode_v1_v2_battle = this->config_json.get_enum("DefaultDropModeV1V2Battle", Lobby::DropMode::CLIENT); + this->default_drop_mode_v1_v2_challenge = this->config_json.get_enum("DefaultDropModeV1V2Challenge", Lobby::DropMode::CLIENT); + this->default_drop_mode_v3_normal = this->config_json.get_enum("DefaultDropModeV3Normal", Lobby::DropMode::CLIENT); + this->default_drop_mode_v3_battle = this->config_json.get_enum("DefaultDropModeV3Battle", Lobby::DropMode::CLIENT); + this->default_drop_mode_v3_challenge = this->config_json.get_enum("DefaultDropModeV3Challenge", Lobby::DropMode::CLIENT); + this->default_drop_mode_v4_normal = this->config_json.get_enum("DefaultDropModeV4Normal", Lobby::DropMode::SERVER_SHARED); + this->default_drop_mode_v4_battle = this->config_json.get_enum("DefaultDropModeV4Battle", Lobby::DropMode::SERVER_SHARED); + this->default_drop_mode_v4_challenge = this->config_json.get_enum("DefaultDropModeV4Challenge", Lobby::DropMode::SERVER_SHARED); if ((this->default_drop_mode_v4_normal == Lobby::DropMode::CLIENT) || (this->default_drop_mode_v4_battle == Lobby::DropMode::CLIENT) || (this->default_drop_mode_v4_challenge == Lobby::DropMode::CLIENT)) { @@ -691,20 +700,20 @@ void ServerState::load_config() { this->quest_flag_persist_mask.update_all(true); try { - for (const auto& flag_id_json : json.get_list("PreventPersistQuestFlags")) { + for (const auto& flag_id_json : this->config_json.get_list("PreventPersistQuestFlags")) { this->quest_flag_persist_mask.clear(flag_id_json->as_int()); } } catch (const out_of_range&) { } - this->persistent_game_idle_timeout_usecs = json.get_int("PersistentGameIdleTimeout", 0); + this->persistent_game_idle_timeout_usecs = this->config_json.get_int("PersistentGameIdleTimeout", 0); this->cheat_mode_behavior = parse_behavior_switch("CheatModeBehavior", BehaviorSwitch::OFF_BY_DEFAULT); - this->default_rare_notifs_enabled_v1_v2 = json.get_bool("RareNotificationsEnabledByDefault", false); + this->default_rare_notifs_enabled_v1_v2 = this->config_json.get_bool("RareNotificationsEnabledByDefault", false); this->default_rare_notifs_enabled_v3_v4 = this->default_rare_notifs_enabled_v1_v2; - this->default_rare_notifs_enabled_v1_v2 = json.get_bool("RareNotificationsEnabledByDefaultV1V2", this->default_rare_notifs_enabled_v1_v2); - this->default_rare_notifs_enabled_v3_v4 = json.get_bool("RareNotificationsEnabledByDefaultV3V4", this->default_rare_notifs_enabled_v3_v4); - this->ep3_send_function_call_enabled = json.get_bool("EnableEpisode3SendFunctionCall", false); - this->catch_handler_exceptions = json.get_bool("CatchHandlerExceptions", true); + this->default_rare_notifs_enabled_v1_v2 = this->config_json.get_bool("RareNotificationsEnabledByDefaultV1V2", this->default_rare_notifs_enabled_v1_v2); + this->default_rare_notifs_enabled_v3_v4 = this->config_json.get_bool("RareNotificationsEnabledByDefaultV3V4", this->default_rare_notifs_enabled_v3_v4); + this->ep3_send_function_call_enabled = this->config_json.get_bool("EnableEpisode3SendFunctionCall", false); + this->catch_handler_exceptions = this->config_json.get_bool("CatchHandlerExceptions", true); auto parse_int_list = +[](const JSON& json) -> vector { vector ret; @@ -714,27 +723,27 @@ void ServerState::load_config() { return ret; }; - this->ep3_infinite_meseta = json.get_bool("Episode3InfiniteMeseta", false); + this->ep3_infinite_meseta = this->config_json.get_bool("Episode3InfiniteMeseta", false); try { - this->ep3_defeat_player_meseta_rewards = parse_int_list(json.at("Episode3DefeatPlayerMeseta")); + this->ep3_defeat_player_meseta_rewards = parse_int_list(this->config_json.at("Episode3DefeatPlayerMeseta")); } catch (const out_of_range&) { this->ep3_defeat_player_meseta_rewards = {300, 400, 500, 600, 700}; } try { - this->ep3_defeat_com_meseta_rewards = parse_int_list(json.get("Episode3DefeatCOMMeseta", JSON::list())); + this->ep3_defeat_com_meseta_rewards = parse_int_list(this->config_json.get("Episode3DefeatCOMMeseta", JSON::list())); } catch (const out_of_range&) { this->ep3_defeat_com_meseta_rewards = {100, 200, 300, 400, 500}; } - this->ep3_final_round_meseta_bonus = json.get_int("Episode3FinalRoundMesetaBonus", 300); - this->ep3_jukebox_is_free = json.get_bool("Episode3JukeboxIsFree", false); - this->ep3_behavior_flags = json.get_int("Episode3BehaviorFlags", false); - this->ep3_card_auction_points = json.get_int("CardAuctionPoints", 0); - this->hide_download_commands = json.get_bool("HideDownloadCommands", true); - this->proxy_allow_save_files = json.get_bool("ProxyAllowSaveFiles", true); - this->proxy_enable_login_options = json.get_bool("ProxyEnableLoginOptions", false); + this->ep3_final_round_meseta_bonus = this->config_json.get_int("Episode3FinalRoundMesetaBonus", 300); + this->ep3_jukebox_is_free = this->config_json.get_bool("Episode3JukeboxIsFree", false); + this->ep3_behavior_flags = this->config_json.get_int("Episode3BehaviorFlags", false); + this->ep3_card_auction_points = this->config_json.get_int("CardAuctionPoints", 0); + this->hide_download_commands = this->config_json.get_bool("HideDownloadCommands", true); + this->proxy_allow_save_files = this->config_json.get_bool("ProxyAllowSaveFiles", true); + this->proxy_enable_login_options = this->config_json.get_bool("ProxyEnableLoginOptions", false); try { - const auto& i = json.at("CardAuctionSize"); + const auto& i = this->config_json.at("CardAuctionSize"); if (i.is_int()) { this->ep3_card_auction_min_size = i.as_int(); this->ep3_card_auction_max_size = this->ep3_card_auction_min_size; @@ -747,58 +756,9 @@ void ServerState::load_config() { this->ep3_card_auction_max_size = 0; } - this->ep3_card_auction_pool.clear(); - try { - for (const auto& it : json.get_dict("CardAuctionPool")) { - uint16_t card_id; - try { - card_id = this->ep3_card_index->definition_for_name_normalized(it.first)->def.card_id; - } catch (const out_of_range&) { - throw runtime_error(string_printf("Ep3 card \"%s\" in auction pool does not exist", it.first.c_str())); - } - this->ep3_card_auction_pool.emplace_back( - CardAuctionPoolEntry{ - .probability = static_cast(it.second->at(0).as_int()), - .card_id = card_id, - .min_price = static_cast(it.second->at(1).as_int())}); - } - } catch (const out_of_range&) { - } - - for (auto& trap_card_ids : this->ep3_trap_card_ids) { - trap_card_ids.clear(); - } - if (this->ep3_card_index) { - try { - const auto& ep3_trap_cards_json = json.get_list("Episode3TrapCards"); - if (!ep3_trap_cards_json.empty()) { - if (ep3_trap_cards_json.size() != 5) { - throw runtime_error("Episode3TrapCards must be a list of 5 lists"); - } - for (size_t trap_type = 0; trap_type < 5; trap_type++) { - auto& trap_card_ids = this->ep3_trap_card_ids[trap_type]; - for (const auto& card_it : ep3_trap_cards_json.at(trap_type)->as_list()) { - try { - const auto& card = this->ep3_card_index->definition_for_name_normalized(card_it->as_string()); - if (card->def.type != Episode3::CardType::ASSIST) { - throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list is not an assist card", name.c_str())); - } - trap_card_ids.emplace_back(card->def.card_id); - } catch (const out_of_range&) { - throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list does not exist", name.c_str())); - } - } - } - } - } catch (const out_of_range&) { - } - } else { - config_log.warning("Episode 3 card definitions missing; cannot set trap card IDs from config"); - } - if (!this->is_replay) { this->ep3_lobby_banners.clear(); - for (const auto& it : json.get("Episode3LobbyBanners", JSON::list()).as_list()) { + for (const auto& it : this->config_json.get("Episode3LobbyBanners", JSON::list()).as_list()) { string path = "system/ep3/banners/" + it->at(2).as_string(); string compressed_gvm_data; @@ -850,7 +810,7 @@ void ServerState::load_config() { } return ret; }; - const auto& categories_json = json.at("Episode3EXResultValues"); + const auto& categories_json = this->config_json.at("Episode3EXResultValues"); this->ep3_default_ex_values = parse_ep3_ex_result_cmd(categories_json.at("Default")); try { this->ep3_tournament_ex_values = parse_ep3_ex_result_cmd(categories_json.at("Tournament")); @@ -864,68 +824,54 @@ void ServerState::load_config() { } } - this->quest_F95E_results.clear(); - this->quest_F95F_results.clear(); - this->quest_F960_success_results.clear(); - this->quest_F960_failure_results = QuestF960Result(); - this->secret_lottery_results.clear(); - if (this->item_name_index(Version::BB_V4)) { - try { - for (const auto& type_it : json.get_list("QuestF95EResultItems")) { - auto& type_res = this->quest_F95E_results.emplace_back(); - for (const auto& difficulty_it : type_it->as_list()) { - auto& difficulty_res = type_res.emplace_back(); - for (const auto& item_it : difficulty_it->as_list()) { - difficulty_res.emplace_back(this->parse_item_description(Version::BB_V4, item_it->as_string())); - } - } + try { + const auto& stack_limits_tables_json = this->config_json.at("ItemStackLimits"); + for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { + try { + Version v = static_cast(v_s); + this->item_stack_limits_tables[v_s] = make_shared( + v, stack_limits_tables_json.at(v_s - NUM_PATCH_VERSIONS)); + } catch (const out_of_range&) { } - } catch (const out_of_range&) { } - try { - for (const auto& it : json.get_list("QuestF95FResultItems")) { - auto& list = it->as_list(); - size_t price = list.at(0)->as_int(); - this->quest_F95F_results.emplace_back(make_pair(price, this->parse_item_description(Version::BB_V4, list.at(1)->as_string()))); - } - } catch (const out_of_range&) { - } - try { - this->quest_F960_failure_results = QuestF960Result(json.at("QuestF960FailureResultItems"), this->item_name_index(Version::BB_V4)); - for (const auto& it : json.get_list("QuestF960SuccessResultItems")) { - this->quest_F960_success_results.emplace_back(*it, this->item_name_index(Version::BB_V4)); - } - } catch (const out_of_range&) { - } - try { - for (const auto& it : json.get_list("SecretLotteryResultItems")) { - this->secret_lottery_results.emplace_back(this->parse_item_description(Version::BB_V4, it->as_string())); - } - } catch (const out_of_range&) { - } - } else { - config_log.warning("BB item name index is missing; cannot load quest reward lists from config"); + } catch (const out_of_range&) { } - this->bb_global_exp_multiplier = json.get_int("BBGlobalEXPMultiplier", 1); + for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { + if (!this->item_stack_limits_tables[v_s]) { + Version v = static_cast(v_s); + if (v == Version::DC_V1_11_2000_PROTOTYPE) { + this->item_stack_limits_tables[v_s] = make_shared( + Version::DC_V1_11_2000_PROTOTYPE, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_11_2000, 999999); + } else if (v_s < static_cast(Version::GC_NTE)) { + this->item_stack_limits_tables[v_s] = make_shared( + v, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2, 999999); + } else { + this->item_stack_limits_tables[v_s] = make_shared( + v, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V3_V4, 999999); + } + } + } - set_log_levels_from_json(json.get("LogLevels", JSON::dict())); + this->bb_global_exp_multiplier = this->config_json.get_int("BBGlobalEXPMultiplier", 1); + + set_log_levels_from_json(this->config_json.get("LogLevels", JSON::dict())); try { - this->run_shell_behavior = json.at("RunInteractiveShell").as_bool() + this->run_shell_behavior = this->config_json.at("RunInteractiveShell").as_bool() ? ServerState::RunShellBehavior::ALWAYS : ServerState::RunShellBehavior::NEVER; } catch (const out_of_range&) { } - this->allow_dc_pc_games = json.get_bool("AllowDCPCGames", true); - this->allow_gc_xb_games = json.get_bool("AllowGCXBGames", true); + this->allow_dc_pc_games = this->config_json.get_bool("AllowDCPCGames", true); + this->allow_gc_xb_games = this->config_json.get_bool("AllowGCXBGames", true); for (auto& order : this->public_lobby_search_orders) { order.clear(); } try { - const auto& orders_json = json.get_list("LobbySearchOrders"); + const auto& orders_json = this->config_json.get_list("LobbySearchOrders"); for (size_t v_s = 0; v_s < orders_json.size(); v_s++) { auto& order = this->public_lobby_search_orders.at(v_s); const auto& order_json = orders_json.at(v_s); @@ -943,7 +889,7 @@ void ServerState::load_config() { } } try { - const auto& events_json = json.get_list("LobbyEvents"); + const auto& events_json = this->config_json.get_list("LobbyEvents"); for (size_t z = 0; z < events_json.size(); z++) { const auto& v = events_json.at(z); uint8_t event = v->is_int() ? v->as_int() : event_for_name(v->as_string()); @@ -958,15 +904,15 @@ void ServerState::load_config() { this->pre_lobby_event = 0; try { - auto v = json.at("MenuEvent"); + auto v = this->config_json.at("MenuEvent"); this->pre_lobby_event = v.is_int() ? v.as_int() : event_for_name(v.as_string()); } catch (const out_of_range&) { } - this->ep3_menu_song = json.get_int("Episode3MenuSong", -1); + this->ep3_menu_song = this->config_json.get_int("Episode3MenuSong", -1); try { - this->quest_category_index = make_shared(json.at("QuestCategories")); + this->quest_category_index = make_shared(this->config_json.at("QuestCategories")); } catch (const exception& e) { throw runtime_error(string_printf( "QuestCategories is missing or invalid in config.json (%s) - see config.example.json for an example", e.what())); @@ -985,9 +931,9 @@ void ServerState::load_config() { "Return to the\nmain menu", MenuItem::Flag::INVISIBLE_IN_INFO_MENU); { auto blank_json = JSON::list(); - const JSON& default_json = json.get("InformationMenuContents", blank_json); - const JSON& v2_json = json.get("InformationMenuContentsV1V2", default_json); - const JSON& v3_json = json.get("InformationMenuContentsV3", default_json); + const JSON& default_json = this->config_json.get("InformationMenuContents", blank_json); + const JSON& v2_json = this->config_json.get("InformationMenuContentsV1V2", default_json); + const JSON& v3_json = this->config_json.get("InformationMenuContentsV3", default_json); uint32_t item_id = 0; for (const auto& item : v2_json.as_list()) { @@ -1018,7 +964,7 @@ void ServerState::load_config() { try { map sorted_jsons; - for (const auto& it : json.at(key).as_dict()) { + for (const auto& it : this->config_json.at(key).as_dict()) { sorted_jsons.emplace(it.first, *it.second); } @@ -1044,7 +990,7 @@ void ServerState::load_config() { this->proxy_destinations_menu_xb = generate_proxy_destinations_menu(this->proxy_destinations_xb, "ProxyDestinations-XB"); try { - const string& netloc_str = json.get_string("ProxyDestination-Patch"); + const string& netloc_str = this->config_json.get_string("ProxyDestination-Patch"); this->proxy_destination_patch = parse_netloc(netloc_str); config_log.info("Patch server proxy is enabled with destination %s", netloc_str.c_str()); for (auto& it : this->name_to_port_config) { @@ -1057,7 +1003,7 @@ void ServerState::load_config() { this->proxy_destination_patch.second = 0; } try { - const string& netloc_str = json.get_string("ProxyDestination-BB"); + const string& netloc_str = this->config_json.get_string("ProxyDestination-BB"); this->proxy_destination_bb = parse_netloc(netloc_str); config_log.info("BB proxy is enabled with destination %s", netloc_str.c_str()); for (auto& it : this->name_to_port_config) { @@ -1070,13 +1016,13 @@ void ServerState::load_config() { this->proxy_destination_bb.second = 0; } - this->welcome_message = json.get_string("WelcomeMessage", ""); - this->pc_patch_server_message = json.get_string("PCPatchServerMessage", ""); - this->bb_patch_server_message = json.get_string("BBPatchServerMessage", ""); + this->welcome_message = this->config_json.get_string("WelcomeMessage", ""); + this->pc_patch_server_message = this->config_json.get_string("PCPatchServerMessage", ""); + this->bb_patch_server_message = this->config_json.get_string("BBPatchServerMessage", ""); this->team_reward_defs_json = nullptr; try { - this->team_reward_defs_json = std::move(json.at("TeamRewards")); + this->team_reward_defs_json = std::move(this->config_json.at("TeamRewards")); } catch (const out_of_range&) { } @@ -1085,14 +1031,14 @@ void ServerState::load_config() { try { string key = "RareEnemyRates-"; key += token_name_for_difficulty(z); - this->rare_enemy_rates_by_difficulty[z] = make_shared(json.at(key)); + this->rare_enemy_rates_by_difficulty[z] = make_shared(this->config_json.at(key)); prev = this->rare_enemy_rates_by_difficulty[z]; } catch (const out_of_range&) { this->rare_enemy_rates_by_difficulty[z] = prev; } } try { - this->rare_enemy_rates_challenge = make_shared(json.at("RareEnemyRates-Challenge")); + this->rare_enemy_rates_challenge = make_shared(this->config_json.at("RareEnemyRates-Challenge")); } catch (const out_of_range&) { this->rare_enemy_rates_challenge = Map::DEFAULT_RARE_ENEMIES; } @@ -1101,7 +1047,7 @@ void ServerState::load_config() { this->min_levels_v4[1] = DEFAULT_MIN_LEVELS_V4_EP2; this->min_levels_v4[2] = DEFAULT_MIN_LEVELS_V4_EP4; try { - for (const auto& ep_it : json.get_dict("BBMinimumLevels")) { + for (const auto& ep_it : this->config_json.get_dict("BBMinimumLevels")) { array levels({0, 0, 0, 0}); for (size_t z = 0; z < 4; z++) { levels[z] = ep_it.second->get_int(z) - 1; @@ -1122,8 +1068,100 @@ void ServerState::load_config() { } } catch (const out_of_range&) { } +} - this->config_loaded = true; +void ServerState::load_config_late() { + this->ep3_card_auction_pool.clear(); + try { + for (const auto& it : this->config_json.get_dict("CardAuctionPool")) { + uint16_t card_id; + try { + card_id = this->ep3_card_index->definition_for_name_normalized(it.first)->def.card_id; + } catch (const out_of_range&) { + throw runtime_error(string_printf("Ep3 card \"%s\" in auction pool does not exist", it.first.c_str())); + } + this->ep3_card_auction_pool.emplace_back( + CardAuctionPoolEntry{ + .probability = static_cast(it.second->at(0).as_int()), + .card_id = card_id, + .min_price = static_cast(it.second->at(1).as_int())}); + } + } catch (const out_of_range&) { + } + + for (auto& trap_card_ids : this->ep3_trap_card_ids) { + trap_card_ids.clear(); + } + if (this->ep3_card_index) { + try { + const auto& ep3_trap_cards_json = this->config_json.get_list("Episode3TrapCards"); + if (!ep3_trap_cards_json.empty()) { + if (ep3_trap_cards_json.size() != 5) { + throw runtime_error("Episode3TrapCards must be a list of 5 lists"); + } + for (size_t trap_type = 0; trap_type < 5; trap_type++) { + auto& trap_card_ids = this->ep3_trap_card_ids[trap_type]; + for (const auto& card_it : ep3_trap_cards_json.at(trap_type)->as_list()) { + try { + const auto& card = this->ep3_card_index->definition_for_name_normalized(card_it->as_string()); + if (card->def.type != Episode3::CardType::ASSIST) { + throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list is not an assist card", name.c_str())); + } + trap_card_ids.emplace_back(card->def.card_id); + } catch (const out_of_range&) { + throw runtime_error(string_printf("Ep3 card \"%s\" in trap card list does not exist", name.c_str())); + } + } + } + } + } catch (const out_of_range&) { + } + } else { + config_log.warning("Episode 3 card definitions missing; cannot set trap card IDs from config"); + } + + this->quest_F95E_results.clear(); + this->quest_F95F_results.clear(); + this->quest_F960_success_results.clear(); + this->quest_F960_failure_results = QuestF960Result(); + this->secret_lottery_results.clear(); + if (this->item_name_index(Version::BB_V4)) { + try { + for (const auto& type_it : this->config_json.get_list("QuestF95EResultItems")) { + auto& type_res = this->quest_F95E_results.emplace_back(); + for (const auto& difficulty_it : type_it->as_list()) { + auto& difficulty_res = type_res.emplace_back(); + for (const auto& item_it : difficulty_it->as_list()) { + difficulty_res.emplace_back(this->parse_item_description(Version::BB_V4, item_it->as_string())); + } + } + } + } catch (const out_of_range&) { + } + try { + for (const auto& it : this->config_json.get_list("QuestF95FResultItems")) { + auto& list = it->as_list(); + size_t price = list.at(0)->as_int(); + this->quest_F95F_results.emplace_back(make_pair(price, this->parse_item_description(Version::BB_V4, list.at(1)->as_string()))); + } + } catch (const out_of_range&) { + } + try { + this->quest_F960_failure_results = QuestF960Result(this->config_json.at("QuestF960FailureResultItems"), this->item_name_index(Version::BB_V4)); + for (const auto& it : this->config_json.get_list("QuestF960SuccessResultItems")) { + this->quest_F960_success_results.emplace_back(*it, this->item_name_index(Version::BB_V4)); + } + } catch (const out_of_range&) { + } + try { + for (const auto& it : this->config_json.get_list("SecretLotteryResultItems")) { + this->secret_lottery_results.emplace_back(this->parse_item_description(Version::BB_V4, it->as_string())); + } + } catch (const out_of_range&) { + } + } else { + config_log.warning("BB item name index is missing; cannot load quest reward lists from config"); + } } void ServerState::load_bb_private_keys(bool from_non_event_thread) { @@ -1366,28 +1404,30 @@ void ServerState::load_word_select_table(bool from_non_event_thread) { } shared_ptr ServerState::create_item_name_index_for_version( - Version version, shared_ptr pmt, shared_ptr text_index) { - switch (version) { + shared_ptr pmt, + shared_ptr limits, + shared_ptr text_index) const { + switch (limits->version) { case Version::DC_NTE: - return make_shared(version, pmt, text_index->get(Version::DC_NTE, 0, 2)); + return make_shared(pmt, limits, text_index->get(Version::DC_NTE, 0, 2)); case Version::DC_V1_11_2000_PROTOTYPE: - return make_shared(version, pmt, text_index->get(Version::DC_V1_11_2000_PROTOTYPE, 1, 2)); + return make_shared(pmt, limits, text_index->get(Version::DC_V1_11_2000_PROTOTYPE, 1, 2)); case Version::DC_V1: - return make_shared(version, pmt, text_index->get(Version::DC_V1, 1, 2)); + return make_shared(pmt, limits, text_index->get(Version::DC_V1, 1, 2)); case Version::DC_V2: - return make_shared(version, pmt, text_index->get(Version::DC_V2, 1, 3)); + return make_shared(pmt, limits, text_index->get(Version::DC_V2, 1, 3)); case Version::PC_NTE: - return make_shared(version, pmt, text_index->get(Version::PC_NTE, 1, 3)); + return make_shared(pmt, limits, text_index->get(Version::PC_NTE, 1, 3)); case Version::PC_V2: - return make_shared(version, pmt, text_index->get(Version::PC_V2, 1, 3)); + return make_shared(pmt, limits, text_index->get(Version::PC_V2, 1, 3)); case Version::GC_NTE: - return make_shared(version, pmt, text_index->get(Version::GC_NTE, 1, 0)); + return make_shared(pmt, limits, text_index->get(Version::GC_NTE, 1, 0)); case Version::GC_V3: - return make_shared(version, pmt, text_index->get(Version::GC_V3, 1, 0)); + return make_shared(pmt, limits, text_index->get(Version::GC_V3, 1, 0)); case Version::XB_V3: - return make_shared(version, pmt, text_index->get(Version::XB_V3, 1, 0)); + return make_shared(pmt, limits, text_index->get(Version::XB_V3, 1, 0)); case Version::BB_V4: - return make_shared(version, pmt, text_index->get(Version::BB_V4, 1, 1)); + return make_shared(pmt, limits, text_index->get(Version::BB_V4, 1, 1)); default: return nullptr; } @@ -1399,7 +1439,8 @@ void ServerState::load_item_name_indexes(bool from_non_event_thread) { for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) { Version v = static_cast(v_s); config_log.info("Generating item name index for %s", name_for_enum(v)); - new_indexes[v_s] = this->create_item_name_index_for_version(v, this->item_parameter_table(v), this->text_index); + new_indexes[v_s] = this->create_item_name_index_for_version( + this->item_parameter_table(v), this->item_stack_limits(v), this->text_index); } auto set = [s = this->shared_from_this(), new_indexes = std::move(new_indexes)]() { @@ -1668,6 +1709,7 @@ void ServerState::create_default_lobbies() { void ServerState::load_all() { this->collect_network_addresses(); + this->load_config_early(); this->load_bb_private_keys(false); this->load_licenses(false); this->clear_map_file_caches(); @@ -1686,7 +1728,7 @@ void ServerState::load_all() { this->load_item_definitions(false); this->load_item_name_indexes(false); this->load_drop_tables(false); - this->load_config(); + this->load_config_late(); this->load_teams(false); this->load_quest_index(false); } diff --git a/src/ServerState.hh b/src/ServerState.hh index ba327704..09f9b061 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -66,8 +66,9 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr base; std::string config_filename; + JSON config_json; bool is_replay = false; - bool config_loaded = false; + bool one_time_config_loaded = false; bool default_lobbies_created = false; std::string name; @@ -148,6 +149,7 @@ struct ServerState : public std::enable_shared_from_this { std::array, 4> weapon_random_sets; std::shared_ptr tekker_adjustment_set; std::array, NUM_VERSIONS> item_parameter_tables; + std::array, NUM_VERSIONS> item_stack_limits_tables; std::shared_ptr mag_evolution_table; std::shared_ptr text_index; std::array, NUM_VERSIONS> item_name_indexes; @@ -281,9 +283,8 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr item_parameter_table(Version version) const; std::shared_ptr item_parameter_table_for_encode(Version version) const; - void set_item_parameter_table(Version version, std::shared_ptr table); + std::shared_ptr item_stack_limits(Version version) const; std::shared_ptr item_name_index(Version version) const; - void set_item_name_index(Version version, std::shared_ptr index); std::string describe_item(Version version, const ItemData& item, bool include_color_codes) const; ItemData parse_item_description(Version version, const std::string& description) const; @@ -325,7 +326,8 @@ struct ServerState : public std::enable_shared_from_this { // argument must be called only from the event thread. void create_default_lobbies(); void collect_network_addresses(); - void load_config(); + void load_config_early(); + void load_config_late(); void load_bb_private_keys(bool from_non_event_thread); void load_licenses(bool from_non_event_thread); void load_teams(bool from_non_event_thread); @@ -334,8 +336,10 @@ struct ServerState : public std::enable_shared_from_this { void load_battle_params(bool from_non_event_thread); void load_level_table(bool from_non_event_thread); void load_text_index(bool from_non_event_thread); - static std::shared_ptr create_item_name_index_for_version( - Version version, std::shared_ptr pmt, std::shared_ptr text_index); + std::shared_ptr create_item_name_index_for_version( + std::shared_ptr pmt, + std::shared_ptr limits, + std::shared_ptr text_index) const; void load_item_name_indexes(bool from_non_event_thread); void load_drop_tables(bool from_non_event_thread); void load_item_definitions(bool from_non_event_thread); diff --git a/src/StaticGameData.cc b/src/StaticGameData.cc index 150b2f7a..77f8a5a3 100644 --- a/src/StaticGameData.cc +++ b/src/StaticGameData.cc @@ -506,23 +506,6 @@ uint8_t language_code_for_char(char language_char) { } } -size_t max_stack_size_for_item(Version version, uint8_t data0, uint8_t data1) { - if (data0 == 4) { - return 999999; - } - if (data0 == 3) { - if (version == Version::DC_V1_11_2000_PROTOTYPE) { - // All tool items are stackable up to x10 on this version - return 10; - } else if ((data1 < 9) && (data1 != 2)) { - return 10; - } else if (data1 == 0x10) { - return 99; - } - } - return 1; -} - const vector tech_id_to_name = { "foie", "gifoie", "rafoie", "barta", "gibarta", "rabarta", diff --git a/src/StaticGameData.hh b/src/StaticGameData.hh index b1e534d7..8a6950b3 100644 --- a/src/StaticGameData.hh +++ b/src/StaticGameData.hh @@ -33,8 +33,6 @@ enum class GameMode { const char* name_for_mode(GameMode mode); const char* abbreviation_for_mode(GameMode mode); -size_t max_stack_size_for_item(Version version, uint8_t data0, uint8_t data1); - extern const std::vector tech_id_to_name; extern const std::unordered_map name_to_tech_id; diff --git a/src/Text.hh b/src/Text.hh index cf5bc143..adf36147 100644 --- a/src/Text.hh +++ b/src/Text.hh @@ -50,6 +50,10 @@ std::string tt_encode_marked_optional(const std::string& utf8, uint8_t default_l std::string tt_encode_marked(const std::string& utf8, uint8_t default_language, bool is_utf16); std::string tt_decode_marked(const std::string& data, uint8_t default_language, bool is_utf16); +char marker_for_language_code(uint8_t language_code); +bool is_language_marker_sjis_8859(char marker); +bool is_language_marker_utf16(char marker); + // Packed array object for use in protocol structs template @@ -582,7 +586,3 @@ std::string remove_color(const std::string& s); std::string strip_color(const std::string& s); std::string escape_player_name(const std::string& name); - -char marker_for_language_code(uint8_t language_code); -bool is_language_marker_sjis_8859(char marker); -bool is_language_marker_utf16(char marker); diff --git a/system/config.example.json b/system/config.example.json index 10ddaf0b..36ad619a 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -667,6 +667,27 @@ [0x40, "download-ep3", "Download", "$E$C6Quests to download\nto your Memory Card"], ], + // Item stack limits. Note that changing these does not affect the client's + // behavior automatically - this only exists to allow the server to understand + // the behavior of clients that are already patched with different stack + // limits. The ToolLimits list is indexed by data1[1] (that is, the second + // byte of the item data); for items beyond the end of the list, the last + // entry's value is used. + "ItemStackLimits": [ + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1]}, // DC NTE + {"MesetaLimit": 999999, "ToolLimits": [10]}, // DC 11/2000 + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1]}, // DC V1 + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1]}, // DC V2 + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1]}, // PC NTE + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1]}, // PC + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // GC NTE + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // GC + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // GC Ep3 NTE + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // GC Ep3 + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // XB + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // BB + ], + // Quest result item definitions for opcode F95E (used in Black Paper's // Dangerous Deal 1 and 2). This list is indexed as [reward_type][difficulty]. // Reward types 0, 1, 2, and 4 are used by vanilla PSOBB; other reward types diff --git a/tests/config.json b/tests/config.json index 257e0025..d0e83c90 100644 --- a/tests/config.json +++ b/tests/config.json @@ -198,6 +198,21 @@ [0x40, "download-ep3", "Download", "$E$C6Quests to download\nto your Memory Card"], ], + "ItemStackLimits": [ + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1]}, // DC NTE + {"MesetaLimit": 999999, "ToolLimits": [10]}, // DC 11/2000 + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1]}, // DC V1 + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1]}, // DC V2 + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1]}, // PC NTE + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1]}, // PC + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // GC NTE + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // GC + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // GC Ep3 NTE + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // GC Ep3 + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // XB + {"MesetaLimit": 999999, "ToolLimits": [10, 10, 1, 10, 10, 10, 10, 10, 10, 1, 1, 1, 1, 1, 1, 1, 99, 1]}, // BB + ], + "QuestF95EResultItems": [ [ ["009000", "009001", "009002", "009003", "009004", "009005", "009006", "009007", "009008", "00B400", "01014E", "010307", "010341", "040000", "040000", "040000", "040000", "040000", "040000", "040000", "040000", "040000"],