#include "client.hh" namespace { std::queue received_chunks; } // namespace namespace client { 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 proto::packet make_request_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, const std::uint32_t& active) noexcept { proto::packet packet; const auto add_block_packet = packet.mutable_add_block_packet(); shared::net::set_coords(add_block_packet->mutable_chunk_pos(), coords); shared::net::set_ivec3(add_block_packet->mutable_block_pos(), pos); add_block_packet->set_active_item(active); return packet; } static proto::packet make_remove_block_packet(const shared::math::coords& coords, const glm::ivec3& pos) noexcept { proto::packet packet; const auto remove_block_packet = packet.mutable_remove_block_packet(); shared::net::set_coords(remove_block_packet->mutable_chunk_pos(), coords); shared::net::set_ivec3(remove_block_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 proto::packet make_move_packet(const client::moveable& localplayer) noexcept { proto::packet packet; const auto move_packet = packet.mutable_move_packet(); move_packet->set_commands(localplayer.get_commands()); shared::net::set_angles(move_packet->mutable_viewangles(), localplayer.get_angles()); move_packet->set_active_item(localplayer.get_active_item()); move_packet->set_sequence(localplayer.get_latest_sequence()); return packet; } static void handle_init_packet(const proto::init& packet) noexcept { // Draw distance is the std::min, but we make it more verbose. if (const auto& wanted = state::draw_distance = settings::get({"video", "draw_distance"}, 32); wanted != packet.draw_distance()) { if (wanted < packet.draw_distance()) { state::draw_distance = wanted; } else { shared::print::warn << shared::print::time << "client: server supported draw_distance (" << packet.draw_distance() << ") less than requested (" << wanted << ")\n"; state::draw_distance = packet.draw_distance(); } } state::seed = packet.seed(); state::tickrate = packet.tickrate(); state::tick = packet.tick(); state::delta_ticks = 0.0f; // amount of time passed (in ticks) const auto& localplayer = packet.localplayer(); const auto& index = localplayer.animate().entity().index(); state::localplayer_index = index; state::entities.emplace(index, std::make_unique(localplayer)); } static void handle_animate_update_packet(const proto::animate_update& packet) noexcept { const auto& animate = packet.animate(); const auto& index = animate.entity().index(); const auto entity_it = get_entity_it(index); if (entity_it == std::end(state::entities)) { state::entities.emplace(index, std::make_unique(animate)); return; } const auto animate_ptr = dynamic_cast(&*entity_it->second); if (animate_ptr == nullptr) { return; } const shared::tick_t tick = packet.tick(); // Localplayer updates time factor and is update is called with the sequence // number instead of the tick. if (packet.has_sequence()) { const shared::tick_t sequence = packet.sequence(); animate_ptr->update_time_factor(sequence, tick); animate_ptr->notify(animate, sequence, true); return; } animate_ptr->notify(animate, tick, true); } // Remove the client whose element is equal to pkt.index. static void handle_remove_entity_packet(const proto::remove_entity& packet) noexcept { const auto entity_it = get_entity_it(packet.index()); if (entity_it == std::end(state::entities)) { return; } state::entities.erase(entity_it); } static void handle_hear_packet(const proto::hear_player& packet) noexcept { const auto entity_it = get_entity_it(packet.index()); if (entity_it == std::end(state::entities)) { return; } const auto player_ptr = dynamic_cast(&*entity_it->second); if (player_ptr == nullptr) { return; } player_ptr->message.emplace(packet.text()); } static void handle_chunk_packet(proto::chunk& packet) noexcept { // Ratelimited parsing, moved into a vector. ::received_chunks.push(std::move(packet)); } 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 << shared::print::time << message; return; } shared::print::warn << shared::print::time << message; shared::should_exit = true; } static void parse_packet(proto::packet&& packet) noexcept { if (packet.has_init_packet()) { handle_init_packet(packet.init_packet()); } else if (packet.has_animate_update_packet()) { handle_animate_update_packet(packet.animate_update_packet()); } else if (packet.has_remove_entity_packet()) { handle_remove_entity_packet(packet.remove_entity_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.mutable_chunk_packet()); } else if (packet.has_server_message_packet()) { handle_server_message_packet(packet.server_message_packet()); } #ifndef NDEBUG else { shared::print::warn << shared::print::time << "client: unhandled packet type\n"; } #endif } static void regenerate_surrounding_chunks(const shared::math::coords& pos) { // 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 = state::chunks.find(pos + shared::math::coords{x, z}); if (find_update_it == std::end(state::chunks)) { continue; } if (!find_update_it->second.has_value()) { continue; } find_update_it->second->should_regenerate_vbo = true; } } } static void parse_new_chunks() { // Add new chunks. We have to ratelimit these because the server responds // so quickly that our frametime dives as it parses these new chunk packets. constexpr int CHUNKS_PER_FRAME = 1; for (int i = 0; i < CHUNKS_PER_FRAME && !::received_chunks.empty(); ++i, ::received_chunks.pop()) { const proto::chunk& packet = ::received_chunks.front(); const shared::math::coords pos{packet.chunk_pos().x(), packet.chunk_pos().z()}; const auto find_it = state::chunks.find(pos); if (find_it == std::end(state::chunks)) { continue; } find_it->second.emplace(state::seed, packet); regenerate_surrounding_chunks(pos); } } static void erase_bad_chunks(const shared::math::coords& lp_pos) { const auto draw_distance = state::draw_distance; std::erase_if(state::chunks, [&](const auto& chunk) { const bool should_erase = !shared::math::coords::is_inside_draw( chunk.first, lp_pos, draw_distance); if (should_erase) { state::connection->rsend_packet( make_remove_chunk_packet(chunk.first)); } return should_erase; }); } static void request_new_chunks(const shared::math::coords& lp_pos) { for (int dist = 0; dist <= state::draw_distance; ++dist) { const auto draw_distance = state::draw_distance; 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 (!shared::math::coords::is_inside_draw(pos, lp_pos, draw_distance)) { return false; } if (state::chunks.contains(pos)) { return false; } state::connection->rsend_packet(make_request_chunk_packet(pos)); state::chunks.emplace(pos, std::nullopt); return true; }; int x = -dist; int z = dist; 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_chunks() { parse_new_chunks(); const shared::math::coords& lp_pos = get_localplayer().get_chunk_pos(); erase_bad_chunks(lp_pos); request_new_chunks(lp_pos); } static void handle_button_input() noexcept { // Don't build our movement commands if we're inputting text. if (input::state.typing) { return; } using spm = shared::player::mask; auto& commands = get_localplayer().get_mutable_commands(); commands |= spm::forward * input::is_key_pressed(SDLK_w); commands |= spm::left * input::is_key_pressed(SDLK_a); commands |= spm::backward * input::is_key_pressed(SDLK_s); commands |= spm::right * input::is_key_pressed(SDLK_d); commands |= spm::jump * input::is_key_pressed(SDLK_SPACE); commands |= spm::crouch * input::is_key_pressed(SDLK_LCTRL); commands |= spm::sprint * input::is_key_pressed(SDLK_LSHIFT); commands |= spm::attack * input::is_key_pressed(SDL_BUTTON_LEFT); } static void update_input() noexcept { input::update(); if (input::is_key_pressed(SDLK_z)) { render::camera::get_xfov() = 30.0f; } else { render::camera::get_xfov() = settings::get({"gameplay", "fov"}, 100.0f); } if (!window::is_open()) { handle_button_input(); } if (input::state.quit) { shared::should_exit = true; } } static void update_delta_ticks() noexcept { static shared::time_point_t last = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now(); const auto delta_ticks = shared::get_duration_seconds(now - last) / shared::get_duration_seconds(state::get_time_per_tick()); state::delta_ticks += state::time_factor * delta_ticks; last = now; } static void update_pre_move() { state::player_count = state::entities.size(); state::requested_chunk_count = static_cast( std::ranges::count_if(state::chunks, [](const auto& c) { return !c.second.has_value(); })); state::networked_chunk_count = std::size(state::chunks) - state::requested_chunk_count; update_delta_ticks(); update_input(); update_chunks(); } static void interp_entities() { for (auto& [index, entity] : state::entities) { const auto animate_ptr = dynamic_cast(&*entity); if (animate_ptr == nullptr) { continue; } animate_ptr->interpolate(); } } static void remove_bad_entities() { const auto lp_index = *state::localplayer_index; // pvs, remove clients outside of chunks we own std::erase_if(state::entities, [&](const auto& pair) { const auto& [idx, entity] = pair; const auto player_ptr = dynamic_cast(&*entity); if (player_ptr == nullptr) { return false; } if (player_ptr->get_index() == lp_index) { return false; } // Players should be removed if the chunk can't draw. const auto chunk_it = state::chunks.find(player_ptr->get_chunk_pos()); if (chunk_it == std::end(state::chunks)) { return true; } const auto& chunk = chunk_it->second; if (chunk.has_value() && !chunk->can_draw()) { return true; } return false; }); } static void update_entities() noexcept { interp_entities(); remove_bad_entities(); } static void update_post_move() { update_entities(); } static void maybe_send_move() noexcept { // An unknown amount of time has passed since we last rendered and this may // be more than one tick. We do nothing if it's less than one tick. const auto num_ticks_passed = static_cast(state::delta_ticks); if (num_ticks_passed <= 0) { return; } // Extrapolate for the number of ticks passed. auto& localplayer = get_localplayer(); for (auto i = 0u; i < num_ticks_passed; ++i) { ++state::tick; // possibly after? state::delta_ticks -= 1.0f; localplayer.extrapolate(); ++localplayer.get_mutable_latest_sequence(); state::connection->usend_packet(make_move_packet(localplayer)); } // reset all commands except for flying, more later probably const auto retained = shared::animate::flying; localplayer.get_mutable_commands() &= retained; } // 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(); auto& angles = lp.get_mutable_angles(); const float pitch_offset = static_cast(event.motion.yrel) * sens; const float yaw_offset = static_cast(event.motion.xrel) * sens; angles.pitch -= glm::radians(pitch_offset); angles.yaw += glm::radians(yaw_offset); angles.normalise(); angles.clamp(); } // 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; } auto& lp = get_localplayer(); const auto mode = event.button.button == SDL_BUTTON_LEFT ? movement::interact_mode::remove : movement::interact_mode::add; const auto position = movement::interact(lp, mode, state::chunks); if (!position.has_value()) { return; } const auto& [chunk_pos, block_pos] = *position; auto& block = state::chunks.find(chunk_pos)->second->get_block(block_pos); if (mode == movement::interact_mode::remove) { lp.inventory.maybe_add(shared::item::block::get_type(block.type), 1, client::item::make_item); block.type = shared::world::block::type::air; state::connection->rsend_packet( make_remove_block_packet(chunk_pos, block_pos)); } else { // Adding is more complicated, needs the active item. const auto& active = lp.get_active_item(); auto& item = lp.inventory.contents[active]; if (item == nullptr) { return; } const auto block_ptr = dynamic_cast(&*item); if (block_ptr == nullptr) { return; } lp.inventory.decrement(active); block.type = block_ptr->type; state::connection->rsend_packet( make_add_block_packet(chunk_pos, block_pos, active)); } regenerate_surrounding_chunks(chunk_pos); } static void handle_space(const SDL_Event& event) noexcept { if (event.type == SDL_KEYUP) { return; } auto& commands = get_localplayer().get_mutable_commands(); commands |= shared::animate::mask::jump; if (event.key.repeat) { return; } constexpr auto JUMP_FLY_DELAY = std::chrono::milliseconds(250); static std::optional prev = std::nullopt; const auto now = std::chrono::steady_clock::now(); if (prev.has_value() && now < *prev + JUMP_FLY_DELAY) { commands ^= shared::animate::mask::flying; prev.reset(); return; } prev.emplace(now); } static void handle_keys(const SDL_Event& event) noexcept { switch (event.key.keysym.sym) { case SDLK_SPACE: handle_space(event); break; default: break; } } static void handle_events(const SDL_Event& event) noexcept { if (window::is_open()) { return; } if (!state::localplayer_index.has_value()) { return; } switch (event.type) { case SDL_KEYDOWN: case SDL_KEYUP: handle_keys(event); break; case SDL_MOUSEBUTTONDOWN: handle_mousebuttons(event); break; case SDL_MOUSEMOTION: handle_mousemotion(event); break; default: break; } } static std::string get_username() { const settings::setting_pair_t loc = {"gameplay", "username"}; if (const auto username = settings::maybe_get_setting_str(loc); username.has_value()) { return *username; } shared::print::notify << shared::print::time << "first launch detected, username required\n"; std::string ret; while (ret.empty() || !std::ranges::all_of(ret, isalnum)) { std::cin.clear(); std::cout << "enter a valid username: "; std::getline(std::cin, ret); } settings::set_setting_str(loc, ret); return ret; } static std::string get_password(const std::string_view& address) { const settings::setting_pair_t loc = { "auth", std::string{address == "0.0.0.0" ? "localhost" : address}}; if (const auto password = settings::maybe_get_setting_str(loc); password.has_value()) { return *password; } // We generate a random string as our password. Our password has decent // complexitity but considering we don't use any encryption it is worthless // to any real attacker and just basic authentication. std::string ret; std::random_device rand{}; std::uniform_int_distribution uniform{'a', 'z'}; for (int i = 0; i < 256; ++i) { const char random_char = uniform(rand); ret.push_back(random_char); } settings::set_setting_str(loc, ret); return ret; } static void send_auth_packet(const std::string_view& address, shared::net::connection& connection) { const std::string username = get_username(); const std::string password = get_password(address); connection.rsend_packet(make_auth_packet(username, password)); } static bool should_do_loop(shared::net::connection& connection) noexcept { if (shared::should_exit) { return false; } if (!connection.good()) { shared::print::notify << shared::print::time << "client: disconnected for \"" << connection.get_bad_reason() << "\"\n"; shared::should_exit = true; // cleanup server if necessary return false; } return true; } static void do_client_loop(shared::net::connection& connection) { while (should_do_loop(connection)) { connection.poll(); while (auto packet = connection.recv_packet()) { parse_packet(std::move(*packet)); } if (!state::has_initialised()) { continue; } update_pre_move(); maybe_send_move(); update_post_move(); render::draw(state::entities, state::chunks); } } static void init_client(const std::string_view& address, const std::string_view& port, shared::net::connection& connection) { // Setup client state, some vars should be cleaned up later (raii lol). state::address = address; state::port = port; state::connection = &connection; input::register_event_handler(&handle_events); send_auth_packet(address, connection); render::init(); } static void cleanup_client() { state::connection = nullptr; state::localplayer_index.reset(); state::chunks.clear(); state::entities.clear(); render::quit(); settings::save(); } void main(const std::string_view address, const std::string_view port) { shared::net::connection connection = make_connection(address, port); init_client(address, port, connection); shared::print::notify << shared::print::time << "client: connected to " << address << ':' << port << '\n'; do_client_loop(connection); shared::print::notify << shared::print::time << "client: disconnecting from server\n"; cleanup_client(); } } // namespace client