diff options
Diffstat (limited to 'src/client/client.cc')
| -rw-r--r-- | src/client/client.cc | 515 |
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 |
