aboutsummaryrefslogtreecommitdiff
path: root/src/server/server.cc
diff options
context:
space:
mode:
authorNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 21:57:46 +1100
committerNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 21:57:46 +1100
commite4483eca01b48b943cd0461e24a74ae1a3139ed4 (patch)
treeed58c3c246e3af1af337697695d780aa31f6ad9a /src/server/server.cc
parent1cc08c51eb4b0f95c30c0a98ad1fc5ad3459b2df (diff)
Update to most recent version (old initial commit)
Diffstat (limited to 'src/server/server.cc')
-rw-r--r--src/server/server.cc470
1 files changed, 314 insertions, 156 deletions
diff --git a/src/server/server.cc b/src/server/server.cc
index 72faa03..4592334 100644
--- a/src/server/server.cc
+++ b/src/server/server.cc
@@ -2,25 +2,39 @@
namespace server {
-static proto::packet
-make_init_packet(const resources::client_map_value& client) noexcept {
- proto::packet packet;
-
- const auto init_packet = packet.mutable_init_packet();
- init_packet->set_seed(state.seed);
- init_packet->set_draw_distance(state.draw_distance);
- shared::net::set_player(*init_packet->mutable_localplayer(),
- client->get_player());
+static std::uint32_t& get_tick() noexcept {
+ static std::uint32_t ret = 0;
+ return ret;
+}
+static proto::player make_player_packet(const shared::player& player) noexcept {
+ proto::player packet;
+ player.pack(&packet);
return packet;
}
-static proto::packet make_player_packet(const shared::player& player) noexcept {
+static proto::packet
+make_animate_update_packet(const shared::animate& animate, const std::optional<shared::tick_t>& sequence) noexcept {
proto::packet packet;
+ const auto animate_update = packet.mutable_animate_update_packet();
+ animate.pack(animate_update->mutable_animate());
+ animate_update->set_tick(get_tick());
+ if (sequence.has_value()) {
+ animate_update->set_sequence(*sequence);
+ }
+ return packet;
+}
- const auto player_packet = packet.mutable_player_packet();
- shared::net::set_player(*player_packet, player);
+static proto::packet
+make_init_packet(const resources::client_map_value& client) noexcept {
+ proto::packet packet;
+ const auto init_packet = packet.mutable_init_packet();
+ init_packet->set_seed(state.seed);
+ init_packet->set_draw_distance(state.draw_distance);
+ init_packet->set_tickrate(state.tickrate);
+ init_packet->set_tick(get_tick());
+ (*client->player_info)->player.pack(init_packet->mutable_localplayer());
return packet;
}
@@ -39,7 +53,7 @@ static proto::packet
make_remove_packet(const resources::client_map_value& client) noexcept {
proto::packet packet;
- const auto remove_packet = packet.mutable_remove_player_packet();
+ const auto remove_packet = packet.mutable_remove_entity_packet();
remove_packet->set_index(client->index);
return packet;
@@ -62,12 +76,11 @@ static proto::packet make_chunk_packet(const world::chunk& chunk) noexcept {
static void block_by_tickrate() noexcept {
const auto tickrate = server::state.tickrate;
- static const auto ratetime = std::chrono::milliseconds(
- static_cast<int>((1.0 / static_cast<double>(tickrate)) * 1000.0));
- static auto prev = std::chrono::steady_clock::now();
- const auto now = std::chrono::steady_clock::now();
- std::this_thread::sleep_for(ratetime - (now - prev));
- prev = std::chrono::steady_clock::now();
+ static const auto ratetime = std::chrono::microseconds(
+ static_cast<int>((1.0 / static_cast<double>(tickrate)) * 1'000'000));
+ static auto wanted = std::chrono::steady_clock::now();
+ std::this_thread::sleep_until(wanted);
+ wanted += ratetime;
}
// Creates, binds, listens on new nonblocking socket.
@@ -89,14 +102,52 @@ static std::optional<shared::net::connection> make_connection(const int sock) {
if (!accept.has_value()) {
return std::nullopt;
}
- return shared::net::connection(accept->socket);
+
+ try {
+ return shared::net::connection(accept->socket);
+ } catch (const std::runtime_error& e) {
+#ifndef NDEBUG
+ shared::print::debug
+ << shared::print::time
+ << "server: constructor for client connection failed; what(): "
+ << e.what() << '\n';
+#endif
+ }
+
+ return std::nullopt;
+}
+
+static void move_client(resources::client_map_value& client,
+ resources::chunk_map& chunks) noexcept {
+ movement::move(*client, chunks);
}
static void handle_move_packet(const proto::move& packet,
- resources::client_map_value& client) noexcept {
- client->get_player().viewangles = {.pitch = packet.viewangles().pitch(),
- .yaw = packet.viewangles().yaw()};
- client->get_player().commands = packet.commands();
+ resources::client_map_value& client,
+ resources::chunk_map& chunks) noexcept {
+ if (!client->has_initialised()) {
+ return;
+ }
+
+ const shared::tick_t sequence = packet.sequence();
+ if (sequence <= client->sequence) { // Packet is late, drop it.
+#ifndef NDEBUG
+ shared::print::debug << shared::print::time
+ << "server: client sent late tick " << sequence
+ << " <= " << client->sequence << '\n';
+#endif
+ return;
+ }
+ client->sequence = sequence;
+
+ auto& player = client->get_player();
+ player.get_mutable_angles() = {.pitch = packet.viewangles().pitch(),
+ .yaw = packet.viewangles().yaw()};
+ player.get_mutable_angles().clamp();
+ player.get_mutable_commands() = packet.commands();
+ player.get_mutable_active_item() = packet.active_item();
+
+ move_client(client, chunks);
}
static void handle_say_packet(const proto::say& packet,
@@ -104,18 +155,24 @@ static void handle_say_packet(const proto::say& packet,
server::resources::client_map& clients) noexcept {
if (std::size(packet.text()) > shared::MAX_SAY_LENGTH) {
#ifndef NDEBUG
- shared::print::warn(
- "server: client tried to say a message that was too long, size: " +
- std::to_string(std::size(packet.text())) + '\n');
+ shared::print::debug
+ << shared::print::time
+ << "server: client tried to say a message that was too long, size: "
+ << std::size(packet.text()) << '\n';
#endif
return;
}
- shared::print::message("server: player " + std::to_string(client->index) +
- " said \"" + packet.text() + "\"\n");
- const auto hear_packet = make_hear_packet(packet.text(), client->index);
+ shared::print::message << shared::print::time << "server: player "
+ << client->index << " said \"" << packet.text()
+ << "\"\n";
+ const auto hear_packet = std::make_shared<shared::net::rpacket>(
+ make_hear_packet(packet.text(), client->index));
for (auto& [index, client_ptr] : clients) {
+ if (!client_ptr->is_in_pvs(*client)) {
+ continue;
+ }
client_ptr->connection.rsend_packet(hear_packet);
}
}
@@ -145,9 +202,9 @@ static void send_chunk_associated(resources::chunk_map_value& chunk_data,
// correctly.
if (find_it == std::end(clients)) {
#ifndef NDEBUG
- shared::print::debug(
- "client index " + std::to_string(client_index) +
- " was associated with a chunk, but not found\n");
+ shared::print::debug
+ << shared::print::time << "client index " << client_index
+ << " was associated with a chunk, but not found\n";
#endif
continue;
}
@@ -209,33 +266,45 @@ static void handle_request_chunk_packet(const proto::request_chunk& packet,
resources::client_map_value& client,
resources::chunk_map& chunks,
resources::pool_t& pool) noexcept {
+ if (!client->has_initialised()) {
+ return;
+ }
const shared::math::coords coords{packet.chunk_pos().x(),
packet.chunk_pos().z()};
if (const auto find_it = chunks.find(coords); find_it != std::end(chunks)) {
auto& chunk_data = find_it->second;
+
+ // Associate client, then post sending of chunk to client.
resources::associate_client_chunk(coords, client, chunk_data);
boost::asio::post(
- pool,
- std::bind(
- [](const shared::math::coords coords,
- const shared::player::index_t index) {
- auto res_lock = resources::get_resources_lock();
- auto& chunk_data = res_lock->chunks.find(coords)->second;
-
- if (!chunk_data->has_initialised()) {
- return; // will be sent on construction
- }
-
- const auto& client_it = res_lock->clients.find(index);
- if (client_it == std::end(res_lock->clients)) {
- return;
- }
- auto& client = client_it->second;
- send_chunk(chunk_data->get_chunk(), client);
- maybe_post_chunk_rm(coords, chunk_data, res_lock->pool);
- },
- coords, client->index));
+ pool, std::bind(
+ [](const shared::math::coords coords,
+ const shared::player::index_t index) {
+ auto res_lock = resources::get_resources_lock();
+
+ // Client has requested a chunk removed before
+ // receiving it?
+ const auto find_it = res_lock->chunks.find(coords);
+ if (find_it == std::end(res_lock->chunks)) {
+ return;
+ }
+ auto& chunk_data = find_it->second;
+
+ if (!chunk_data->has_initialised()) {
+ return; // will be sent on construction
+ }
+
+ const auto& client_it = res_lock->clients.find(index);
+ if (client_it == std::end(res_lock->clients)) {
+ return;
+ }
+ auto& client = client_it->second;
+ send_chunk(chunk_data->get_chunk(), client);
+ maybe_post_chunk_rm(coords, chunk_data,
+ res_lock->pool);
+ },
+ coords, client->index));
return;
}
@@ -247,19 +316,26 @@ static void handle_request_chunk_packet(const proto::request_chunk& packet,
resources::associate_client_chunk(coords, client, data);
boost::asio::post(
- pool,
- std::bind(
- [](const shared::math::coords coords) {
- server::world::chunk chunk{server::state.seed, coords};
+ pool, std::bind(
+ [](const shared::math::coords coords) {
+ auto chunk =
+ [&coords]() -> std::shared_ptr<server::world::chunk> {
+ auto maybe_chunk = database::maybe_read_chunk(coords);
+ if (maybe_chunk.has_value()) {
+ return std::make_shared<server::world::chunk>(
+ server::state.seed, *maybe_chunk);
+ }
+ return std::make_shared<server::world::chunk>(
+ server::state.seed, coords);
+ }();
- auto res_lock = resources::get_resources_lock();
- auto& chunk_data = res_lock->chunks.find(coords)->second;
- chunk_data->chunk.emplace(
- std::make_unique<server::world::chunk>(std::move(chunk)));
- send_chunk_associated(chunk_data, res_lock->clients);
- maybe_post_chunk_rm(coords, chunk_data, res_lock->pool);
- },
- coords));
+ auto res_lock = resources::get_resources_lock();
+ auto& chunk_data = res_lock->chunks.find(coords)->second;
+ chunk_data->chunk.emplace(std::move(chunk));
+ send_chunk_associated(chunk_data, res_lock->clients);
+ maybe_post_chunk_rm(coords, chunk_data, res_lock->pool);
+ },
+ coords));
}
// Disassociates a client with a chunk, while performing necessary cleanups.
@@ -310,17 +386,21 @@ static void post_chunk_update(const shared::math::coords& coords,
coords));
}
-static void modify_block(const enum shared::world::block::type block_type,
- const glm::ivec3& block_pos,
- const shared::math::coords& coords,
- resources::chunk_map& chunks) noexcept {
+static void
+handle_add_block_packet(const proto::add_block& packet,
+ [[maybe_unused]] resources::client_map_value& client,
+ server::resources::chunk_map& chunks,
+ resources::pool_t& pool) noexcept {
+
+ const auto coords = shared::net::get_coords(packet.chunk_pos());
+ const auto pos = shared::net::get_ivec3(packet.block_pos());
+ const auto active_item = packet.active_item();
const auto find_it = chunks.find(coords);
if (find_it == std::end(chunks)) {
return;
}
-
- if (shared::world::chunk::is_outside_chunk(block_pos)) {
+ if (shared::world::chunk::is_outside_chunk(pos)) {
return;
}
@@ -329,22 +409,24 @@ static void modify_block(const enum shared::world::block::type block_type,
return;
}
- chunk_data->get_chunk().get_block(block_pos) = block_type;
- chunk_data->get_chunk().arm_should_update();
-}
+ auto& inventory = client->get_player().inventory;
+ if (active_item < 0 || active_item >= std::size(inventory.contents)) {
+ return;
+ }
-static void
-handle_add_block_packet(const proto::add_block& packet,
- [[maybe_unused]] resources::client_map_value& client,
- server::resources::chunk_map& chunks,
- resources::pool_t& pool) noexcept {
- const shared::math::coords coords{packet.chunk_pos().x(),
- packet.chunk_pos().z()};
- const glm::ivec3 block_pos{packet.block_pos().x(), packet.block_pos().y(),
- packet.block_pos().z()};
- const auto block =
- static_cast<enum shared::world::block::type>(packet.block());
- modify_block(block, block_pos, coords, chunks);
+ const auto& item = inventory.contents[active_item];
+ if (item == nullptr) {
+ return;
+ }
+ const auto block_ptr = dynamic_cast<shared::item::block*>(&*item);
+ if (block_ptr == nullptr) {
+ return;
+ }
+
+ chunk_data->get_chunk().get_block(pos) = block_ptr->type;
+ inventory.decrement(active_item);
+
+ chunk_data->get_chunk().arm_should_update();
post_chunk_update(coords, pool);
}
@@ -353,11 +435,30 @@ handle_remove_block_packet(const proto::remove_block& packet,
[[maybe_unused]] resources::client_map_value& client,
resources::chunk_map& chunks,
resources::pool_t& pool) noexcept {
- const shared::math::coords coords{packet.chunk_pos().x(),
- packet.chunk_pos().z()};
- const glm::ivec3 block_pos{packet.block_pos().x(), packet.block_pos().y(),
- packet.block_pos().z()};
- modify_block(shared::world::block::type::air, block_pos, coords, chunks);
+ const auto coords = shared::net::get_coords(packet.chunk_pos());
+ const auto pos = shared::net::get_ivec3(packet.block_pos());
+
+ const auto find_it = chunks.find(coords);
+ if (find_it == std::end(chunks)) {
+ return;
+ }
+ if (shared::world::chunk::is_outside_chunk(pos)) {
+ return;
+ }
+
+ auto& chunk_data = find_it->second;
+ if (!chunk_data->has_initialised()) {
+ return;
+ }
+
+ auto& block = chunk_data->get_chunk().get_block(pos);
+
+ auto& inventory = client->get_player().inventory;
+ inventory.maybe_add(shared::item::block::get_type(block.type), 1);
+
+ block = shared::world::block::type::air;
+
+ chunk_data->get_chunk().arm_should_update();
post_chunk_update(coords, pool);
}
@@ -379,47 +480,58 @@ static void handle_auth_packet(const proto::auth& packet,
auto& client = find_it->second;
// find if we're already associated, eventually kick our client
- // it would be nice if this wasn't a find_if
- if (std::find_if(std::begin(res_lock->clients),
- std::end(res_lock->clients),
- [&](const auto& it) {
- auto& client = it.second;
- if (!client->has_initialised()) {
- return false;
- }
- return client->index != client_index &&
- client->get_username() == user;
- }) != std::end(res_lock->clients)) {
-
+ if (std::ranges::any_of(res_lock->clients,
+ [&client_index, &user](const auto& it) {
+ const auto& [idx, c] = it;
+ if (idx == client_index) {
+ return false;
+ }
+ if (!c->has_initialised()) {
+ return false;
+ }
+ if (c->get_username() != user) {
+ return false;
+ }
+ return true;
+ })) {
client->disconnect_reason.emplace("user already in server");
return;
}
- struct client::player_info player_info {
- .username = user, .password = pass
- };
- if (db_plr.has_value()) {
- auto& [player, db_password] = *db_plr;
+ const auto in_db = db_plr.has_value();
+ if (in_db && db_plr->second != pass) {
+ client->disconnect_reason.emplace("bad password");
+ return;
+ }
- if (db_password != pass) {
- client->disconnect_reason.emplace("bad password");
- return;
+ shared::player player = [&]() -> shared::player {
+ if (in_db) {
+ // update to new client_index before returning
+ shared::player player{db_plr->first};
+ player.get_mutable_index() = client_index;
+ return player;
}
-
- player_info.player = shared::net::get_player(player);
- player_info.player.index = client_index;
- } else {
// TODO: Find a random spawn chunk and put the player
// on the highest block. Because we don't want to gen chunks
// while blocking, we can't do this here. For now, we'll
// just put the player at a high spot.
- player_info.player =
- shared::player{.index = client_index,
- .local_pos = {0.0f, 140.0f, 0.0f}};
- }
+ return shared::player{
+ shared::item::items{},
+ 0u,
+ shared::math::angles{0.0f, 0.0f},
+ glm::vec3{0.0f, 0.0f, 0.0f},
+ 0u,
+ client_index,
+ shared::math::coords{0, 0},
+ glm::vec3{0.0f, 120.0f, 0.0f},
+ };
+ }();
+
+ using pi = struct client::player_info;
client->player_info.emplace(
- std::make_shared<struct client::player_info>(
- std::move(player_info)));
+ std::make_shared<pi>(pi{.username = user,
+ .password = pass,
+ .player = std::move(player)}));
client->connection.rsend_packet(make_init_packet(client));
},
@@ -427,6 +539,30 @@ static void handle_auth_packet(const proto::auth& packet,
std::move(packet.password())));
}
+static void
+handle_item_swap_packet(const proto::item_swap& proto,
+ const resources::client_map_value& client) noexcept {
+ auto& inventory = client->get_player().inventory;
+
+ const auto a = proto.index_a();
+ const auto b = proto.index_b();
+
+ if (a == b) {
+ return;
+ }
+ const auto in_range = [&](const auto val) {
+ const auto MAX_SIZE =
+ static_cast<std::uint32_t>(std::size(inventory.contents));
+ return std::clamp(val, 0u, MAX_SIZE) == val;
+ };
+
+ if (!in_range(a) || !in_range(b)) {
+ return;
+ }
+
+ std::swap(inventory.contents[a], inventory.contents[b]);
+}
+
// Get new packets from clients, this will change client data and worldata.
static void parse_client_packets(resources::client_map_value& client,
resources::resources& res) noexcept {
@@ -434,7 +570,7 @@ static void parse_client_packets(resources::client_map_value& client,
if (packet->has_auth_packet()) {
handle_auth_packet(packet->auth_packet(), client, res.pool);
} else if (packet->has_move_packet()) {
- handle_move_packet(packet->move_packet(), client);
+ handle_move_packet(packet->move_packet(), client, res.chunks);
} else if (packet->has_say_packet()) {
handle_say_packet(packet->say_packet(), client, res.clients);
} else if (packet->has_request_chunk_packet()) {
@@ -449,10 +585,13 @@ static void parse_client_packets(resources::client_map_value& client,
} else if (packet->has_remove_block_packet()) {
handle_remove_block_packet(packet->remove_block_packet(), client,
res.chunks, res.pool);
+ } else if (packet->has_item_swap_packet()) {
+ handle_item_swap_packet(packet->item_swap_packet(), client);
}
#ifndef NDEBUG
else {
- shared::print::warn("server: unhandled packet type\n");
+ shared::print::debug << shared::print::time
+ << "server: unhandled packet type\n";
}
#endif
}
@@ -462,7 +601,8 @@ static void parse_client_packets(resources::client_map_value& client,
static void
send_remove_packets(server::resources::client_map_value& remove_client,
server::resources::client_map& clients) noexcept {
- const auto remove_packet = make_remove_packet(remove_client);
+ const auto remove_packet = std::make_shared<shared::net::rpacket>(
+ make_remove_packet(remove_client));
for (auto& [index, client_ptr] : clients) {
client_ptr->connection.rsend_packet(remove_packet);
}
@@ -470,8 +610,9 @@ send_remove_packets(server::resources::client_map_value& remove_client,
static void handle_new_connections(shared::net::connection& connection,
resources::resources& res) noexcept {
- shared::print::message("server: got connection from " +
- connection.get_address() + '\n');
+ shared::print::message << shared::print::time
+ << "server: got connection from "
+ << connection.get_address() << '\n';
// Add the client, waiting for an auth packet.
static uint32_t index = 0;
@@ -481,25 +622,25 @@ static void handle_new_connections(shared::net::connection& connection,
++index;
}
-static void move_client(resources::client_map_value& client,
- resources::chunk_map& chunks) noexcept {
- // shared::movement::fly(client.player, client.commands);
- movement::move(*client, chunks);
-}
-
static void send_client_packets(resources::client_map_value& client,
resources::resources& res) noexcept {
- // TODO PVS, as simple as checking if a client has our chunk pos in their
- // associated chunks list
+
+ if (!client->has_initialised()) {
+ return;
+ }
+ const auto non_local_packet = std::make_shared<shared::net::upacket>(
+ make_animate_update_packet(client->get_player(), std::nullopt));
for (auto& [index, c] : res.clients) {
- if (!c->has_initialised()) {
- continue;
- }
- if (!client->chunks.contains(c->get_player().chunk_pos)) {
+ if (!c->is_in_pvs(*client)) {
continue;
}
- client->connection.usend_packet(make_player_packet(c->get_player()));
+ // only network the last received sequence if sending an animate packet for the client's local player
+ if (c->index == client->index) {
+ c->connection.usend_packet( make_animate_update_packet(client->get_player(), client->sequence) );
+ } else {
+ c->connection.usend_packet(non_local_packet);
+ }
}
}
@@ -510,15 +651,17 @@ get_sent_disconnect_reason(const resources::client_map_value& client) noexcept {
return client->connection.get_bad_reason();
}
- if (client->chunks.size() >
- unsigned(state.draw_distance * state.draw_distance * 4)) {
+ if (static const unsigned max = static_cast<unsigned>(
+ state.draw_distance * state.draw_distance * 4);
+ client->chunks.size() > max) {
return "too many chunks associated with client";
}
return client->disconnect_reason;
}
-static void remove_bad_clients(resources::resources& res) noexcept {
+static void remove_bad_clients(resources::resources& res,
+ const bool send_remove = true) noexcept {
for (auto& [index, client] : res.clients) {
const auto reason = get_sent_disconnect_reason(client);
if (reason == std::nullopt) {
@@ -530,11 +673,18 @@ static void remove_bad_clients(resources::resources& res) noexcept {
}
client->disconnecting = true;
- shared::print::message("server: dropped " +
- client->connection.get_address() + " for \"" +
- *reason + "\"\n");
- send_message("disconnected for " + *reason, client, true);
- send_remove_packets(client, res.clients);
+ shared::print::message << shared::print::time << "server: dropped "
+ << client->connection.get_address() << " for \""
+ << *reason << "\"\n";
+ send_message(*reason, client, true);
+ if (send_remove) {
+ send_remove_packets(client, res.clients);
+ }
+
+ // Close early so the client may reconnect and receive a better message
+ // than a generic errno error if they connect too fast later.
+ client->connection.poll();
+ client->connection.close();
boost::asio::post(
res.pool,
@@ -546,8 +696,7 @@ static void remove_bad_clients(resources::resources& res) noexcept {
if (plr_info.has_value()) {
database::write_player(
(*plr_info)->username, (*plr_info)->password,
- make_player_packet((*plr_info)->player)
- .player_packet());
+ make_player_packet((*plr_info)->player));
}
// cleanup associated chunks
@@ -583,9 +732,10 @@ static void process_resources(resources::resources& res) noexcept {
}
// Move clients via their (hopefully updated) command.
- for (auto& [index, client] : res.clients) {
+ // CHANGED: we now do this when we get move packets
+ /*for (auto& [index, client] : res.clients) {
move_client(client, res.chunks);
- }
+ }*/
// Send packets which are sent once per tick, as of now the player
// struct of other clients.
@@ -593,9 +743,15 @@ static void process_resources(resources::resources& res) noexcept {
send_client_packets(client, res);
}
+ // Send queued packets.
+ for (auto& [index, client] : res.clients) {
+ client->connection.poll();
+ }
+
// Delete bad connections, print if it happens. Also sends a remove
// to all clients per client removed.
remove_bad_clients(res);
+ ++get_tick();
}
void main(const std::string_view address, const std::string_view port) {
@@ -603,8 +759,8 @@ void main(const std::string_view address, const std::string_view port) {
server::resources::init();
has_initialised = true;
- shared::print::notify("server: started at " + std::string{address} + ':' +
- std::string{port} + '\n');
+ shared::print::notify << shared::print::time << "server: started at "
+ << address << ':' << port << '\n';
// Server has a tickrate, we will use non-blocking polling at this
// tickrate.
@@ -629,12 +785,14 @@ void main(const std::string_view address, const std::string_view port) {
const auto& client = it.second;
client->disconnect_reason.emplace("server shutting down");
});
- remove_bad_clients(*res_lock);
+ remove_bad_clients(*res_lock, false);
}
- shared::print::notify("server: writing world data\n");
+ shared::print::notify << shared::print::time
+ << "server: writing world data\n";
server::resources::quit();
- shared::print::notify("server: gracefully exited\n");
+ shared::print::notify << shared::print::time
+ << "server: gracefully exited\n";
}
} // namespace server