aboutsummaryrefslogtreecommitdiff
path: root/src/client/client.cc
diff options
context:
space:
mode:
authorNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 18:05:18 +1100
committerNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 18:05:18 +1100
commit1cc08c51eb4b0f95c30c0a98ad1fc5ad3459b2df (patch)
tree222dfcd07a1e40716127a347bbfd7119ce3d0984 /src/client/client.cc
initial commit
Diffstat (limited to 'src/client/client.cc')
-rw-r--r--src/client/client.cc515
1 files changed, 515 insertions, 0 deletions
diff --git a/src/client/client.cc b/src/client/client.cc
new file mode 100644
index 0000000..b4cacdc
--- /dev/null
+++ b/src/client/client.cc
@@ -0,0 +1,515 @@
+#include "client.hh"
+
+namespace {
+bool should_send_move = false; // Ratelimit move packets by player packets.
+
+shared::world::block last_block = shared::world::block::type::dirt;
+
+client::world::chunk::map chunks{4096, shared::world::chunk::hash,
+ shared::world::chunk::equal};
+
+client::players players;
+} // namespace
+
+namespace client {
+
+static auto get_player_it(const std::uint32_t index) {
+ return std::ranges::find_if(
+ ::players, [&](const auto& player) { return player.index == index; });
+}
+
+static proto::packet
+make_get_chunk_packet(const shared::math::coords& coords) noexcept {
+ proto::packet packet;
+
+ const auto chunk_packet = packet.mutable_request_chunk_packet();
+ shared::net::set_coords(*chunk_packet->mutable_chunk_pos(), coords);
+
+ return packet;
+}
+
+static proto::packet make_auth_packet(const std::string& username,
+ const std::string& password) noexcept {
+ proto::packet packet;
+
+ const auto auth_packet = packet.mutable_auth_packet();
+ auth_packet->set_username(username);
+ auth_packet->set_password(password);
+
+ return packet;
+}
+
+static proto::packet make_add_block_packet(const shared::math::coords& coords,
+ const glm::ivec3& pos) noexcept {
+ proto::packet packet;
+
+ const auto interact_packet = packet.mutable_add_block_packet();
+ shared::net::set_coords(*interact_packet->mutable_chunk_pos(), coords);
+ shared::net::set_ivec3(*interact_packet->mutable_block_pos(), pos);
+ interact_packet->set_block(static_cast<std::uint32_t>(::last_block.type));
+
+ return packet;
+}
+
+static proto::packet
+make_remove_block_packet(const shared::math::coords& coords,
+ const glm::ivec3& pos) noexcept {
+ proto::packet packet;
+
+ const auto interact_packet = packet.mutable_remove_block_packet();
+ shared::net::set_coords(*interact_packet->mutable_chunk_pos(), coords);
+ shared::net::set_ivec3(*interact_packet->mutable_block_pos(), pos);
+
+ return packet;
+}
+
+[[maybe_unused]] // [[actually_used]]
+static proto::packet
+make_remove_chunk_packet(const shared::math::coords& coords) noexcept {
+ proto::packet packet;
+
+ const auto remove_chunk_packet = packet.mutable_remove_chunk_packet();
+ shared::net::set_coords(*remove_chunk_packet->mutable_chunk_pos(), coords);
+
+ return packet;
+}
+
+static void handle_init_packet(const proto::init& packet) noexcept {
+ client::state.seed = packet.seed();
+ client::state.draw_distance =
+ std::min(packet.draw_distance(),
+ client::settings::get({"video", "draw_distance"}, 32));
+
+ auto& localplayer = packet.localplayer();
+ client::state.localplayer = localplayer.index();
+
+ ::players.emplace_back(shared::net::get_player(localplayer));
+}
+
+static void handle_player_packet(const proto::player& packet) noexcept {
+ if (std::size(::players) <= 0) {
+ return;
+ }
+
+ const auto player_it = get_player_it(packet.index());
+
+ shared::player player = shared::net::get_player(packet);
+
+ if (player_it == std::end(::players)) {
+ ::players.emplace_back(player);
+ return;
+ }
+ // If the player packet refers to us, we do not override our localplayer's
+ // commands or viewangles. Also, we should send a new move packet.
+ if (auto& lp = get_localplayer(::players); lp.index == packet.index()) {
+
+ player.viewangles = lp.viewangles;
+ player.commands = 0u;
+ ::should_send_move = true;
+ }
+ player_it->update(player);
+}
+
+// Remove the client whose element is equal to pkt.index.
+static void handle_remove_packet(const proto::remove_player& packet) noexcept {
+ const auto player_it = get_player_it(packet.index());
+ if (player_it == std::end(::players)) {
+ return;
+ }
+ ::players.erase(player_it);
+}
+
+static void handle_hear_packet(const proto::hear_player& packet) noexcept {
+ const auto player_it = get_player_it(packet.index());
+ if (player_it == std::end(::players)) {
+ return;
+ }
+ player_it->message.emplace(packet.text());
+}
+
+static void handle_chunk_packet(const proto::chunk& packet) noexcept {
+ const shared::math::coords pos{.x = packet.chunk_pos().x(),
+ .z = packet.chunk_pos().z()};
+ const auto find_it = ::chunks.find(pos);
+ if (find_it == std::end(::chunks)) {
+ return;
+ }
+ find_it->second.emplace(
+ client::state.seed, pos,
+ shared::world::chunk::make_blocks_from_chunk(packet));
+ // Force the surrounding chunks to regenerate their vbos.
+ // It's possible we could only generate the ones that we actually need to
+ // generate, but that's not really a priority atm.
+ for (auto x = -1; x <= 1; ++x) {
+ for (auto z = -1; z <= 1; ++z) {
+ if (std::abs(x) == 1 && std::abs(z) == 1) {
+ continue;
+ }
+ const auto find_update_it =
+ ::chunks.find(pos + shared::math::coords{x, z});
+ if (find_update_it == std::end(::chunks)) {
+ continue;
+ }
+ if (!find_update_it->second.has_value()) {
+ continue;
+ }
+ find_update_it->second->should_regenerate_vbo = true;
+ }
+ }
+}
+
+static void
+handle_server_message_packet(const proto::server_message& packet) noexcept {
+ const bool fatal = packet.fatal();
+ const std::string message =
+ "client: received " + std::string{fatal ? "fatal" : ""} +
+ " message from the server \"" + packet.message() + "\"\n";
+ if (!fatal) {
+ shared::print::notify(message);
+ return;
+ }
+
+ shared::print::warn(message);
+ shared::should_exit = true;
+}
+
+static void parse_packet(const proto::packet& packet) noexcept {
+
+ if (packet.has_init_packet()) {
+ handle_init_packet(packet.init_packet());
+ } else if (packet.has_player_packet()) {
+ handle_player_packet(packet.player_packet());
+ } else if (packet.has_remove_player_packet()) {
+ handle_remove_packet(packet.remove_player_packet());
+ } else if (packet.has_hear_player_packet()) {
+ handle_hear_packet(packet.hear_player_packet());
+ } else if (packet.has_chunk_packet()) {
+ handle_chunk_packet(packet.chunk_packet());
+ } else if (packet.has_server_message_packet()) {
+ handle_server_message_packet(packet.server_message_packet());
+ }
+#ifndef NDEBUG
+ else {
+ shared::print::warn("client: unhandled packet type\n");
+ }
+#endif
+}
+
+static shared::net::connection make_connection(const std::string_view address,
+ const std::string_view port) {
+ constexpr addrinfo hints = {.ai_flags = AI_PASSIVE,
+ .ai_family = AF_INET,
+ .ai_socktype = SOCK_STREAM};
+ const auto info = shared::net::get_addr_info(address, port, &hints);
+ const auto sock = shared::net::make_socket(info.get());
+ shared::net::connect_socket(sock, info.get());
+ return shared::net::connection(sock);
+}
+
+static void update_chunks(shared::net::connection& connection) noexcept {
+
+ const auto draw_distance = client::state.draw_distance;
+ const shared::math::coords& lp_pos = get_localplayer(::players).chunk_pos;
+
+ // Remove bad chunks.
+ std::erase_if(::chunks, [&](const auto& chunk) {
+ const bool should_erase =
+ !shared::math::is_inside_draw(chunk.first, lp_pos, draw_distance);
+ if (should_erase) {
+ connection.rsend_packet(make_remove_chunk_packet(chunk.first));
+ }
+ return should_erase;
+ });
+
+ for (int dist = 0; dist <= client::state.draw_distance; ++dist) {
+
+ const auto maybe_add_chunk = [&](const int x, const int z) -> bool {
+ const auto pos = shared::math::coords{x + lp_pos.x, z + lp_pos.z};
+ if (!is_inside_draw(pos, lp_pos, draw_distance)) {
+ return false;
+ }
+ if (::chunks.contains(pos)) {
+ return false;
+ }
+ connection.rsend_packet(make_get_chunk_packet(pos));
+ ::chunks.emplace(pos, std::nullopt);
+ return true;
+ };
+
+ int x = -dist;
+ int z = dist;
+
+ // Does a spiral pattern, but it's basically unnoticable :(
+ for (int i = 0; i < dist * 2 + 1; ++i) {
+ if (maybe_add_chunk(x, z)) {
+ return;
+ }
+ x += (i < dist * 2);
+ }
+ for (int i = 0; i < dist * 2; ++i) {
+ --z;
+ if (maybe_add_chunk(x, z)) {
+ return;
+ }
+ }
+ for (int i = 0; i < dist * 2; ++i) {
+ --x;
+ if (maybe_add_chunk(x, z)) {
+ return;
+ }
+ }
+ for (int i = 0; i < dist * 2 - 1; ++i) {
+ ++z;
+ if (maybe_add_chunk(x, z)) {
+ return;
+ }
+ }
+ }
+}
+
+static void update_state() noexcept {
+ client::state.player_count = ::players.size();
+ client::state.requested_chunk_count =
+ static_cast<std::uint32_t>(std::ranges::count_if(
+ ::chunks, [](const auto& c) { return !c.second.has_value(); }));
+ client::state.networked_chunk_count =
+ std::size(::chunks) - client::state.requested_chunk_count;
+}
+
+static proto::packet make_say_packet(const std::string& text) noexcept {
+ proto::packet packet;
+
+ const auto sub_say_packet = packet.mutable_say_packet();
+ sub_say_packet->set_text(text);
+
+ return packet;
+}
+
+// unfortunate non-static :(
+void send_say_packet(const std::string& text) noexcept {
+ client::state.connection->rsend_packet(make_say_packet(text));
+}
+
+static void handle_button_input() noexcept {
+ if (input::is_key_pressed(SDLK_z)) {
+ client::render::camera::get_xfov() = 30.0f;
+ } else {
+ client::render::camera::get_xfov() =
+ client::settings::get({"gameplay", "fov"}, 100.0f);
+ }
+
+ // Don't build our movement commands if we're inputting text.
+ if (input::state.typing) {
+ return;
+ }
+
+ auto& lp = get_localplayer(::players);
+
+ using spm = shared::player::mask;
+ lp.commands |= spm::forward * input::is_key_pressed(SDLK_w);
+ lp.commands |= spm::left * input::is_key_pressed(SDLK_a);
+ lp.commands |= spm::backward * input::is_key_pressed(SDLK_s);
+ lp.commands |= spm::right * input::is_key_pressed(SDLK_d);
+ lp.commands |= spm::jump * input::is_key_pressed(SDLK_SPACE);
+ lp.commands |= spm::crouch * input::is_key_pressed(SDLK_LCTRL);
+ lp.commands |= spm::sprint * input::is_key_pressed(SDLK_LSHIFT);
+ lp.commands |= spm::attack * input::is_key_pressed(SDL_BUTTON_LEFT);
+}
+
+static void update_input() noexcept {
+ client::input::update();
+
+ if (!client::window::is_open()) {
+ handle_button_input();
+ }
+
+ if (client::input::state.quit) {
+ shared::should_exit = true;
+ }
+}
+
+static proto::packet
+make_move_packet(const shared::player& localplayer) noexcept {
+ proto::packet packet;
+
+ const auto move_packet = packet.mutable_move_packet();
+ move_packet->set_commands(localplayer.commands);
+ shared::net::set_angles(*move_packet->mutable_viewangles(),
+ localplayer.viewangles);
+
+ return packet;
+}
+
+static void send_move_packet(shared::net::connection& connection) noexcept {
+ const auto& localplayer = get_localplayer(::players);
+ connection.usend_packet(make_move_packet(localplayer));
+}
+
+static void update_players() noexcept {
+ const auto lp_index = get_localplayer(::players).index;
+ // pvs, remove clients outside of chunks we own
+ std::erase_if(::players, [&](const auto& player) {
+ if (player.index == lp_index) {
+ return false;
+ }
+
+ // Players should be removed if the chunk can't draw.
+ const auto chunk_it = ::chunks.find(player.chunk_pos);
+ if (chunk_it == std::end(::chunks)) {
+ return true;
+ }
+ const auto& chunk = chunk_it->second;
+ if (!chunk.has_value()) {
+ return false;
+ }
+
+ if (!chunk->can_draw()) {
+ return true;
+ }
+ return false;
+ });
+}
+
+// requires SDL_MOUSEMOTION
+static void handle_mousemotion(const SDL_Event& event) noexcept {
+ const float sens =
+ settings::get<float>({"gameplay", "mouse_sensitivity"}, 0.0235f);
+
+ auto& lp = get_localplayer(::players);
+ auto& angles = lp.viewangles;
+
+ const float pitch_offset = static_cast<float>(event.motion.yrel) * sens;
+ const float yaw_offset = static_cast<float>(event.motion.xrel) * sens;
+
+ angles.pitch = std::clamp(angles.pitch - glm::radians(pitch_offset),
+ glm::radians(-89.0f), glm::radians(89.0f));
+ angles.yaw =
+ std::fmod(angles.yaw + glm::radians(yaw_offset), glm::radians(360.0f)) +
+ (angles.yaw < 0.0f) * glm::radians(360.0f);
+}
+
+// requires SDL_MOUSEBUTTONDOWN
+static void handle_mousebuttons(const SDL_Event& event) noexcept {
+ if (event.button.button != SDL_BUTTON_LEFT &&
+ event.button.button != SDL_BUTTON_RIGHT) {
+ return;
+ }
+
+ const auto mode = event.button.button == SDL_BUTTON_LEFT
+ ? client::movement::interact_mode::remove
+ : client::movement::interact_mode::add;
+
+ const auto position =
+ client::movement::interact(get_localplayer(::players), mode, ::chunks);
+
+ if (!position.has_value()) {
+ return;
+ }
+
+ const auto& [chunk_pos, block_pos] = *position;
+
+ auto& connection = *client::state.connection;
+ switch (mode) {
+ case client::movement::interact_mode::add:
+ connection.usend_packet(make_add_block_packet(chunk_pos, block_pos));
+ break;
+ case client::movement::interact_mode::remove:
+ connection.usend_packet(make_remove_block_packet(chunk_pos, block_pos));
+ ::last_block = ::chunks[chunk_pos]->get_block(block_pos);
+ break;
+ }
+}
+
+static void handle_events(const SDL_Event& event) noexcept {
+ if (client::window::is_open()) {
+ return;
+ }
+
+ switch (event.type) {
+ case SDL_MOUSEBUTTONDOWN:
+ handle_mousebuttons(event);
+ break;
+ case SDL_MOUSEMOTION:
+ handle_mousemotion(event);
+ break;
+ default:
+ break;
+ }
+}
+
+static void authenticate_client(shared::net::connection& connection) noexcept {
+ const auto rand_alphanum_string = [](const int size) -> std::string {
+ static const std::string allowed = "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "1234567890";
+ std::string ret;
+ for (int i = 0; i < size; ++i) {
+ std::ranges::sample(allowed, std::back_inserter(ret), 1,
+ std::random_device{});
+ }
+
+ return ret;
+ };
+
+ const std::string username = settings::get<std::string>(
+ {"auth", "username"},
+ rand_alphanum_string(shared::MAX_USER_PASS_LENGTH));
+ const std::string password = settings::get<std::string>(
+ {"auth", "password"},
+ rand_alphanum_string(shared::MAX_USER_PASS_LENGTH));
+
+ connection.rsend_packet(make_auth_packet(username, password));
+}
+
+void main(const std::string_view address, const std::string_view port) {
+ client::state.address = address;
+ client::state.port = port;
+
+ shared::net::connection connection = make_connection(address, port);
+ shared::print::notify("client: connected to " + std::string{address} + ':' +
+ std::string{port} + '\n');
+ client::state.connection = &connection;
+
+ client::render::init();
+ client::input::register_event_handler(&handle_events);
+
+ authenticate_client(connection);
+
+ while (!shared::should_exit) {
+ // Parse all new packets.
+ while (const auto packet = connection.recv_packet()) {
+ parse_packet(packet.value());
+ }
+
+ // Wait for localplayer to be constructed before doing anything.
+ if (::players.empty()) {
+ continue;
+ }
+
+ // Handle input, which may prime text input -> send it to the
+ // server.
+ update_input();
+ // Send our localplayer to the server.
+ if (::should_send_move) {
+ send_move_packet(connection);
+ ::should_send_move = false;
+ }
+ update_chunks(connection);
+ update_state();
+ update_players();
+
+ client::draw::draw(::players, ::chunks);
+
+ if (!connection.good()) {
+ shared::print::notify("client: disconnected for \"" +
+ connection.get_bad_reason() + "\"\n");
+ shared::should_exit = true;
+ }
+ }
+ shared::print::notify("client: disconnecting from server\n");
+
+ client::render::quit();
+ client::settings::save();
+}
+
+} // namespace client