persist item state when no players are in a game
This commit is contained in:
+5
-3
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user