allow overriding stack sizes
This commit is contained in:
+6
-5
@@ -18,15 +18,16 @@ ItemCreator::ItemCreator(
|
||||
shared_ptr<const WeaponRandomSet> weapon_random_set,
|
||||
shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set,
|
||||
shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
Version version,
|
||||
std::shared_ptr<const ItemData::StackLimits> stack_limits,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
uint8_t section_id,
|
||||
uint32_t random_seed,
|
||||
shared_ptr<const BattleRules> 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;
|
||||
|
||||
+2
-1
@@ -19,7 +19,7 @@ public:
|
||||
std::shared_ptr<const WeaponRandomSet> weapon_random_set,
|
||||
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
Version version,
|
||||
std::shared_ptr<const ItemData::StackLimits> stack_limits,
|
||||
Episode episode,
|
||||
GameMode mode,
|
||||
uint8_t difficulty,
|
||||
@@ -56,6 +56,7 @@ public:
|
||||
private:
|
||||
PrefixedLogger log;
|
||||
Version version;
|
||||
std::shared_ptr<const ItemData::StackLimits> stack_limits;
|
||||
Episode episode;
|
||||
GameMode mode;
|
||||
uint8_t difficulty;
|
||||
|
||||
+53
-20
@@ -8,6 +8,39 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
const vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_11_2000(
|
||||
{10});
|
||||
const vector<uint8_t> ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2(
|
||||
{10, 10, 1, 10, 10, 10, 10, 10, 10, 1});
|
||||
const vector<uint8_t> 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<uint8_t>& 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<size_t>(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;
|
||||
}
|
||||
|
||||
|
||||
+31
-11
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/JSON.hh>
|
||||
#include <string>
|
||||
|
||||
#include "Text.hh"
|
||||
@@ -54,7 +55,26 @@ struct ItemMagStats {
|
||||
}
|
||||
};
|
||||
|
||||
struct ItemData { // 0x14 bytes
|
||||
struct ItemData {
|
||||
struct StackLimits {
|
||||
Version version;
|
||||
std::vector<uint8_t> max_tool_stack_sizes_by_data1_1;
|
||||
uint32_t max_meseta_stack_size;
|
||||
|
||||
StackLimits(Version version, const std::vector<uint8_t>& 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<uint8_t> DEFAULT_TOOL_LIMITS_DC_11_2000;
|
||||
static const std::vector<uint8_t> DEFAULT_TOOL_LIMITS_V1_V2;
|
||||
static const std::vector<uint8_t> 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;
|
||||
|
||||
+10
-10
@@ -5,16 +5,16 @@
|
||||
using namespace std;
|
||||
|
||||
ItemNameIndex::ItemNameIndex(
|
||||
Version version,
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table,
|
||||
std::shared_ptr<const ItemData::StackLimits> limits,
|
||||
const std::vector<std::string>& 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",
|
||||
|
||||
@@ -20,8 +20,8 @@ public:
|
||||
};
|
||||
|
||||
ItemNameIndex(
|
||||
Version version,
|
||||
std::shared_ptr<const ItemParameterTable> pmt,
|
||||
std::shared_ptr<const ItemData::StackLimits> limits,
|
||||
const std::vector<std::string>& 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<const ItemParameterTable> item_parameter_table;
|
||||
std::shared_ptr<const ItemData::StackLimits> limits;
|
||||
|
||||
std::unordered_map<uint32_t, std::shared_ptr<const ItemMetadata>> primary_identifier_index;
|
||||
std::map<std::string, std::shared_ptr<const ItemMetadata>> name_index;
|
||||
|
||||
+3
-3
@@ -111,9 +111,9 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<PSOLFGE
|
||||
}
|
||||
armor.data.data1[5]++;
|
||||
|
||||
} else if (item.data.is_wrapped(c->version())) {
|
||||
} 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<Client> c, size_t item_index, shared_ptr<PSOLFGE
|
||||
if (should_delete_item) {
|
||||
// Allow overdrafting meseta if the client is not BB, since the server isn't
|
||||
// informed when meseta is added or removed from the bank.
|
||||
player->remove_item(item.data.id, 1, c->version());
|
||||
player->remove_item(item.data.id, 1, *s->item_stack_limits(c->version()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+15
-13
@@ -1739,11 +1739,13 @@ Action a_describe_item(
|
||||
|
||||
Action a_name_all_items(
|
||||
"name-all-items", nullptr, +[](Arguments&) {
|
||||
auto s = make_shared<ServerState>();
|
||||
auto s = make_shared<ServerState>("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<uint32_t> 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<Version>(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<size_t>("min-count", 1);
|
||||
string quest_name = args.get<string>("quest", false);
|
||||
|
||||
ServerState s("system/config.json");
|
||||
auto s = make_shared<ServerState>("system/config.json");
|
||||
shared_ptr<const VersionedQuest> 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<const Map::RareEnemyRates> 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);
|
||||
|
||||
@@ -444,7 +444,7 @@ PlayerRecordsBB_Challenge::operator PlayerRecordsV3_Challenge<false>() 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;
|
||||
|
||||
@@ -101,8 +101,8 @@ struct PlayerBank {
|
||||
/* 0008 */ parray<PlayerBankItem, 200> 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();
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -3740,7 +3740,7 @@ static void on_DF_BB(shared_ptr<Client> 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<Client> 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<Client> to_c, shared_ptr<Client> 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<Client> 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<Client> 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<Client> 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;
|
||||
|
||||
+78
-52
@@ -1385,9 +1385,10 @@ static void on_player_drop_item(shared_ptr<Client> 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<Client> 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<Client> c, uint8_t command, uint
|
||||
auto l = c->require_lobby();
|
||||
if (l->base_version == Version::BB_V4) {
|
||||
const auto& cmd = check_size_t<G_SplitStackedItem_BB_6xC3>(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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> c, uint8_t, uint8_t, void* data, size_t size) {
|
||||
const auto& cmd = check_size_t<G_ItemRewardRequest_BB_6xCA>(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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<G_BuyShopItem_BB_6xB7>(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<Client> 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<Client> 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<G_ExchangeItemInQuest_BB_6xD5>(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<Client> 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<G_WrapItem_BB_6xD6>(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<Client> 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<G_PaganiniPhotonDropExchange_BB_6xD7>(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<Client> c,
|
||||
auto l = c->require_lobby();
|
||||
if (l->is_game() && (l->base_version == Version::BB_V4)) {
|
||||
const auto& cmd = check_size_t<G_AddSRankWeaponSpecial_BB_6xD8>(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<Client> 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<Client> 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<Client> 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<Client> 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<G_ExchangePhotonCrystals_BB_6xDF>(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<Client> 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<Client> 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<Client> 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<Client> 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<Client> 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<G_MomokaItemExchange_BB_6xD9>(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<Client> 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<Client> 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<G_UpgradeWeaponAttribute_BB_6xDA>(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<Client> 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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -233,8 +233,8 @@ struct PSOBBCharacterFile {
|
||||
const PlayerDispDataBBPreview& preview,
|
||||
std::shared_ptr<const LevelTable> 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);
|
||||
|
||||
|
||||
+4
-1
@@ -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") {
|
||||
|
||||
+228
-186
@@ -385,6 +385,14 @@ shared_ptr<const ItemParameterTable> ServerState::item_parameter_table_for_encod
|
||||
return this->item_parameter_table(is_v1(version) ? Version::PC_V2 : version);
|
||||
}
|
||||
|
||||
shared_ptr<const ItemData::StackLimits> ServerState::item_stack_limits(Version version) const {
|
||||
auto ret = this->item_stack_limits_tables.at(static_cast<size_t>(version));
|
||||
if (ret == nullptr) {
|
||||
throw runtime_error("no item stack limits table exists for this version");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
shared_ptr<const ItemNameIndex> ServerState::item_name_index(Version version) const {
|
||||
auto ret = this->item_name_indexes.at(static_cast<size_t>(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<PortConfiguration>& port_configs) {
|
||||
void ServerState::set_port_configuration(const vector<PortConfiguration>& 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("<local>");
|
||||
this->all_addresses.emplace("<local>", 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("<external>");
|
||||
this->all_addresses.emplace("<external>", 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<uint32_t> {
|
||||
vector<uint32_t> 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<uint64_t>(it.second->at(0).as_int()),
|
||||
.card_id = card_id,
|
||||
.min_price = static_cast<uint16_t>(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<Version>(v_s);
|
||||
this->item_stack_limits_tables[v_s] = make_shared<ItemData::StackLimits>(
|
||||
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<Version>(v_s);
|
||||
if (v == Version::DC_V1_11_2000_PROTOTYPE) {
|
||||
this->item_stack_limits_tables[v_s] = make_shared<ItemData::StackLimits>(
|
||||
Version::DC_V1_11_2000_PROTOTYPE, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_DC_11_2000, 999999);
|
||||
} else if (v_s < static_cast<size_t>(Version::GC_NTE)) {
|
||||
this->item_stack_limits_tables[v_s] = make_shared<ItemData::StackLimits>(
|
||||
v, ItemData::StackLimits::DEFAULT_TOOL_LIMITS_V1_V2, 999999);
|
||||
} else {
|
||||
this->item_stack_limits_tables[v_s] = make_shared<ItemData::StackLimits>(
|
||||
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<QuestCategoryIndex>(json.at("QuestCategories"));
|
||||
this->quest_category_index = make_shared<QuestCategoryIndex>(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<string, const JSON&> 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<Map::RareEnemyRates>(json.at(key));
|
||||
this->rare_enemy_rates_by_difficulty[z] = make_shared<Map::RareEnemyRates>(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<Map::RareEnemyRates>(json.at("RareEnemyRates-Challenge"));
|
||||
this->rare_enemy_rates_challenge = make_shared<Map::RareEnemyRates>(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<size_t, 4> 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<uint64_t>(it.second->at(0).as_int()),
|
||||
.card_id = card_id,
|
||||
.min_price = static_cast<uint16_t>(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<ItemNameIndex> ServerState::create_item_name_index_for_version(
|
||||
Version version, shared_ptr<const ItemParameterTable> pmt, shared_ptr<const TextIndex> text_index) {
|
||||
switch (version) {
|
||||
shared_ptr<const ItemParameterTable> pmt,
|
||||
shared_ptr<const ItemData::StackLimits> limits,
|
||||
shared_ptr<const TextIndex> text_index) const {
|
||||
switch (limits->version) {
|
||||
case Version::DC_NTE:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::DC_NTE, 0, 2));
|
||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_NTE, 0, 2));
|
||||
case Version::DC_V1_11_2000_PROTOTYPE:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::DC_V1_11_2000_PROTOTYPE, 1, 2));
|
||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_V1_11_2000_PROTOTYPE, 1, 2));
|
||||
case Version::DC_V1:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::DC_V1, 1, 2));
|
||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_V1, 1, 2));
|
||||
case Version::DC_V2:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::DC_V2, 1, 3));
|
||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::DC_V2, 1, 3));
|
||||
case Version::PC_NTE:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::PC_NTE, 1, 3));
|
||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::PC_NTE, 1, 3));
|
||||
case Version::PC_V2:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::PC_V2, 1, 3));
|
||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::PC_V2, 1, 3));
|
||||
case Version::GC_NTE:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::GC_NTE, 1, 0));
|
||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::GC_NTE, 1, 0));
|
||||
case Version::GC_V3:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::GC_V3, 1, 0));
|
||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::GC_V3, 1, 0));
|
||||
case Version::XB_V3:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::XB_V3, 1, 0));
|
||||
return make_shared<ItemNameIndex>(pmt, limits, text_index->get(Version::XB_V3, 1, 0));
|
||||
case Version::BB_V4:
|
||||
return make_shared<ItemNameIndex>(version, pmt, text_index->get(Version::BB_V4, 1, 1));
|
||||
return make_shared<ItemNameIndex>(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<Version>(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);
|
||||
}
|
||||
|
||||
+10
-6
@@ -66,8 +66,9 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::shared_ptr<struct event_base> 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<ServerState> {
|
||||
std::array<std::shared_ptr<const WeaponRandomSet>, 4> weapon_random_sets;
|
||||
std::shared_ptr<const TekkerAdjustmentSet> tekker_adjustment_set;
|
||||
std::array<std::shared_ptr<const ItemParameterTable>, NUM_VERSIONS> item_parameter_tables;
|
||||
std::array<std::shared_ptr<const ItemData::StackLimits>, NUM_VERSIONS> item_stack_limits_tables;
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table;
|
||||
std::shared_ptr<const TextIndex> text_index;
|
||||
std::array<std::shared_ptr<const ItemNameIndex>, NUM_VERSIONS> item_name_indexes;
|
||||
@@ -281,9 +283,8 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table(Version version) const;
|
||||
std::shared_ptr<const ItemParameterTable> item_parameter_table_for_encode(Version version) const;
|
||||
void set_item_parameter_table(Version version, std::shared_ptr<const ItemParameterTable> table);
|
||||
std::shared_ptr<const ItemData::StackLimits> item_stack_limits(Version version) const;
|
||||
std::shared_ptr<const ItemNameIndex> item_name_index(Version version) const;
|
||||
void set_item_name_index(Version version, std::shared_ptr<const ItemNameIndex> 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<ServerState> {
|
||||
// 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<ServerState> {
|
||||
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<ItemNameIndex> create_item_name_index_for_version(
|
||||
Version version, std::shared_ptr<const ItemParameterTable> pmt, std::shared_ptr<const TextIndex> text_index);
|
||||
std::shared_ptr<ItemNameIndex> create_item_name_index_for_version(
|
||||
std::shared_ptr<const ItemParameterTable> pmt,
|
||||
std::shared_ptr<const ItemData::StackLimits> limits,
|
||||
std::shared_ptr<const TextIndex> 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);
|
||||
|
||||
@@ -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<string> tech_id_to_name = {
|
||||
"foie", "gifoie", "rafoie",
|
||||
"barta", "gibarta", "rabarta",
|
||||
|
||||
@@ -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<std::string> tech_id_to_name;
|
||||
extern const std::unordered_map<std::string, uint8_t> name_to_tech_id;
|
||||
|
||||
|
||||
+4
-4
@@ -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 <typename ItemT, size_t Count>
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user