#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(::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::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({"gameplay", "mouse_sensitivity"}, 0.0235f); auto& lp = get_localplayer(::players); auto& angles = lp.viewangles; const float pitch_offset = static_cast(event.motion.yrel) * sens; const float yaw_offset = static_cast(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( {"auth", "username"}, rand_alphanum_string(shared::MAX_USER_PASS_LENGTH)); const std::string password = settings::get( {"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