persist item state when no players are in a game

This commit is contained in:
Martin Michelsen
2023-12-11 12:11:32 -08:00
parent aa27c579f6
commit bc017578e3
9 changed files with 217 additions and 143 deletions
+5 -3
View File
@@ -477,14 +477,16 @@ static void proxy_command_patch(shared_ptr<ProxyServer::LinkedSession> ses, cons
}
static void server_command_persist(shared_ptr<Client> c, const std::string&) {
check_license_flags(c, License::Flag::DEBUG);
auto l = c->require_lobby();
if (l->check_flag(Lobby::Flag::DEFAULT)) {
send_text_message(c, "$C6Default lobbies\ncannot be marked\ntemporary");
} else if (!l->check_flag(Lobby::Flag::GAME)) {
send_text_message(c, "$C6Private lobbies\ncannot be marked\npersistent");
} else if (l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) || l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
send_text_message(c, "$C6Games cannot be\npersistent if a\nquest has already\nbegun");
} else {
l->toggle_flag(Lobby::Flag::PERSISTENT);
send_text_message_printf(c, "Lobby persistence\n%s",
l->check_flag(Lobby::Flag::PERSISTENT) ? "enabled" : "disabled");
send_text_message_printf(c, "Lobby persistence\n%s", l->check_flag(Lobby::Flag::PERSISTENT) ? "enabled" : "disabled");
}
}
+1
View File
@@ -56,6 +56,7 @@ public:
USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000,
HAS_GUILD_CARD_NUMBER = 0x0000000040000000,
AT_BANK_COUNTER = 0x0000000080000000,
SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000,
// Cheat mode flags
SWITCH_ASSIST_ENABLED = 0x0000000100000000,
+128 -1
View File
@@ -4,6 +4,7 @@
#include <phosg/Random.hh>
#include "Compression.hh"
#include "Loggers.hh"
#include "SendCommands.hh"
#include "Text.hh"
@@ -104,6 +105,115 @@ void Lobby::create_item_creator() {
this->quest ? this->quest->battle_rules : nullptr);
}
void Lobby::load_maps() {
auto s = this->require_server_state();
this->map = make_shared<Map>(this->lobby_id);
if (this->quest) {
auto leader_c = this->clients.at(this->leader_id);
if (!leader_c) {
throw logic_error("lobby leader is missing");
}
auto vq = this->quest->version(Version::BB_V4, leader_c->language());
auto dat_contents = prs_decompress(*vq->dat_contents);
this->map->clear();
this->map->add_enemies_and_objects_from_quest_data(
this->episode,
this->difficulty,
this->event,
dat_contents.data(),
dat_contents.size(),
this->random_seed,
this->rare_enemy_rates ? this->rare_enemy_rates : Map::NO_RARE_ENEMIES);
if (this->item_creator) {
this->item_creator->clear_destroyed_entities();
}
} else { // No quest loaded
for (size_t floor = 0; floor < 0x10; floor++) {
this->log.info("[Map/%zu] Using variations %" PRIX32 ", %" PRIX32,
floor, this->variations[floor * 2].load(), this->variations[floor * 2 + 1].load());
auto enemy_filenames = map_filenames_for_variation(
this->episode,
(this->mode == GameMode::SOLO),
floor,
this->variations[floor * 2],
this->variations[floor * 2 + 1],
true);
if (enemy_filenames.empty()) {
this->log.info("[Map/%zu:e] No file to load", floor);
} else {
bool any_map_loaded = false;
for (const string& filename : enemy_filenames) {
try {
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
this->map->add_enemies_from_map_data(
this->episode,
this->difficulty,
this->event,
floor,
map_data->data(),
map_data->size(),
this->rare_enemy_rates);
any_map_loaded = true;
break;
} catch (const exception& e) {
this->log.info("[Map/%zu:e] Failed to load %s: %s", floor, filename.c_str(), e.what());
}
}
if (!any_map_loaded) {
throw runtime_error(string_printf("no enemy maps loaded for floor %zu", floor));
}
}
auto object_filenames = map_filenames_for_variation(
this->episode,
(this->mode == GameMode::SOLO),
floor,
this->variations[floor * 2],
this->variations[floor * 2 + 1],
false);
if (object_filenames.empty()) {
this->log.info("[Map/%zu:o] No file to load", floor);
} else {
bool any_map_loaded = false;
for (const string& filename : object_filenames) {
try {
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
this->map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
any_map_loaded = true;
break;
} catch (const exception& e) {
this->log.info("[Map/%zu:o] Failed to load %s: %s", floor, filename.c_str(), e.what());
}
}
if (!any_map_loaded) {
throw runtime_error(string_printf("no object maps loaded for floor %zu", floor));
}
}
}
}
this->log.info("Generated objects list (%zu entries):", this->map->objects.size());
for (size_t z = 0; z < this->map->objects.size(); z++) {
string o_str = this->map->objects[z].str(s->item_name_index);
this->log.info("(K-%zX) %s", z, o_str.c_str());
}
this->log.info("Generated enemies list (%zu entries):", this->map->enemies.size());
for (size_t z = 0; z < this->map->enemies.size(); z++) {
string e_str = this->map->enemies[z].str();
this->log.info("(E-%zX) %s", z, e_str.c_str());
}
this->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)",
this->map->objects.size(), this->map->enemies.size(), this->map->rare_enemy_indexes.size());
if (this->item_creator) {
this->item_creator->clear_destroyed_entities();
}
}
void Lobby::create_ep3_server() {
auto s = this->require_server_state();
if (!this->ep3_server) {
@@ -212,8 +322,25 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
}
// If the lobby is a game and item tracking is enabled, assign the inventory's
// item IDs
// item IDs. If there was no one else in the lobby, reset all the next item
// IDs also
if (this->is_game() && this->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
if (leader_index >= this->max_clients) {
for (size_t x = 0; x < 12; x++) {
this->next_item_id[x] = 0x00010000 + 0x00200000 * x;
}
this->next_game_item_id = 0x00810000;
// Reassign all floor item IDs so they won't conflict with any players'
// item IDs
unordered_map<uint32_t, FloorItem> new_item_id_to_floor_item;
for (const auto& it : this->item_id_to_floor_item) {
uint32_t new_item_id = this->generate_item_id(0xFF);
auto& new_fi = new_item_id_to_floor_item.emplace(new_item_id, it.second).first->second;
new_fi.data.id = new_item_id;
}
this->item_id_to_floor_item = std::move(new_item_id_to_floor_item);
}
this->assign_inventory_and_bank_item_ids(c);
}
+1
View File
@@ -152,6 +152,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
std::shared_ptr<ServerState> require_server_state() const;
std::shared_ptr<ChallengeParameters> require_challenge_params() const;
void create_item_creator();
void load_maps();
void create_ep3_server();
[[nodiscard]] inline bool is_game() const {
+22 -124
View File
@@ -342,6 +342,14 @@ static void on_1D(shared_ptr<Client> c, uint16_t, uint32_t, string&) {
}
c->game_join_command_queue.reset();
}
if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE)) {
c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE);
auto l = c->require_lobby();
if (!is_ep3(c->version()) && l->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
send_artificial_item_state(c);
}
}
}
static void on_05_XB(shared_ptr<Client> c, uint16_t, uint32_t, string&) {
@@ -1851,34 +1859,7 @@ static void on_quest_loaded(shared_ptr<Lobby> l) {
// For Challenge quests, don't replace the map now - the leader will send an
// 02DF command to create overlays, which also replaces the map.
if ((l->base_version == Version::BB_V4) && l->map && (l->quest->challenge_template_index < 0)) {
auto leader_c = l->clients.at(l->leader_id);
if (!leader_c) {
throw logic_error("lobby leader is missing");
}
auto vq = l->quest->version(Version::BB_V4, leader_c->language());
auto dat_contents = prs_decompress(*vq->dat_contents);
l->map->clear();
l->map->add_enemies_and_objects_from_quest_data(
l->episode,
l->difficulty,
l->event,
dat_contents.data(),
dat_contents.size(),
l->random_seed,
l->rare_enemy_rates ? l->rare_enemy_rates : Map::NO_RARE_ENEMIES);
l->item_creator->clear_destroyed_entities();
l->log.info("Replaced objects list with quest layout (%zu entries)", l->map->objects.size());
for (size_t z = 0; z < l->map->objects.size(); z++) {
string o_str = l->map->objects[z].str(s->item_name_index);
l->log.info("(K-%zX) %s", z, o_str.c_str());
}
l->log.info("Replaced enemies list with quest layout (%zu entries)", l->map->enemies.size());
for (size_t z = 0; z < l->map->enemies.size(); z++) {
string e_str = l->map->enemies[z].str();
l->log.info("(E-%zX) %s", z, e_str.c_str());
}
l->load_maps();
}
for (auto& lc : l->clients) {
@@ -1925,6 +1906,7 @@ void set_lobby_quest(shared_ptr<Lobby> l, shared_ptr<const Quest> q, bool substi
} else {
l->set_flag(Lobby::Flag::QUEST_IN_PROGRESS);
}
l->clear_flag(Lobby::Flag::PERSISTENT);
l->quest = q;
if (!is_ep3(l->base_version)) {
@@ -2306,6 +2288,16 @@ static void on_10(shared_ptr<Client> c, uint16_t, uint32_t, string& data) {
throw logic_error("client cannot join game after all preconditions satisfied");
}
c->config.set_flag(Client::Flag::LOADING);
// If no one was in the game before, then there's no leader to send the
// item state - send it to the joining player (who is now the leader)
if (game->count_clients() == 1) {
// No one was in the game before, so the object and enemy state is lost;
// regenerate it as if the game was just created
if ((game->base_version == Version::BB_V4) && game->map) {
game->load_maps();
}
c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE);
}
break;
}
@@ -3465,10 +3457,6 @@ static void on_DF_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
throw runtime_error("challenge template index in quest metadata does not match index sent by client");
}
if (l->item_creator) {
l->item_creator->clear_destroyed_entities();
}
for (auto lc : l->clients) {
if (lc) {
lc->use_default_bank();
@@ -3478,16 +3466,7 @@ static void on_DF_BB(shared_ptr<Client> c, uint16_t command, uint32_t, string& d
}
}
auto dat_contents = prs_decompress(*vq->dat_contents);
l->map->clear();
l->map->add_enemies_and_objects_from_quest_data(
l->episode,
l->difficulty,
l->event,
dat_contents.data(),
dat_contents.size(),
l->random_seed,
l->rare_enemy_rates ? l->rare_enemy_rates : Map::NO_RARE_ENEMIES);
l->load_maps();
break;
}
@@ -4004,89 +3983,8 @@ shared_ptr<Lobby> create_game_generic(
} else {
game->rare_enemy_rates = s->rare_enemy_rates_by_difficulty.at(game->difficulty);
}
if (game->base_version == Version::BB_V4) {
game->map = make_shared<Map>(game->lobby_id);
for (size_t floor = 0; floor < 0x10; floor++) {
c->log.info("[Map/%zu] Using variations %" PRIX32 ", %" PRIX32,
floor, game->variations[floor * 2].load(), game->variations[floor * 2 + 1].load());
auto enemy_filenames = map_filenames_for_variation(
game->episode,
is_solo,
floor,
game->variations[floor * 2],
game->variations[floor * 2 + 1],
true);
if (enemy_filenames.empty()) {
c->log.info("[Map/%zu:e] No file to load", floor);
} else {
bool any_map_loaded = false;
for (const string& filename : enemy_filenames) {
try {
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
size_t start_offset = game->map->enemies.size();
game->map->add_enemies_from_map_data(
game->episode,
game->difficulty,
game->event,
floor,
map_data->data(),
map_data->size(),
game->rare_enemy_rates);
size_t entries_loaded = game->map->enemies.size() - start_offset;
c->log.info("[Map/%zu:e] Loaded %s (%zu entries)", floor, filename.c_str(), entries_loaded);
for (size_t z = start_offset; z < game->map->enemies.size(); z++) {
string e_str = game->map->enemies[z].str();
static_game_data_log.info("(E-%zX) %s", z, e_str.c_str());
}
any_map_loaded = true;
break;
} catch (const exception& e) {
c->log.info("[Map/%zu:e] Failed to load %s: %s", floor, filename.c_str(), e.what());
}
}
if (!any_map_loaded) {
throw runtime_error(string_printf("no enemy maps loaded for floor %zu", floor));
}
}
auto object_filenames = map_filenames_for_variation(
game->episode,
is_solo,
floor,
game->variations[floor * 2],
game->variations[floor * 2 + 1],
false);
if (object_filenames.empty()) {
c->log.info("[Map/%zu:o] No file to load", floor);
} else {
bool any_map_loaded = false;
for (const string& filename : object_filenames) {
try {
auto map_data = s->load_bb_file(filename, "", "map/" + filename);
size_t start_offset = game->map->objects.size();
game->map->add_objects_from_map_data(floor, map_data->data(), map_data->size());
size_t entries_loaded = game->map->objects.size() - start_offset;
c->log.info("[Map/%zu:o] Loaded %s (%zu entries)", floor, filename.c_str(), entries_loaded);
for (size_t z = start_offset; z < game->map->objects.size(); z++) {
string e_str = game->map->objects[z].str(s->item_name_index);
static_game_data_log.info("(K-%zX) %s", z, e_str.c_str());
}
any_map_loaded = true;
break;
} catch (const exception& e) {
c->log.info("[Map/%zu:o] Failed to load %s: %s", floor, filename.c_str(), e.what());
}
}
if (!any_map_loaded) {
throw runtime_error(string_printf("no object maps loaded for floor %zu", floor));
}
}
}
c->log.info("Loaded maps contain %zu object entries and %zu enemy entries overall (%zu as rares)",
game->map->objects.size(), game->map->enemies.size(), game->map->rare_enemy_indexes.size());
game->load_maps();
}
return game;
}
+1 -10
View File
@@ -2391,7 +2391,6 @@ static void on_battle_restart_bb(shared_ptr<Client> c, uint8_t, uint8_t, const v
auto new_rules = make_shared<BattleRules>(cmd.rules);
if (l->item_creator) {
l->item_creator->set_restrictions(new_rules);
l->item_creator->clear_destroyed_entities();
}
for (auto& lc : l->clients) {
@@ -2401,15 +2400,7 @@ static void on_battle_restart_bb(shared_ptr<Client> c, uint8_t, uint8_t, const v
lc->create_battle_overlay(new_rules, s->level_table);
}
}
l->map->clear();
l->map->add_enemies_and_objects_from_quest_data(
l->episode,
l->difficulty,
l->event,
dat_contents.data(),
dat_contents.size(),
l->random_seed,
l->rare_enemy_rates ? l->rare_enemy_rates : Map::NO_RARE_ENEMIES);
l->load_maps();
}
}
+57 -4
View File
@@ -2296,16 +2296,69 @@ void send_ep3_change_music(Channel& ch, uint32_t song) {
ch.send(0x60, 0x00, cmd);
}
void send_set_player_visibility(shared_ptr<Lobby> l, shared_ptr<Client> c,
bool visible) {
void send_set_player_visibility(shared_ptr<Lobby> l, shared_ptr<Client> c, bool visible) {
uint8_t subcmd = visible ? 0x23 : 0x22;
uint16_t client_id = c->lobby_client_id;
G_SetPlayerVisibility_6x22_6x23 cmd = {{subcmd, 0x01, client_id}};
send_command_t(l, 0x60, 0x00, cmd);
}
////////////////////////////////////////////////////////////////////////////////
// BB game commands
void send_artificial_item_state(std::shared_ptr<Client> c) {
auto l = c->require_lobby();
if (c->lobby_client_id != l->leader_id) {
throw runtime_error("artificial item state can only be sent to the leader");
}
if (l->count_clients() != 1) {
throw runtime_error("artificial item state can only be sent with no one else in the game");
}
array<StringWriter, 15> floor_ws;
G_SyncItemState_6x6D_Decompressed decompressed_header;
for (size_t z = 0; z < 12; z++) {
decompressed_header.next_item_id_per_player[z] = l->next_item_id[z];
}
for (const auto& it : l->item_id_to_floor_item) {
const auto& item = it.second;
FloorItem fi;
fi.floor = item.floor;
fi.from_enemy = 0;
fi.entity_id = 0xFFFF;
fi.x = item.x;
fi.z = item.z;
fi.unknown_a2 = 0;
fi.drop_number = decompressed_header.total_items_dropped_per_floor.at(item.floor)++;
fi.item = item.data;
floor_ws.at(item.floor).put(fi);
decompressed_header.floor_item_count_per_floor.at(item.floor)++;
}
StringWriter decompressed_w;
decompressed_w.put(decompressed_header);
for (const auto& floor_w : floor_ws) {
decompressed_w.write(floor_w.str());
}
string compressed_data = bc0_compress(decompressed_w.str());
G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E compressed_header;
compressed_header.header.basic_header.subcommand = 0x6D;
compressed_header.header.basic_header.size = 0x00;
compressed_header.header.basic_header.unused = 0x0000;
compressed_header.header.size = (compressed_data.size() + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E) + 3) & (~3);
compressed_header.decompressed_size = decompressed_w.size();
compressed_header.compressed_size = compressed_data.size();
StringWriter w;
w.put(compressed_header);
w.write(compressed_data);
while (w.size() & 3) {
w.put_u8(0x00);
}
send_command(c, 0x6D, c->lobby_client_id, w.str());
}
void send_drop_item(shared_ptr<ServerState> s, Channel& ch, const ItemData& item,
bool from_enemy, uint8_t floor, float x, float z, uint16_t entity_id) {
+1
View File
@@ -298,6 +298,7 @@ void send_ep3_change_music(Channel& ch, uint32_t song);
void send_set_player_visibility(std::shared_ptr<Client> c, bool visible);
void send_revive_player(std::shared_ptr<Client> c);
void send_artificial_item_state(std::shared_ptr<Client> c);
void send_drop_item(std::shared_ptr<ServerState> s, Channel& ch, const ItemData& item,
bool from_enemy, uint8_t floor, float x, float z, uint16_t request_id);
void send_drop_item(std::shared_ptr<Lobby> l, const ItemData& item,