implement 6xE2 subcommand

This commit is contained in:
Martin Michelsen
2023-12-20 14:59:48 -08:00
parent f048a4f5fb
commit 82aaf4cd34
8 changed files with 217 additions and 23 deletions
+15 -8
View File
@@ -5647,9 +5647,9 @@ struct G_EnemyEXPRequest_BB_6xC8 {
parray<uint8_t, 3> unused;
} __packed__;
// 6xC9: Request meseta reward from quest (BB; handled by server)
// 6xC9: Adjust player Meseta (BB; handled by server)
struct G_MesetaRewardRequest_BB_6xC9 {
struct G_AdjustPlayerMeseta_BB_6xC9 {
G_UnusedHeader header;
le_int32_t amount = 0;
} __packed__;
@@ -5866,19 +5866,26 @@ struct G_ExchangePhotonTickets_BB_6xE1 {
le_uint16_t unknown_a5 = 0; // argsA[4]
} __packed__;
// 6xE2: Coren actions (BB)
// 6xE2: Get Meseta slot prize (BB)
// The client sends this when it executes an F960 quest opcode.
struct G_CorenActions_BB_6xE2 {
struct G_GetMesetaSlotPrize_BB_6xE2 {
G_ClientIDHeader header;
parray<uint8_t, 12> unknown_a1; // TODO: There might be uint16_ts and uint32_ts in here.
uint8_t result_tier; // This contains the argument value from the F960 opcode
uint8_t floor;
uint8_t unknown_a2;
uint8_t unused;
le_float x; // TODO: Verify this guess
le_float z; // TODO: Verify this guess
} __packed__;
// 6xE3: Coren actions result (BB)
// 6xE3: Set Meseta slot prize result (BB)
// The client only uses this to populate the <meseta_slot_prize> quest text
// replacement token.
struct G_CorenActionsResult_BB_6xE3 {
struct G_SetMesetaSlotPrizeResult_BB_6xE3 {
G_ClientIDHeader header;
ItemData item_data;
ItemData item;
} __packed__;
// 6xE4: Invalid subcommand
+4 -2
View File
@@ -390,9 +390,11 @@ ItemData ItemNameIndex::parse_item_description(Version version, const std::strin
}
} catch (const exception& ed) {
if (strcmp(e1.what(), e2.what())) {
throw runtime_error(string_printf("cannot parse item description (as text 1: %s) (as text 2: %s) (as data: %s)", e1.what(), e2.what(), ed.what()));
throw runtime_error(string_printf("cannot parse item description \"%s\" in %s (as text 1: %s) (as text 2: %s) (as data: %s)",
desc.c_str(), name_for_enum(version), e1.what(), e2.what(), ed.what()));
} else {
throw runtime_error(string_printf("cannot parse item description (as text: %s) (as data: %s)", e1.what(), ed.what()));
throw runtime_error(string_printf("cannot parse item description \"%s\" in %s (as text: %s) (as data: %s)",
desc.c_str(), name_for_enum(version), e1.what(), ed.what()));
}
}
}
+56 -4
View File
@@ -2082,8 +2082,8 @@ static void on_enemy_exp_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, cons
}
}
void on_meseta_reward_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
const auto& cmd = check_size_t<G_MesetaRewardRequest_BB_6xC9>(data, size);
void on_adjust_player_meseta_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
const auto& cmd = check_size_t<G_AdjustPlayerMeseta_BB_6xC9>(data, size);
auto p = c->character();
if (cmd.amount < 0) {
@@ -2692,6 +2692,58 @@ static void on_quest_F95F_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, cons
}
}
static void on_quest_F960_result_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == Version::BB_V4)) {
const auto& cmd = check_size_t<G_GetMesetaSlotPrize_BB_6xE2>(data, size);
auto s = c->require_server_state();
auto p = c->character();
time_t t_secs = now() / 1000000;
struct tm t_parsed;
gmtime_r(&t_secs, &t_parsed);
size_t weekday = t_parsed.tm_wday;
ItemData item;
for (size_t num_failures = 0; num_failures <= cmd.result_tier; num_failures++) {
size_t tier = cmd.result_tier - num_failures;
const auto& results = s->quest_F960_success_results.at(tier);
uint64_t probability = results.base_probability + num_failures * results.probability_upgrade;
if (random_object<uint32_t>() <= probability) {
c->log.info("Tier %zu yielded a prize", tier);
const auto& result_items = results.results.at(weekday);
item = result_items[random_object<uint32_t>() % result_items.size()];
break;
} else {
c->log.info("Tier %zu did not yield a prize", tier);
}
}
if (item.empty()) {
c->log.info("Choosing result from failure tier");
const auto& result_items = s->quest_F960_failure_results.results.at(weekday);
item = result_items[random_object<uint32_t>() % result_items.size()];
}
if (item.empty()) {
throw runtime_error("no item produced, even from failure tier");
}
// The client sends a 6xC9 to remove Meseta before sending 6xE2, so we don't
// have to deal with Meseta here.
item.id = l->generate_item_id(c->lobby_client_id);
p->add_item(item);
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
G_SetMesetaSlotPrizeResult_BB_6xE3 cmd_6xE3 = {{0xE3, sizeof(G_SetMesetaSlotPrizeResult_BB_6xE3) >> 2, 0x0000}, item};
send_command_t(c, 0x60, 0x00, cmd_6xE3);
if (c->log.should_log(LogLevel::INFO)) {
string name = s->item_name_index->describe_item(c->version(), item);
c->log.info("Awarded item %s", name.c_str());
}
}
}
static void on_momoka_item_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game() && (l->base_version == Version::BB_V4) && l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) {
@@ -2984,7 +3036,7 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
/* 6xC6 */ {0x00, 0x00, 0xC6, on_steal_exp_bb},
/* 6xC7 */ {0x00, 0x00, 0xC7, on_charge_attack_bb},
/* 6xC8 */ {0x00, 0x00, 0xC8, on_enemy_exp_request_bb},
/* 6xC9 */ {0x00, 0x00, 0xC9, on_meseta_reward_request_bb},
/* 6xC9 */ {0x00, 0x00, 0xC9, on_adjust_player_meseta_bb},
/* 6xCA */ {0x00, 0x00, 0xCA, on_item_reward_request_bb},
/* 6xCB */ {0x00, 0x00, 0xCB, on_transfer_item_via_mail_message_bb},
/* 6xCC */ {0x00, 0x00, 0xCC, on_exchange_item_for_team_points_bb},
@@ -3009,7 +3061,7 @@ const SubcommandDefinition subcommand_definitions[0x100] = {
/* 6xDF */ {0x00, 0x00, 0xDF, on_photon_crystal_exchange_bb},
/* 6xE0 */ {0x00, 0x00, 0xE0, on_quest_F95E_result_bb},
/* 6xE1 */ {0x00, 0x00, 0xE1, on_quest_F95F_result_bb},
/* 6xE2 */ {0x00, 0x00, 0xE2, nullptr},
/* 6xE2 */ {0x00, 0x00, 0xE2, on_quest_F960_result_bb},
/* 6xE3 */ {0x00, 0x00, 0xE3, nullptr},
/* 6xE4 */ {0x00, 0x00, 0xE4, nullptr},
/* 6xE5 */ {0x00, 0x00, 0xE5, nullptr},
+1
View File
@@ -297,6 +297,7 @@ Proxy session commands:\n\
} else if (type == "level-table") {
this->state->load_level_table();
} else if (type == "item-tables") {
this->state->load_item_name_index();
this->state->load_item_tables();
} else if (type == "word-select") {
this->state->load_word_select_table();
+27 -7
View File
@@ -17,6 +17,18 @@
using namespace std;
ServerState::QuestF960Result::QuestF960Result(const JSON& json, std::shared_ptr<const ItemNameIndex> name_index) {
static const array<string, 7> day_names = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
this->meseta_cost = json.get_int("MesetaCost", 0);
this->base_probability = json.get_int("BaseProbability", 0);
this->probability_upgrade = json.get_int("ProbabilityUpgrade", 0);
for (size_t day = 0; day < 7; day++) {
for (const auto& item_it : json.get_list(day_names[day])) {
this->results[day].emplace_back(name_index->parse_item_description(Version::BB_V4, item_it->as_string()));
}
}
}
ServerState::ServerState(shared_ptr<struct event_base> base, const string& config_filename, bool is_replay)
: base(base),
config_filename(config_filename),
@@ -125,6 +137,7 @@ void ServerState::init() {
// Load all the necessary data
auto config = this->load_config();
this->collect_network_addresses();
this->load_item_name_index();
this->parse_config(config, false);
this->load_bb_private_keys();
this->load_licenses();
@@ -806,8 +819,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
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()) {
string data = parse_data_string(item_it->as_string());
difficulty_res.emplace_back(ItemData::from_data(data));
difficulty_res.emplace_back(this->item_name_index->parse_item_description(Version::BB_V4, item_it->as_string()));
}
}
}
@@ -818,16 +830,22 @@ void ServerState::parse_config(const JSON& json, bool is_reload) {
for (const auto& it : json.get_list("QuestF95FResultItems")) {
auto& list = it->as_list();
size_t price = list.at(0)->as_int();
string data = parse_data_string(list.at(1)->as_string());
this->quest_F95F_results.emplace_back(make_pair(price, ItemData::from_data(data)));
this->quest_F95F_results.emplace_back(make_pair(price, this->item_name_index->parse_item_description(Version::BB_V4, list.at(1)->as_string())));
}
} catch (const out_of_range&) {
}
try {
this->quest_F960_success_results.clear();
this->quest_F960_failure_results = QuestF960Result(json.at("QuestF960FailureResultItems"), this->item_name_index);
for (const auto& it : json.get_list("QuestF960SuccessResultItems")) {
this->quest_F960_success_results.emplace_back(*it, this->item_name_index);
}
} catch (const out_of_range&) {
}
try {
this->secret_lottery_results.clear();
for (const auto& it : json.get_list("SecretLotteryResultItems")) {
string data = parse_data_string(it->as_string());
this->secret_lottery_results.emplace_back(ItemData::from_data(data));
this->secret_lottery_results.emplace_back(this->item_name_index->parse_item_description(Version::BB_V4, it->as_string()));
}
} catch (const out_of_range&) {
}
@@ -1086,13 +1104,15 @@ void ServerState::load_word_select_table() {
this->word_select_table = make_shared<WordSelectTable>(JSON::parse(load_file("system/word-select-table.json")));
}
void ServerState::load_item_tables() {
void ServerState::load_item_name_index() {
config_log.info("Loading item name index");
this->item_name_index = make_shared<ItemNameIndex>(
JSON::parse(load_file("system/item-tables/names-v2.json")),
JSON::parse(load_file("system/item-tables/names-v3.json")),
JSON::parse(load_file("system/item-tables/names-v4.json")));
}
void ServerState::load_item_tables() {
config_log.info("Loading rare item sets");
unordered_map<string, shared_ptr<const RareItemSet>> new_rare_item_sets;
for (const auto& filename : list_directory_sorted("system/item-tables")) {
+13
View File
@@ -141,9 +141,21 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::shared_ptr<const Map::RareEnemyRates> rare_enemy_rates_challenge;
std::array<std::array<size_t, 4>, 3> min_levels_v4; // Indexed as [episode][difficulty]
struct QuestF960Result {
uint32_t meseta_cost = 0;
uint32_t base_probability = 0;
uint32_t probability_upgrade = 0;
std::array<std::vector<ItemData>, 7> results;
QuestF960Result() = default;
QuestF960Result(const JSON& json, std::shared_ptr<const ItemNameIndex> name_index);
};
// Indexed as [type][difficulty][random_choice]
std::vector<std::vector<std::vector<ItemData>>> quest_F95E_results;
std::vector<std::pair<size_t, ItemData>> quest_F95F_results; // [(num_photon_tickets, item)]
std::vector<QuestF960Result> quest_F960_success_results;
QuestF960Result quest_F960_failure_results;
std::vector<ItemData> secret_lottery_results;
uint16_t bb_global_exp_multiplier;
@@ -268,6 +280,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
void load_patch_indexes();
void load_battle_params();
void load_level_table();
void load_item_name_index();
void load_item_tables();
void load_word_select_table();
void load_ep3_data();