diff options
Diffstat (limited to 'src/client')
60 files changed, 3354 insertions, 1608 deletions
diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt new file mode 100644 index 0000000..e6994ab --- /dev/null +++ b/src/client/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 3.18) + +project(blockgame_linux) + +file (GLOB_RECURSE SOURCE_FILES CONFIGURE_DEPENDS + "*.cc" +) +file (GLOB_RECURSE HEADER_FILES CONFIGURE_DEPENDS + "*.hh" + "../server/*.hh" + "../shared/*.hh" +) +add_executable(${PROJECT_NAME} + ${SOURCE_FILES} +) + +find_library(LIB_SDL2 sdl2 SDL2 REQUIRED) +find_library(LIB_EPOXY epoxy epoxy EPOXY libepoxy REQUIRED) +find_library(LIB_SQLITE3 sqlite3 SQLITE3 REQUIRED) +find_library(LIB_FREETYPE freetype libfreetype REQUIRED) +find_library(LIB_ASSIMP assimp libassimp REQUIRED) +find_library(LIB_PROTOBUF protobuf libprotobuf REQUIRED) +find_package(Boost COMPONENTS iostreams REQUIRED) +find_package(Threads REQUIRED) +find_package(Backtrace REQUIRED) +find_package(Freetype REQUIRED) + +target_compile_options(${PROJECT_NAME} PRIVATE + -Wall -Wextra -Wshadow -Wdouble-promotion -Wformat=2 -Wundef -fno-common + -Wconversion -Wpedantic -std=c++20 -O2 + -Wno-exceptions + -Wno-missing-field-initializers -Wno-unknown-pragmas +) +target_compile_options(${PROJECT_NAME} PRIVATE + -fstack-protector-strong -fno-omit-frame-pointer #-fsanitize=undefined -fsanitize-trap=undefined +) +target_link_options(${PROJECT_NAME} PRIVATE + -fstack-protector-strong #-fsanitize=undefined -fsanitize-trap=undefined +) +target_include_directories(${PROJECT_NAME} PRIVATE + "../../src" + ${FREETYPE_INCLUDE_DIRS} +) +target_link_libraries(${PROJECT_NAME} PRIVATE + ${LIB_SDL2} + ${LIB_EPOXY} + ${LIB_FREETYPE} + ${LIB_ASSIMP} + ${LIB_PROTOBUF} + ${LIB_SQLITE3} + ${Backtrace_LIBRARIES} + ${Threads_LIBRARIES} + ${Boost_LIBRARIES} + ${FREETYPE_LIBRARIES} + shared + server +) +target_precompile_headers(${PROJECT_NAME} PRIVATE + ${HEADER_FILES} +) diff --git a/src/client/client.cc b/src/client/client.cc index b4cacdc..28b3163 100644 --- a/src/client/client.cc +++ b/src/client/client.cc @@ -1,29 +1,29 @@ #include "client.hh" namespace { -bool should_send_move = false; // Ratelimit move packets by player packets. +std::queue<proto::chunk> received_chunks; -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 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_get_chunk_packet(const shared::math::coords& coords) noexcept { +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); + shared::net::set_coords(chunk_packet->mutable_chunk_pos(), coords); return packet; } @@ -39,14 +39,16 @@ static proto::packet make_auth_packet(const std::string& username, return packet; } -static proto::packet make_add_block_packet(const shared::math::coords& coords, - const glm::ivec3& pos) noexcept { +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 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)); + 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; } @@ -56,9 +58,9 @@ 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); + 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; } @@ -69,93 +71,113 @@ 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); + 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 { - 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(); + // 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) - ::players.emplace_back(shared::net::get_player(localplayer)); + const auto& localplayer = packet.localplayer(); + const auto& index = localplayer.animate().entity().index(); + + state::localplayer_index = index; + state::entities.emplace(index, std::make_unique<player>(localplayer)); } -static void handle_player_packet(const proto::player& packet) noexcept { - if (std::size(::players) <= 0) { +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<player>(animate)); return; } - const auto player_it = get_player_it(packet.index()); + const auto animate_ptr = dynamic_cast<class animate*>(&*entity_it->second); + if (animate_ptr == nullptr) { + return; + } - shared::player player = shared::net::get_player(packet); + const shared::tick_t tick = packet.tick(); - if (player_it == std::end(::players)) { - ::players.emplace_back(player); + // 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; } - // 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); + animate_ptr->notify(animate, tick, true); } // 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)) { +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; } - ::players.erase(player_it); + + state::entities.erase(entity_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)) { + const auto entity_it = get_entity_it(packet.index()); + if (entity_it == std::end(state::entities)) { 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)) { + const auto player_ptr = dynamic_cast<player*>(&*entity_it->second); + if (player_ptr == nullptr) { 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; - } - } + + 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 @@ -165,81 +187,111 @@ handle_server_message_packet(const proto::server_message& packet) noexcept { "client: received " + std::string{fatal ? "fatal" : ""} + " message from the server \"" + packet.message() + "\"\n"; if (!fatal) { - shared::print::notify(message); + shared::print::notify << shared::print::time << message; return; } - shared::print::warn(message); + shared::print::warn << shared::print::time << message; shared::should_exit = true; } -static void parse_packet(const proto::packet& packet) noexcept { - +static void parse_packet(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_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.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("client: unhandled packet type\n"); + shared::print::warn << shared::print::time + << "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 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 update_chunks(shared::net::connection& connection) noexcept { +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 auto draw_distance = client::state.draw_distance; - const shared::math::coords& lp_pos = get_localplayer(::players).chunk_pos; + const proto::chunk& packet = ::received_chunks.front(); + const shared::math::coords pos{packet.chunk_pos().x(), + packet.chunk_pos().z()}; - // 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); + 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) { - connection.rsend_packet(make_remove_chunk_packet(chunk.first)); + state::connection->rsend_packet( + make_remove_chunk_packet(chunk.first)); } return should_erase; }); +} - for (int dist = 0; dist <= client::state.draw_distance; ++dist) { +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 (!is_inside_draw(pos, lp_pos, draw_distance)) { + if (!shared::math::coords::is_inside_draw(pos, lp_pos, + draw_distance)) { return false; } - if (::chunks.contains(pos)) { + if (state::chunks.contains(pos)) { return false; } - connection.rsend_packet(make_get_chunk_packet(pos)); - ::chunks.emplace(pos, std::nullopt); + state::connection->rsend_packet(make_request_chunk_packet(pos)); + state::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; @@ -267,125 +319,162 @@ static void update_chunks(shared::net::connection& connection) noexcept { } } -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); +static void update_chunks() { + parse_new_chunks(); - return packet; -} - -// unfortunate non-static :( -void send_say_packet(const std::string& text) noexcept { - client::state.connection->rsend_packet(make_say_packet(text)); + 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 { - 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); + 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 { - client::input::update(); + 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 (!client::window::is_open()) { + if (!window::is_open()) { handle_button_input(); } - if (client::input::state.quit) { + if (input::state.quit) { shared::should_exit = true; } } -static proto::packet -make_move_packet(const shared::player& localplayer) noexcept { - proto::packet packet; +static void update_delta_ticks() noexcept { + static shared::time_point_t last = std::chrono::steady_clock::now(); - const auto move_packet = packet.mutable_move_packet(); - move_packet->set_commands(localplayer.commands); - shared::net::set_angles(*move_packet->mutable_viewangles(), - localplayer.viewangles); + 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()); - return packet; + state::delta_ticks += state::time_factor * delta_ticks; + + last = now; } -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_pre_move() { + state::player_count = state::entities.size(); + state::requested_chunk_count = static_cast<std::uint32_t>( + 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 update_players() noexcept { - const auto lp_index = get_localplayer(::players).index; +static void interp_entities() { + for (auto& [index, entity] : state::entities) { + const auto animate_ptr = dynamic_cast<animate*>(&*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(::players, [&](const auto& player) { - if (player.index == lp_index) { + std::erase_if(state::entities, [&](const auto& pair) { + const auto& [idx, entity] = pair; + + const auto player_ptr = dynamic_cast<const player*>(&*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 = ::chunks.find(player.chunk_pos); - if (chunk_it == std::end(::chunks)) { + 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()) { - return false; - } - - if (!chunk->can_draw()) { + 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<unsigned>(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<float>({"gameplay", "mouse_sensitivity"}, 0.0235f); - auto& lp = get_localplayer(::players); - auto& angles = lp.viewangles; + auto& lp = get_localplayer(); + auto& angles = lp.get_mutable_angles(); 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); + angles.pitch -= glm::radians(pitch_offset); + angles.yaw += glm::radians(yaw_offset); + angles.normalise(); + angles.clamp(); } // requires SDL_MOUSEBUTTONDOWN @@ -395,37 +484,93 @@ static void handle_mousebuttons(const SDL_Event& event) noexcept { return; } + auto& lp = get_localplayer(); 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); - + ? 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<const shared::item::block*>(&*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); +} - auto& connection = *client::state.connection; - switch (mode) { - case client::movement::interact_mode::add: - connection.usend_packet(make_add_block_packet(chunk_pos, block_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<shared::time_point_t> 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; - 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); + default: break; } } static void handle_events(const SDL_Event& event) noexcept { - if (client::window::is_open()) { + 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; @@ -437,79 +582,124 @@ static void handle_events(const SDL_Event& event) noexcept { } } -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{}); - } +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; + } - return ret; - }; + 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; +} - 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)); +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; + } - connection.rsend_packet(make_auth_packet(username, 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<char> 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; } -void main(const std::string_view address, const std::string_view port) { - client::state.address = address; - client::state.port = port; +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); - 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; + connection.rsend_packet(make_auth_packet(username, password)); +} + +static bool should_do_loop(shared::net::connection& connection) noexcept { + if (shared::should_exit) { + return false; + } - client::render::init(); - client::input::register_event_handler(&handle_events); + 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; + } - authenticate_client(connection); + return true; +} - while (!shared::should_exit) { - // Parse all new packets. - while (const auto packet = connection.recv_packet()) { - parse_packet(packet.value()); +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)); } - // Wait for localplayer to be constructed before doing anything. - if (::players.empty()) { + if (!state::has_initialised()) { 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); + update_pre_move(); + maybe_send_move(); + update_post_move(); - if (!connection.good()) { - shared::print::notify("client: disconnected for \"" + - connection.get_bad_reason() + "\"\n"); - shared::should_exit = true; - } + render::draw(state::entities, state::chunks); } - shared::print::notify("client: disconnecting from server\n"); +} + +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); - client::render::quit(); - client::settings::save(); + shared::print::notify << shared::print::time + << "client: disconnecting from server\n"; + cleanup_client(); } } // namespace client diff --git a/src/client/client.hh b/src/client/client.hh index 6dbabc1..0eadfe4 100644 --- a/src/client/client.hh +++ b/src/client/client.hh @@ -3,25 +3,30 @@ #include <algorithm> #include <cmath> +#include <queue> +#include <random> #include <string_view> #include <unordered_map> -#include "client/draw.hh" +#include "client/entity/player.hh" #include "client/input.hh" -#include "client/movement.hh" -#include "client/player.hh" +#include "client/item/items.hh" +#include "client/movement/movement.hh" #include "client/render/camera.hh" +#include "client/render/draw.hh" #include "client/settings.hh" #include "client/shared.hh" -#include "client/window.hh" -#include "client/world.hh" -#include "shared/math.hh" +#include "client/state/chunks.hh" +#include "client/state/entities.hh" +#include "client/state/state.hh" +#include "client/window/window.hh" +#include "client/world/chunk.hh" +#include "shared/math/math.hh" +#include "shared/net/connection.hh" #include "shared/net/net.hh" #include "shared/net/proto.hh" namespace client { -void send_say_packet(const std::string& text) noexcept; - void main(const std::string_view address, const std::string_view port); } // namespace client diff --git a/src/client/draw.hh b/src/client/draw.hh deleted file mode 100644 index 821d67b..0000000 --- a/src/client/draw.hh +++ /dev/null @@ -1,59 +0,0 @@ -#ifndef CLIENT_DRAW_HH_ -#define CLIENT_DRAW_HH_ - -#include <string_view> - -#include <glm/glm.hpp> - -#include "client/movement.hh" -#include "client/player.hh" -#include "client/render/camera.hh" -#include "client/render/render.hh" -#include "client/render/program.hh" -#include "client/shared.hh" -#include "client/window.hh" -#include "client/world.hh" - -namespace client { -namespace draw { - -// Scale takes the range [0, 1] and represents an amount of the screen. -// Offset is any value that is added after scale. -struct relative_arg { - glm::vec2 extent; - glm::vec2 offset; - - glm::vec2 to_vec2() const noexcept { - const glm::vec2& window = render::get_window_size(); - const float x = this->extent.x * window.x + this->offset.x; - const float y = this->extent.y * window.y + this->offset.y; - return {x, y}; - } -}; - -struct rectangle_args { - relative_arg pos; - relative_arg size; - glm::vec4 colour; -}; - -struct text_args { - relative_arg pos; - float extent_height; // we don't get width here, - float offset_height; - glm::vec4 colour; - bool has_backing; - bool is_centered; - bool is_vcentered; -}; - -void draw_rectangle(const rectangle_args& args) noexcept; -void draw_colour(const glm::vec4& colour) noexcept; -void draw_text(const std::string_view text, const text_args& args) noexcept; - -void draw(client::players& players, client::world::chunk::map& chunks) noexcept; - -} // namespace draw -} // namespace client - -#endif diff --git a/src/client/entity/animate.cc b/src/client/entity/animate.cc new file mode 100644 index 0000000..9142b55 --- /dev/null +++ b/src/client/entity/animate.cc @@ -0,0 +1,158 @@ +#include "player.hh" + +namespace client { + +// We might start receiving data slower or faster. We compensate by adjusting +// how fast time progresses on our client.; +void animate::update_time_factor(const shared::tick_t& sequence, + const shared::tick_t& tick) noexcept { + + // Check if we're older than the latest, don't update if so. + if (std::ranges::any_of(this->updates, [&](const auto& update) { + return update.from_server && update.tick_sequence >= sequence; + })) { + return; + } + + // How many ticks in the future we prefer to be at. If the server is + // jittery, we would want this value to be greater, but this should be OK + // for most cases. A larger value decreases the max ping we can handle. + constexpr float TARGET_TICKS_AHEAD = 1.341519f; + + if (const auto tick_dist = static_cast<std::int64_t>(tick) - + static_cast<std::int64_t>(state::tick); + tick_dist > 0) { + + // We're behind, jump ahead. We only touch delta_ticks so extrapolate + // is called the correct number of times in client.cc. +#ifndef NDEBUG + shared::print::debug << shared::print::time + << "client: time_factor jumped ahead, got tick " + << tick << ", was " << state::tick << " + " + << state::delta_ticks + << ", time_factor: " << state::time_factor << '\n'; +#endif + state::delta_ticks = static_cast<float>(tick_dist) + TARGET_TICKS_AHEAD; + state::time_factor = 1.0f; + + return; + } + + // Otherwise we try to move towards the value by adjusting how fast time + // progresses. This shouldn't be possible to notice, our constants should + // be adjusted if it is. + const float time_diff = + (static_cast<float>(tick) + TARGET_TICKS_AHEAD) - + (static_cast<float>(state::tick) + state::delta_ticks); + + constexpr float MAX_TIME_FACTOR_DIFF = 0.1f; + constexpr float TIME_FACTOR_AGGRESSIVENESS = 0.25f; + state::time_factor = + std::clamp(1.0f + (time_diff * TIME_FACTOR_AGGRESSIVENESS), + 1.0f - MAX_TIME_FACTOR_DIFF, 1.0f + MAX_TIME_FACTOR_DIFF); +} + +void animate::notify(const shared::animate& animate, + const shared::tick_t& tick_sequence, + const bool from_server) noexcept { + // If it's from the server we want to update previously emplaced predicted + // (and potentially predicted) values. + if (const auto it = std::ranges::find_if(this->updates, + [&](const auto& update) { + return update.tick_sequence == + tick_sequence; + }); + it != std::end(this->updates)) { + + if (from_server) { + *it = animate_update{.tick_sequence = tick_sequence, + .animate = animate, + .from_server = from_server}; + } + + return; + } + + this->updates.push_back({.tick_sequence = tick_sequence, + .animate = animate, + .from_server = from_server}); + + std::ranges::sort(this->updates, [](const auto& a, const auto& b) { + return a.tick_sequence < b.tick_sequence; + }); +} + +float animate::get_target_ticks_back() noexcept { + // The localplayer is interpolated via the latest tick. + const unsigned base_ticks_back = [&]() -> unsigned { + if (this->index == *state::localplayer_index) { + return 0u; + } + const float base = settings::get<float>({"engine", "interp"}, 0.25f); + const float ret = base * static_cast<float>(state::tickrate); + return std::clamp(static_cast<unsigned>(ret), 0u, state::tickrate); + }(); + + return state::delta_ticks + static_cast<float>(base_ticks_back); +} + +void animate::interpolate() noexcept { + const float target_ticks_back = this->get_target_ticks_back(); + + const bool is_localplayer = this->index == *state::localplayer_index; + const auto b_it = [&, this]() { + if (is_localplayer) { + return std::rbegin(this->updates); + } + return std::ranges::find_if( + std::rbegin(this->updates), std::rend(this->updates), + [&](const auto& update) { + const unsigned target = + state::tick - static_cast<unsigned>(target_ticks_back); + return update.tick_sequence <= target; + }); + }(); + if (b_it == std::rend(this->updates)) { + return; + } + + const auto a_it = std::next(b_it); + if (a_it == std::rend(this->updates)) { + return; + } + + const glm::vec3 a_pos = a_it->animate.get_local_pos(); + const glm::vec3 b_pos = shared::movement::make_relative( + a_it->animate.get_chunk_pos(), b_it->animate.get_local_pos(), + b_it->animate.get_chunk_pos()); + + const float b_interp = [&]() { + const float diff = + is_localplayer + ? 1.0f + : static_cast<float>(b_it->tick_sequence - a_it->tick_sequence); + const float base = std::fmod(target_ticks_back, 1.0f); + const float a_dist = diff - (1.0f - base); + return a_dist / diff; + }(); + const float a_interp = (1.0f - b_interp); + + const auto& a = a_it->animate; + const auto& b = b_it->animate; + this->local_pos = a_pos * a_interp + b_pos * b_interp; + this->chunk_pos = a.get_chunk_pos(); + this->velocity = a.get_velocity() * a_interp + b.get_velocity() * b_interp; + // Update viewangles if we're not the localplayer, yaw requires special care + // because it should snap to the closest difference angle. + if (!is_localplayer) { + this->viewangles.pitch = + a.get_angles().pitch * a_interp + b.get_angles().pitch * b_interp; + const float yaw_delta = shared::math::angles::get_yaw_delta( + a.get_angles().yaw, b.get_angles().yaw); + this->viewangles.yaw = a.get_angles().yaw + yaw_delta * b_interp; + this->viewangles.normalise(); + } + shared::movement::normalise_position(this->local_pos, this->chunk_pos); +} + +} // namespace client diff --git a/src/client/entity/animate.hh b/src/client/entity/animate.hh new file mode 100644 index 0000000..79016ea --- /dev/null +++ b/src/client/entity/animate.hh @@ -0,0 +1,71 @@ +#ifndef CLIENT_ENTITY_ANIMATE_HH_ +#define CLIENT_ENTITY_ANIMATE_HH_ + +#include <algorithm> +#include <chrono> +#include <cmath> +#include <ranges> +#include <utility> +#include <vector> + +#include <boost/circular_buffer.hpp> + +#include "client/entity/entity.hh" +#include "client/movement/movement.hh" +#include "client/settings.hh" +#include "client/state/state.hh" +#include "shared/entity/animate.hh" +#include "shared/entity/entity.hh" +#include "shared/movement/movement.hh" +#include "shared/net/proto.hh" +#include "shared/shared.hh" + +namespace client { + +class animate : virtual public shared::animate, virtual public client::entity { +protected: + struct animate_update { + shared::tick_t tick_sequence; // tick or sequence, if LP then sequence. + shared::animate animate; + bool from_server; + }; + + // We have 1 second of potential player interpolation to use here when we + // initialise this with the server's tickrate. + boost::circular_buffer<animate_update> updates{state::tickrate}; + shared::tick_t latest_sequence = 0; + +private: + float get_target_ticks_back() noexcept; + +public: + animate(shared::player&& p) noexcept + : shared::entity(p), shared::animate(p), client::entity(p) {} + + animate(const proto::animate& proto) noexcept + : shared::entity(proto.entity()), shared::animate(proto), + client::entity(proto.entity()) {} + +public: + // Call when localplayer gets tick to update state timers. + void update_time_factor(const shared::tick_t& sequence, + const shared::tick_t& tick) noexcept; + + virtual void notify(const shared::animate& animate, + const shared::tick_t& tick_sequence, + const bool from_server) noexcept; + + shared::tick_t& get_mutable_latest_sequence() noexcept { + return this->latest_sequence; + } + shared::tick_t get_latest_sequence() const noexcept { + return this->latest_sequence; + } + // An animate may be interpolated, however it's the moveable class that is + // required for extrapolation and other prediction. + void interpolate() noexcept; +}; + +} // namespace client + +#endif diff --git a/src/client/entity/entity.cc b/src/client/entity/entity.cc new file mode 100644 index 0000000..c305a66 --- /dev/null +++ b/src/client/entity/entity.cc @@ -0,0 +1,6 @@ +#include "client/entity/entity.hh" + +namespace client { + + +} // namespace client diff --git a/src/client/entity/entity.hh b/src/client/entity/entity.hh new file mode 100644 index 0000000..e8d6cc6 --- /dev/null +++ b/src/client/entity/entity.hh @@ -0,0 +1,27 @@ +#ifndef CLIENT_ENTITY_ENTITY_HH_ +#define CLIENT_ENTITY_ENTITY_HH_ + +#include "shared/entity/entity.hh" +#include "shared/entity/player.hh" + +namespace client { + +class player; // forward declaration + +// A client::entity is a renderable shared::entity. +class entity : virtual public shared::entity { +public: + entity(shared::entity& e) noexcept + : shared::entity(std::forward<shared::entity>(e)) {} + entity(const proto::entity& e) noexcept : shared::entity(e) {} + + virtual ~entity() noexcept {} + +public: + virtual void draw(const client::player& localplayer) noexcept = 0; + virtual void draw_wts(const client::player& localplayer) noexcept = 0; +}; + +} // namespace client + +#endif diff --git a/src/client/entity/moveable.cc b/src/client/entity/moveable.cc new file mode 100644 index 0000000..480e6db --- /dev/null +++ b/src/client/entity/moveable.cc @@ -0,0 +1,86 @@ +#include "client/entity/moveable.hh" + +namespace client { + +static auto find_sequence_it(auto& updates, + const shared::tick_t& sequence) noexcept { + return std::ranges::find_if(updates, [&sequence](const auto& update) { + return update.tick_sequence == sequence; + }); +} + +void moveable::repredict_from(const shared::tick_t& sequence) noexcept { + // find the animate update we want to repredict starting from + const auto find_it = find_sequence_it(this->updates, sequence); + if (find_it == std::end(this->updates)) { + return; + } + + // backup current animate + const shared::animate before_move = *this; + + // predict the next ticks starting from find_it until we run + // out of prediction history + for (auto it = find_it; it != std::end(this->updates); ++it) { + const auto next_it = std::next(it); + if (next_it == std::end(this->updates)) { + break; + } + + const auto viewangles = next_it->animate.get_angles(); + const auto commands = next_it->animate.get_commands(); + + this->shared::animate::operator=(it->animate); + this->commands = commands; + this->viewangles = viewangles; + const auto predicted = movement::move(*this, state::chunks); + if (!predicted.has_value()) { + break; + } + next_it->animate = *predicted; + next_it->animate.get_mutable_angles() = viewangles; + next_it->animate.get_mutable_commands() = commands; + } + // restore current animate + this->shared::animate::operator=(before_move); +} + +void moveable::notify(const shared::animate& animate, + const shared::tick_t& sequence, + const bool from_server) noexcept { + this->animate::notify(animate, sequence, from_server); + + if (!from_server) { // Only repredict if we're from the server. + return; + } + this->repredict_from(sequence); +} + +void moveable::extrapolate() noexcept { + // can't extrapolate if no data + if (this->updates.empty()) { + return; + } + + const auto& latest_it = std::rbegin(this->updates); + if (latest_it == std::rend(this->updates)) { + return; + } + + // backup current animate, load latest animate + const shared::animate before_extrapolate = *this; + this->shared::animate::operator=(latest_it->animate); + + // FIXED: we were writing our commands with the old commands, so we were + // always predicting the first commands, which were always for not moving! + this->commands = before_extrapolate.get_commands(); + this->viewangles = before_extrapolate.get_angles(); + + const auto predicted = movement::move(*this, state::chunks); + if (predicted.has_value()) { + this->notify(*predicted, this->get_latest_sequence() + 1, false); + } + this->shared::animate::operator=(before_extrapolate); +} + +} // namespace client diff --git a/src/client/entity/moveable.hh b/src/client/entity/moveable.hh new file mode 100644 index 0000000..fa08d22 --- /dev/null +++ b/src/client/entity/moveable.hh @@ -0,0 +1,29 @@ +#ifndef CLIENT_ENTITY_MOVEABLE_HH_ +#define CLIENT_ENTITY_MOVEABLE_HH_ + +#include "client/entity/animate.hh" +#include "client/state/chunks.hh" +#include "client/state/state.hh" +#include "client/world/chunk.hh" +#include "shared/entity/moveable.hh" + +namespace client { + +class moveable : virtual public client::animate, + virtual public shared::moveable, + virtual public shared::animate { +private: + void repredict_from(const shared::tick_t& sequence) noexcept; + +public: + // Notify repredicts if necessary. + virtual void notify(const shared::animate& animate, + const shared::tick_t& sequence, + const bool from_server) noexcept override; + + void extrapolate() noexcept; +}; + +} // namespace client + +#endif diff --git a/src/client/entity/player.cc b/src/client/entity/player.cc new file mode 100644 index 0000000..550968f --- /dev/null +++ b/src/client/entity/player.cc @@ -0,0 +1,54 @@ +#include "player.hh" +namespace client { + +glm::vec3 player::get_world_pos(const client::player& lp) noexcept { + const auto& lp_cp = lp.get_chunk_pos(); + + // clang-format off + const float world_x = static_cast<float>(this->get_chunk_pos().x - lp_cp.x) * shared::world::chunk::WIDTH + this->local_pos.x; + const float world_y = static_cast<float>(this->get_local_pos().y) + shared::player::HEIGHT / 2.0f; + const float world_z = static_cast<float>(this->get_chunk_pos().z - lp_cp.z) * shared::world::chunk::WIDTH + this->local_pos.z; + // clang-format on + return {world_x, world_y, world_z}; +} + +void player::draw(const client::player& localplayer) noexcept { + + static render::model playermodel{"res/models/player/player.obj"}; + static render::program program{"res/shaders/model.vs", + "res/shaders/model.fs"}; + + const glm::vec3 world_pos = this->get_world_pos(localplayer); + const glm::mat4 rotate = glm::rotate(glm::mat4(1.0f), -this->viewangles.yaw, + glm::vec3(0.0f, 1.0f, 0.0f)); + const glm::mat4 translate = glm::translate(glm::mat4(1.0f), world_pos); + const glm::mat4& view = render::camera::get_view(); + const glm::mat4& proj = render::camera::get_proj(); + + playermodel.draw(program, proj * view * translate * rotate); +} + +void player::draw_wts(const client::player& localplayer) noexcept { + const auto& msg = this->get_message(); + if (!msg.has_value()) { + return; + } + + const glm::vec3 world_pos = this->get_world_pos(localplayer); + const auto pos = math::world_to_screen(world_pos); + + if (!pos.has_value()) { + return; + } + + const glm::vec2& window = render::get_window_size(); + const glm::mat4 proj = glm::ortho(0.0f, window.x, 0.0f, window.y); + const glm::mat4 tran = + glm::translate(glm::mat4(1.0f), + glm::vec3{std::floor(pos->x), std::floor(pos->y), 0.0f}); + + constexpr glm::vec4 white{1.0f, 1.0f, 1.0f, 1.0f}; + render::render_text(msg->text, 30.0f, true, true, white, proj * tran); +} + +} // namespace client diff --git a/src/client/entity/player.hh b/src/client/entity/player.hh new file mode 100644 index 0000000..d9bdc18 --- /dev/null +++ b/src/client/entity/player.hh @@ -0,0 +1,73 @@ +#ifndef CLIENT_ENTITY_PLAYER_HH_ +#define CLIENT_ENTITY_PLAYER_HH_ + +#include <chrono> +#include <optional> +#include <string> +#include <utility> + +#include "client/entity/moveable.hh" +#include "client/entity/entity.hh" +#include "client/item/block.hh" +#include "client/item/items.hh" +#include "client/math.hh" +#include "client/render/camera.hh" +#include "client/render/model.hh" +#include "client/render/program.hh" + +namespace client { + +// Renderable player, similar to client::chunk. We also store the message the +// player wants to say. +class player : virtual public shared::player, public client::moveable { +public: + static constexpr auto MSG_SHOW_TIME = std::chrono::seconds{15}; + struct message { + std::string text; + std::chrono::time_point<std::chrono::steady_clock> receive_time; + message(const std::string& message) noexcept { + this->text = message; + this->receive_time = std::chrono::steady_clock::now(); + } + }; + std::optional<message> message; + +private: + glm::vec3 get_world_pos(const client::player& lp) noexcept; + +public: + player(shared::player&& p) noexcept + : shared::entity(p), shared::animate(p), shared::player(p), + client::entity(p), client::animate(std::move(p)) { + + // upgrade our inventory contents to client::item::item + for (auto& item : this->inventory.contents) { + if (item == nullptr) { + continue; + } + + item = client::item::make_item(item->get_type(), item->quantity); + } + } + player(const proto::animate& proto) noexcept + : shared::entity(proto.entity()), shared::animate(proto), + shared::player(proto), client::entity(proto.entity()), + client::animate(proto) {} + + virtual void draw(const client::player& localplayer) noexcept override; + virtual void draw_wts(const client::player& localplayer) noexcept override; + + const std::optional<struct message>& get_message() noexcept { + if (this->message.has_value()) { + const auto now = std::chrono::steady_clock::now(); + if (now > this->message->receive_time + player::MSG_SHOW_TIME) { + this->message.reset(); + } + } + return this->message; + } +}; + +} // namespace client + +#endif diff --git a/src/client/item/block.cc b/src/client/item/block.cc new file mode 100644 index 0000000..08f072b --- /dev/null +++ b/src/client/item/block.cc @@ -0,0 +1,91 @@ +#include "client/item/block.hh" + +namespace client { +namespace item { + +void block::regenerate_glo() noexcept { + const auto make_vbo = [](const auto& data) -> GLuint { + GLuint vbo = 0; + glGenBuffers(1, &vbo); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, + std::size(data) * sizeof(client::world::block::glvert), + std::data(data), GL_STATIC_DRAW); + return vbo; + }; + const auto make_vao = []() -> GLuint { + GLuint vao = 0; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + // posiiton + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, sizeof(glm::vec3) / sizeof(float), GL_FLOAT, + GL_FALSE, sizeof(client::world::block::glvert), + nullptr); + // texture + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, sizeof(glm::vec3) / sizeof(float), GL_FLOAT, + GL_FALSE, sizeof(client::world::block::glvert), + reinterpret_cast<void*>(sizeof(glm::vec3))); + return vao; + }; + std::vector<client::world::block::glvert> data; + + const float tex_yoff = static_cast<float>(this->type) - 1.0f; + for (const auto& face : + client::world::block::get_glfaces(this->shared::item::block::type)) { + + std::ranges::transform(face, std::back_inserter(data), [&](auto vert) { + vert.texture.z += tex_yoff * 6.0f; + return vert; + }); + } + + const auto vbo = make_vbo(data); + const auto vao = make_vao(); + this->glo.emplace(vbo, vao, std::size(data)); +} + +void block::draw(const glm::vec2& pos, const glm::vec2& size) noexcept { + const auto make_matrix = [&]() -> glm::mat4 { + const glm::vec2 window = client::render::get_window_size(); + + constexpr auto identity = glm::mat4{1.0f}; + const auto proj = glm::ortho(0.0f, window.x, 0.0f, window.y); + const auto scal = + glm::scale(identity, glm::vec3{size.x * 0.5f, size.y * 0.5f, 0.0f}); + const auto tran = + glm::translate(identity, glm::vec3{pos.x + size.x / 2.0f, + pos.y + size.y / 2.0f, 0.0f}); + + // hack for shurbs, which are already offset and look bad when rotated + const auto rotey = client::world::block::get_draw_type(this->type) == + client::world::block::draw_type::block + ? glm::rotate(identity, glm::radians(45.0f), + glm::vec3(0.0f, 1.0f, 0.0f)) + : identity; + const auto rotex = glm::rotate(identity, glm::radians(30.0f), + glm::vec3(1.0f, 0.0f, 0.0f)); + return proj * tran * scal * rotex * rotey; + }; + + static const client::render::program program{"res/shaders/face.vs", + "res/shaders/face.fs"}; + static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix"); + + if (!this->glo.has_value()) { + this->regenerate_glo(); + } + + glDisable(GL_BLEND); + glDisable(GL_DEPTH_TEST); + glUseProgram(program); + glBindVertexArray(this->glo->vao); + + glUniformMatrix4fv(u_matrix, 1, GL_FALSE, glm::value_ptr(make_matrix())); + + glDrawArrays(GL_TRIANGLES, 0, this->glo->elements); +} + +} // namespace item +} // namespace client diff --git a/src/client/item/block.hh b/src/client/item/block.hh new file mode 100644 index 0000000..f259aad --- /dev/null +++ b/src/client/item/block.hh @@ -0,0 +1,58 @@ +#ifndef CLIENT_ITEM_BLOCK_HH_ +#define CLIENT_ITEM_BLOCK_HH_ + +#include <algorithm> +#include <optional> + +#include "client/item/item.hh" +#include "client/render/render.hh" +#include "client/world/block.hh" +#include "shared/item/block.hh" +#include "shared/item/item.hh" + +namespace client { +namespace item { + +class block : virtual public shared::item::block, public client::item::item { +private: + struct gl_object { + GLuint vbo; + GLuint vao; + unsigned long elements; + + gl_object(const GLuint& vbo, const GLuint& vao, + const unsigned long& elements) noexcept + : vbo(vbo), vao(vao), elements(elements) {} + gl_object(const gl_object&) = delete; + gl_object(gl_object&&) = delete; + ~gl_object() noexcept { + glDeleteBuffers(1, &vbo); + glDeleteVertexArrays(1, &vao); + } + }; + +private: + std::optional<gl_object> glo = std::nullopt; + void regenerate_glo() noexcept; + +public: + template <typename... Args> + block(const enum shared::world::block::type& type, Args&&... args) noexcept + : shared::item::block(type, args...), client::item::item( + std::forward<Args>(args)...) { + } + template <typename... Args> + block(const shared::item::item::type_t& type, Args&&... args) noexcept + : shared::item::item(type, args...), shared::item::block(type, args...), + client::item::item(type, std::forward<Args>(args)...) {} + virtual ~block() noexcept {} + +public: + virtual void draw(const glm::vec2& pos, + const glm::vec2& size) noexcept override; +}; + +} // namespace item +} // namespace client + +#endif diff --git a/src/client/item/item.cc b/src/client/item/item.cc new file mode 100644 index 0000000..64cc1f5 --- /dev/null +++ b/src/client/item/item.cc @@ -0,0 +1 @@ +#include "client/item/item.hh" diff --git a/src/client/item/item.hh b/src/client/item/item.hh new file mode 100644 index 0000000..ff80369 --- /dev/null +++ b/src/client/item/item.hh @@ -0,0 +1,30 @@ +#ifndef CLIENT_ITEM_ITEM_HH_ +#define CLIENT_ITEM_ITEM_HH_ + +#include <algorithm> + +#include <glm/glm.hpp> + +#include "shared/item/item.hh" + +namespace client { +namespace item { + +// client::item is a renderiable shared::item +class item : virtual public shared::item::item { +public: +public: + template <typename... Args> + item(Args&&... args) noexcept + : shared::item::item(std::forward<Args>(args)...) {} + virtual ~item() noexcept {} + +public: + // 2d drawing of items + virtual void draw(const glm::vec2& pos, const glm::vec2& size) noexcept = 0; +}; + +} // namespace item +} // namespace client + +#endif diff --git a/src/client/item/items.cc b/src/client/item/items.cc new file mode 100644 index 0000000..9ff25fe --- /dev/null +++ b/src/client/item/items.cc @@ -0,0 +1,16 @@ +#include "client/item/items.hh" + +namespace client { +namespace item { + +shared::item::item_t make_item(const shared::item::item::type_t& type, + const std::uint32_t& quantity) noexcept { + if (type >= shared::item::block::type_offset) { + return std::make_shared<client::item::block>(type, quantity); + } + // TODO non-block items + return std::make_shared<client::item::block>(type, quantity); +} + +} // namespace item +} // namespace client diff --git a/src/client/item/items.hh b/src/client/item/items.hh new file mode 100644 index 0000000..53cebfa --- /dev/null +++ b/src/client/item/items.hh @@ -0,0 +1,17 @@ +#ifndef CLIENT_ITEM_ITEMS_HH_ +#define CLIENT_ITEM_ITEMS_HH_ + +#include "client/item/block.hh" +#include "shared/item/item.hh" +#include "shared/item/items.hh" + +namespace client { +namespace item { + +shared::item::item_t make_item(const shared::item::item::type_t& type, + const std::uint32_t& quantity) noexcept; + +} // namespace item +} // namespace client + +#endif diff --git a/src/client/main.cc b/src/client/main.cc new file mode 100644 index 0000000..858c6df --- /dev/null +++ b/src/client/main.cc @@ -0,0 +1,90 @@ +#include "client/main.hh" + +static bool& get_has_address() noexcept { + static bool ret = false; + return ret; +} + +static bool& get_has_port() noexcept { + static bool ret = false; + return ret; +} + +static std::string& get_address() noexcept { + static std::string address{shared::DEFAULT_ADDRESS}; + return address; +} + +static std::string& get_port() noexcept { + static std::string port{shared::DEFAULT_PORT}; + return port; +} + +static bool& get_is_headless() noexcept { + static bool is_headless = false; + return is_headless; +} + +static bool parse_arg(const int& c, const char* const arg) { + switch (c) { + case 'a': + get_address() = boost::lexical_cast<std::string>(arg); + get_has_address() = true; + break; + case 'p': + get_port() = boost::lexical_cast<std::string>(arg); + get_has_port() = true; + break; + case 'H': + get_is_headless() = true; + break; + case 's': + get_address() = "localhost"; + break; + default: + return false; + } + return true; +} + +static const shared::args_t& get_options() { + static shared::args_t ret = []() -> shared::args_t { + shared::args_t ret{ + {.name = "address", .desc = "connect elsewhere", .val = "a:"}, + {.name = "port", .desc = "override the default port", .val = "p:"}, + {.name = "headless", .desc = "run without a client ", .val = "H"}, + {.name = "singleplayer", + .desc = "avoid hosting a lan server", + .val = "s"}}; + std::ranges::copy(shared::get_options(), std::back_inserter(ret)); + std::ranges::copy(server::get_options(), std::back_inserter(ret)); + return ret; + }(); + + return ret; +} + +int main(const int argc, char* const* argv) { + shared::init(); + + shared::parse_args(argc, argv, get_options(), + {&parse_arg, &shared::parse_arg, &server::parse_arg}); + + if (get_has_address()) { + shared::try_main(client::main, get_address(), get_port()); + return EXIT_SUCCESS; + } + + if (get_is_headless()) { + shared::try_main(server::main, get_address(), get_port()); + return EXIT_SUCCESS; + } + + const std::jthread server_thread{shared::try_main, server::main, + get_address(), get_port()}; + while (!server::has_initialised) { + continue; + } + shared::try_main(client::main, get_address(), get_port()); + return EXIT_SUCCESS; +} diff --git a/src/client/main.hh b/src/client/main.hh new file mode 100644 index 0000000..8610763 --- /dev/null +++ b/src/client/main.hh @@ -0,0 +1,23 @@ +#ifndef CLIENT_MAIN_HH_ +#define CLIENT_MAIN_HH_ + +#include <algorithm> +#include <exception> +#include <execinfo.h> +#include <getopt.h> +#include <iostream> +#include <iterator> +#include <signal.h> +#include <string_view> +#include <thread> +#include <vector> + +#include <boost/lexical_cast.hpp> + +#include "client/client.hh" +#include "server/server.hh" +#include "server/shared.hh" +#include "shared/init.hh" +#include "shared/shared.hh" + +#endif diff --git a/src/client/movement.hh b/src/client/movement.hh deleted file mode 100644 index 82b56b1..0000000 --- a/src/client/movement.hh +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef CLIENT_MOVEMENT_HH_ -#define CLIENT_MOVEMENT_HH_ - -#include "client/world.hh" -#include "shared/movement.hh" -#include "shared/net/net.hh" -#include "shared/player.hh" - -namespace client { -namespace movement { - -void move(shared::player& player, client::world::chunk::map& chunks, - const float deltatime) noexcept; - -enum class interact_mode { add, remove }; -std::optional<std::pair<shared::math::coords, glm::ivec3>> -interact(const shared::player& player, const interact_mode& mode, - const client::world::chunk::map& chunks) noexcept; - -} // namespace movement -} // namespace client - -#endif diff --git a/src/client/movement.cc b/src/client/movement/movement.cc index e5898eb..8712f89 100644 --- a/src/client/movement.cc +++ b/src/client/movement/movement.cc @@ -1,11 +1,11 @@ -#include "client/movement.hh" +#include "client/movement/movement.hh" namespace client { namespace movement { static std::optional<shared::world::block> maybe_get_block(const shared::math::coords& pos, - const client::world::chunk::map& chunks, + const client::world::chunks_t& chunks, const glm::ivec3& block_pos) noexcept { const auto find_it = chunks.find(pos); @@ -20,19 +20,20 @@ maybe_get_block(const shared::math::coords& pos, return find_it->second->get_block(block_pos); } -static std::optional<std::vector<shared::movement::blockinfo>> -make_blockinfos(const shared::player& player, - const client::world::chunk::map& chunks, const int width, - const int height) noexcept { +static std::optional<shared::movement::blocks> +maybe_make_blocks(const glm::vec3& local_pos, + const shared::math::coords& chunk_pos, + const client::world::chunks_t& chunks, const int width, + const int height) noexcept { - std::vector<shared::movement::blockinfo> blockinfos; + shared::movement::blocks blocks; for (int x = -width; x <= width; ++x) { for (int y = -height; y <= height; ++y) { for (int z = -width; z <= width; ++z) { const glm::ivec3 rel_pos = - glm::ivec3{x, y, z} + glm::ivec3{player.local_pos}; + glm::ivec3{x, y, z} + glm::ivec3{local_pos}; if (rel_pos.y < 0 || rel_pos.y >= shared::world::chunk::HEIGHT) { @@ -41,7 +42,7 @@ make_blockinfos(const shared::player& player, const shared::math::coords norm_chunk_pos = shared::world::chunk::get_normalised_chunk( - player.chunk_pos, rel_pos.x, rel_pos.z); + chunk_pos, rel_pos.x, rel_pos.z); const glm::ivec3 norm_pos = shared::world::chunk::get_normalised_coords(rel_pos); @@ -52,72 +53,68 @@ make_blockinfos(const shared::player& player, return std::nullopt; } - const glm::vec3 pos = glm::vec3{rel_pos} - player.local_pos; + const glm::vec3 pos = glm::vec3{rel_pos} - local_pos; const shared::movement::aabb aabb = { .min = glm::vec3{0.0f, 0.0f, 0.0f} + pos, .max = glm::vec3{1.0f, 1.0f, 1.0f} + pos}; - blockinfos.push_back( - shared::movement::blockinfo{.block = block.value(), - .aabb = aabb, - .chunk_pos = norm_chunk_pos, - .pos = norm_pos}); + blocks.push_back( + shared::movement::block{.block = *block, + .aabb = aabb, + .chunk_pos = norm_chunk_pos, + .pos = norm_pos}); } } } - return blockinfos; + return blocks; } -void move(shared::player& player, client::world::chunk::map& chunks, - const float deltatime) noexcept { - const auto blockinfos = - make_blockinfos(player, chunks, shared::movement::move_width, - shared::movement::move_height); - if (!blockinfos.has_value()) { - return; +std::optional<shared::animate> move(const shared::moveable& moveable, + const world::chunks_t& chunks) noexcept { + + const auto xy = shared::movement::get_move_xy(state::tickrate, moveable); + const auto blocks = maybe_make_blocks( + moveable.get_local_pos(), moveable.get_chunk_pos(), chunks, xy.x, xy.y); + if (!blocks.has_value()) { + return std::nullopt; } - shared::movement::move(player, blockinfos.value(), deltatime); + return shared::movement::move(moveable, *blocks, state::tickrate); } std::optional<std::pair<shared::math::coords, glm::ivec3>> -interact(const shared::player& player, const interact_mode& mode, - const client::world::chunk::map& chunks) noexcept { - const auto blockinfos = make_blockinfos(player, chunks, 5, 5); - if (!blockinfos.has_value()) { +interact(const shared::moveable& moveable, const interact_mode& mode, + const client::world::chunks_t& chunks) noexcept { + const auto blocks = maybe_make_blocks( + moveable.get_local_pos(), moveable.get_chunk_pos(), chunks, 5, 5); + if (!blocks.has_value()) { return std::nullopt; } constexpr float epsilon = 0.001f; - constexpr shared::movement::aabb player_aabb = { - .min = {-shared::player::HALFWIDTH + epsilon, 0.0f + epsilon, - -shared::player::HALFWIDTH + epsilon}, - .max = {shared::player::HALFWIDTH - epsilon, - shared::player::HEIGHT - epsilon, - shared::player::HALFWIDTH - epsilon}}; + const auto& aabb = moveable.get_aabb(); constexpr shared::movement::aabb ray_aabb = { .min = {-epsilon, shared::player::EYE_HEIGHT - epsilon, -epsilon}, .max = {epsilon, shared::player::EYE_HEIGHT + epsilon, epsilon}}; const shared::movement::moving_aabb ray_moving_aabb{ - .aabb = ray_aabb, - .velocity = shared::math::angle_to_dir(player.viewangles)}; + .aabb = ray_aabb, .velocity = moveable.get_angles().to_dir()}; - std::optional<std::pair<shared::movement::moving_aabb_ret, - shared::movement::blockinfo>> + std::optional< + std::pair<shared::movement::moving_aabb_ret, shared::movement::block>> best; - for (const auto& blockinfo : *blockinfos) { - if (blockinfo.block == shared::world::block::type::air) { + for (const auto& block : *blocks) { + if (block.block == shared::world::block::type::air) { continue; } - if (!shared::world::block::is_removable(blockinfo.block)) { + if (!shared::world::block::is_removable(block.block)) { continue; } const auto intersect = shared::movement::intersect_moving_aabbs( ray_moving_aabb, - shared::movement::moving_aabb{.aabb = blockinfo.aabb, + shared::movement::moving_aabb{.aabb = block.aabb, .velocity = {0.0f, 0.0f, 0.0f}}); constexpr float MAX_REACH = 3.8f; @@ -128,18 +125,18 @@ interact(const shared::player& player, const interact_mode& mode, continue; } - best = std::make_pair(*intersect, blockinfo); + best = std::make_pair(*intersect, block); } if (!best.has_value()) { return std::nullopt; } - const auto& [intersect, blockinfo] = *best; + const auto& [intersect, block] = *best; - const glm::ivec3 position = blockinfo.pos; + const glm::ivec3 position = block.pos; if (mode == interact_mode::remove) { - return std::make_pair(blockinfo.chunk_pos, position); + return std::make_pair(block.chunk_pos, position); } const glm::ivec3 normal_i = -intersect.normal; @@ -147,15 +144,15 @@ interact(const shared::player& player, const interact_mode& mode, shared::world::chunk::get_normalised_coords(normal_i + position); const auto find_it = - std::find_if(std::begin(*blockinfos), std::end(*blockinfos), + std::find_if(std::begin(*blocks), std::end(*blocks), [&](const auto b) { return b.pos == normalised_pos; }); - if (find_it == std::end(*blockinfos)) { + if (find_it == std::end(*blocks)) { return std::nullopt; } if (!shared::world::block::is_replaceable(find_it->block)) { return std::nullopt; } - if (shared::movement::intersect_aabbs(player_aabb, find_it->aabb)) { + if (shared::movement::intersect_aabbs(aabb, find_it->aabb)) { return std::nullopt; } return std::make_pair(find_it->chunk_pos, normalised_pos); diff --git a/src/client/movement/movement.hh b/src/client/movement/movement.hh new file mode 100644 index 0000000..19144ba --- /dev/null +++ b/src/client/movement/movement.hh @@ -0,0 +1,27 @@ +#ifndef CLIENT_MOVEMENT_MOVEMENT_HH_ +#define CLIENT_MOVEMENT_MOVEMENT_HH_ + +#include <cstdint> +#include <optional> + +#include "client/state/state.hh" +#include "client/world/chunk.hh" +#include "shared/entity/moveable.hh" +#include "shared/movement/movement.hh" +#include "shared/net/net.hh" + +namespace client { +namespace movement { + +std::optional<shared::animate> move(const shared::moveable& moveable, + const world::chunks_t& chunks) noexcept; + +enum class interact_mode { add, remove }; +std::optional<std::pair<shared::math::coords, glm::ivec3>> +interact(const shared::moveable& player, const interact_mode& mode, + const world::chunks_t& chunks) noexcept; + +} // namespace movement +} // namespace client + +#endif diff --git a/src/client/player.cc b/src/client/player.cc deleted file mode 100644 index 5ff4118..0000000 --- a/src/client/player.cc +++ /dev/null @@ -1,58 +0,0 @@ -#include "player.hh" - -glm::vec3 client::player::get_world_pos(const shared::player& lp) noexcept { - // clang-format off - const float world_x = static_cast<float>(this->chunk_pos.x - lp.chunk_pos.x) * shared::world::chunk::WIDTH + this->local_pos.x; - const float world_y = static_cast<float>(this->local_pos.y) + shared::player::HEIGHT / 2.0f; - const float world_z = static_cast<float>(this->chunk_pos.z - lp.chunk_pos.z) * shared::world::chunk::WIDTH + this->local_pos.z; - // clang-format on - return {world_x, world_y, world_z}; -} - -void client::player::draw_playermodel( - const shared::player& localplayer) noexcept { - - static client::render::model playermodel{"res/models/player/player.obj"}; - static client::render::program program{"res/shaders/model.vs", - "res/shaders/model.fs"}; - - const glm::vec3 world_pos = this->get_world_pos(localplayer); - const glm::mat4 rotate = glm::rotate(glm::mat4(1.0f), -this->viewangles.yaw, - glm::vec3(0.0f, 1.0f, 0.0f)); - const glm::mat4 translate = glm::translate(glm::mat4(1.0f), world_pos); - const glm::mat4& view = client::render::camera::get_view(); - const glm::mat4& proj = client::render::camera::get_proj(); - - playermodel.draw(program, proj * view * translate * rotate); -} - -void client::player::draw_message(const shared::player& localplayer) noexcept { - // Clear the message if it's too old. - if (this->message.has_value()) { - const auto now = std::chrono::steady_clock::now(); - if (now > this->message->receive_time + player::MSG_SHOW_TIME) { - this->message.reset(); - } - } - - if (!this->message.has_value()) { - return; - } - - const glm::vec3 world_pos = this->get_world_pos(localplayer); - const auto pos = client::math::world_to_screen(world_pos); - - if (!pos.has_value()) { - return; - } - - const glm::vec2& window = client::render::get_window_size(); - const glm::mat4 proj = glm::ortho(0.0f, window.x, 0.0f, window.y); - const glm::mat4 tran = - glm::translate(glm::mat4(1.0f), - glm::vec3{std::floor(pos->x), std::floor(pos->y), 0.0f}); - - constexpr glm::vec4 white{1.0f, 1.0f, 1.0f, 1.0f}; - client::render::render_text(this->message->text, 30.0f, true, true, white, - proj * tran); -} diff --git a/src/client/player.hh b/src/client/player.hh deleted file mode 100644 index c41ba23..0000000 --- a/src/client/player.hh +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef CLIENT_PLAYER_HH_ -#define CLIENT_PLAYER_HH_ - -#include <chrono> -#include <optional> -#include <string> -#include <utility> - -#include "client/math.hh" -#include "client/render/camera.hh" -#include "client/render/model.hh" -#include "client/render/program.hh" -#include "shared/player.hh" - -namespace client { - -// Renderable player, similar to client::chunk. We also store the message the -// player wants to say. -class player : public shared::player { -public: - static constexpr auto MSG_SHOW_TIME = std::chrono::seconds(15); - struct message { - std::string text; - std::chrono::time_point<std::chrono::steady_clock> receive_time; - message(const std::string& message) noexcept { - this->text = message; - this->receive_time = std::chrono::steady_clock::now(); - } - }; - std::optional<message> message; - -private: - glm::vec3 get_world_pos(const shared::player& lp) noexcept; - -public: - player(shared::player&& p) noexcept - : shared::player(std::forward<shared::player>(p)) {} - player(const shared::player& p) noexcept : shared::player(p) {} - - void update(const shared::player& p) noexcept { - this->index = p.index; // there is no syntax for doing this in one line - this->commands = p.commands; - this->chunk_pos = p.chunk_pos; - this->local_pos = p.local_pos; - this->viewangles = p.viewangles; - this->velocity = p.velocity; - } - - void draw_playermodel(const shared::player& localplayer) noexcept; - void draw_message(const shared::player& localplayer) noexcept; - - const std::optional<struct message>& get_message() const noexcept { - return this->message; - } -}; - -using players = std::vector<player>; -static inline client::player& get_localplayer(players& players) noexcept { - return players[0]; -} - -} // namespace client - -#endif diff --git a/src/client/draw.cc b/src/client/render/draw.cc index c549ffe..d9ea9c3 100644 --- a/src/client/draw.cc +++ b/src/client/render/draw.cc @@ -1,53 +1,7 @@ -#include "draw.hh" +#include "client/render/draw.hh" namespace client { -namespace draw { - -void draw_rectangle(const rectangle_args& ra) noexcept { - const glm::vec2 pos = ra.pos.to_vec2(); - const glm::vec2 size = ra.size.to_vec2(); - - // We want to render from the bottom left corner. - render::render_rectangle(pos + (size / 2.0f), size, ra.colour); -} - -void draw_colour(const glm::vec4& colour) noexcept { - const rectangle_args args = { - .pos = {.extent = {0.0f, 0.0f}}, - .size = {.extent = {1.0f, 1.0f}}, - .colour = colour, - }; - draw_rectangle(args); -} - -void draw_text(const std::string_view text, const text_args& args) noexcept { - const glm::vec2& window = render::get_window_size(); - const float x = floorf(window.x * args.pos.extent.x + args.pos.offset.x); - const float y = floorf(window.y * args.pos.extent.y + args.pos.offset.y); - const unsigned height = static_cast<unsigned>( - args.extent_height * window.y + args.offset_height); - - const auto make_matrix = [&](const float xo, const float yo) -> glm::mat4 { - constexpr auto identity = glm::mat4(1.0f); - const auto proj = glm::ortho(0.f, window.x, 0.f, window.y); - const auto tran = - glm::translate(identity, glm::vec3{x + xo, y + yo, 0.0f}); - return proj * tran; - }; - - glDisable(GL_DEPTH_TEST); - glDisable(GL_BLEND); - if (args.has_backing) { - constexpr glm::vec4 black{0.0f, 0.0f, 0.0f, 1.0f}; - render::render_text(text, height, args.is_centered, args.is_vcentered, - black, make_matrix(1.0f, -2.0f)); - } - glEnable(GL_BLEND); - - render::render_text(text, height, args.is_centered, args.is_vcentered, - args.colour, make_matrix(0, 0)); - glEnable(GL_DEPTH_TEST); -} +namespace render { static void draw_state_info(const shared::player& lp) noexcept { @@ -58,14 +12,16 @@ static void draw_state_info(const shared::player& lp) noexcept { }; const auto make_position = [&]() -> std::string { - const auto chunk_x = lp.chunk_pos.x; - const auto chunk_z = lp.chunk_pos.z; - const auto x = static_cast<double>(lp.local_pos.x); - const auto y = static_cast<double>(lp.local_pos.y); - const auto z = static_cast<double>(lp.local_pos.z); + const auto chunk_x = lp.get_chunk_pos().x; + const auto chunk_z = lp.get_chunk_pos().z; + const auto x = static_cast<double>(lp.get_local_pos().x); + const auto y = static_cast<double>(lp.get_local_pos().y); + const auto z = static_cast<double>(lp.get_local_pos().z); constexpr auto WIDTH = shared::world::chunk::WIDTH; - const auto abs_x = static_cast<double>(lp.chunk_pos.x) * WIDTH + x; - const auto abs_z = static_cast<double>(lp.chunk_pos.z) * WIDTH + z; + const auto abs_x = + static_cast<double>(lp.get_chunk_pos().x) * WIDTH + x; + const auto abs_z = + static_cast<double>(lp.get_chunk_pos().z) * WIDTH + z; constexpr auto nstr = [](const double d) -> std::string { auto str = std::to_string(d); @@ -80,16 +36,16 @@ static void draw_state_info(const shared::player& lp) noexcept { }; const auto make_direction = [&]() -> std::string { - const float yaw = glm::degrees(lp.viewangles.yaw); - const float pitch = glm::degrees(lp.viewangles.pitch); + const float yaw = glm::degrees(lp.get_angles().yaw); + const float pitch = glm::degrees(lp.get_angles().pitch); const std::string bearing = [&]() -> std::string { - if (yaw >= 315.0f || yaw < 45.0f) { - return "North"; - } else if (yaw >= 45.0f && yaw < 135.0f) { - return "East"; - } else if (yaw >= 135.0f && yaw < 225.0f) { + if (yaw > 135.0f || yaw < -135.0f) { return "South"; + } else if (yaw > 45.0f) { + return "East"; + } else if (yaw > -45.0f) { + return "North"; } else { return "West"; } @@ -100,34 +56,65 @@ static void draw_state_info(const shared::player& lp) noexcept { }; const auto make_address = []() -> std::string { - const std::string address = std::string(client::state.address); - const std::string port = std::string(client::state.port); + const std::string address = std::string(client::state::address); + const std::string port = std::string(client::state::port); return "address: " + address + ":" + port; }; + const auto make_tickrate = [&]() -> std::string { + return "tickrate: " + std::to_string(state::tickrate); + }; + const auto make_seed = []() -> std::string { - return "seed: " + std::to_string(client::state.seed); + return "seed: " + std::to_string(client::state::seed); }; - const auto make_players = []() -> std::string { - const auto index = std::to_string(client::state.localplayer); + const auto make_entities = []() -> std::string { + const auto index = std::to_string(*client::state::localplayer_index); return "index: " + index; }; const auto make_chunks = []() -> std::string { - return "chunks [req, rcv]: " + - std::to_string(client::state.requested_chunk_count) + ", " + - std::to_string(client::state.networked_chunk_count) + " @ " + - std::to_string(client::state.draw_distance) + " draw distance"; + return "chunks: " + + std::to_string(client::state::requested_chunk_count) + ", " + + std::to_string(client::state::networked_chunk_count) + " @ " + + std::to_string(client::state::draw_distance) + " draw distance"; }; const auto make_speed = [&]() -> std::string { - return "speed: " + nstr(glm::length(lp.velocity)) + " b/s"; + return "speed: " + nstr(glm::length(lp.get_velocity())) + " b/s"; }; const auto make_velocity = [&]() -> std::string { - return "velocity: (" + nstr(lp.velocity.x) + ", " + - nstr(lp.velocity.y) + ", " + nstr(lp.velocity.z) + ")"; + return "velocity: (" + nstr(lp.get_velocity().x) + ", " + + nstr(lp.get_velocity().y) + ", " + nstr(lp.get_velocity().z) + + ")"; + }; + + const auto make_msg = [&]() -> std::string { + const auto& message = get_localplayer().get_message(); + return "message: " + (message.has_value() ? message->text : ""); + }; + + const auto make_biome = [&]() -> std::string { + const int x = static_cast<int>(lp.get_local_pos().x); + const int z = static_cast<int>(lp.get_local_pos().z); + const auto is_inside = [](const int val) { + return std::clamp(val, 0, shared::world::chunk::WIDTH) == val; + }; + if (!is_inside(x) || !is_inside(z)) { + return "?"; + } + + const auto find_it = client::state::chunks.find(lp.get_chunk_pos()); + if (find_it == std::end(client::state::chunks)) { + return "?"; + } + const auto& chunk = find_it->second; + if (!chunk.has_value()) { + return "?"; + } + return std::string{"biome: "} + chunk->get_biome(x, z); }; // Draws all of our strings and works its way down the top of the screen. @@ -139,15 +126,18 @@ static void draw_state_info(const shared::player& lp) noexcept { draw_text(str, args); args.pos.extent.y -= 0.02f; }; - draw_str_trail("blockgame_linux v0.20"); + draw_str_trail("blockgame_linux v0.22"); draw_str_trail(make_address()); + draw_str_trail(make_tickrate()); draw_str_trail(make_seed()); - draw_str_trail(make_players()); + draw_str_trail(make_entities()); draw_str_trail(make_chunks()); draw_str_trail(make_position()); draw_str_trail(make_direction()); draw_str_trail(make_speed()); draw_str_trail(make_velocity()); + draw_str_trail(make_biome()); + draw_str_trail(make_msg()); } static void draw_fps() noexcept { @@ -180,26 +170,38 @@ static void draw_overlay(const shared::player& lp) noexcept { draw_fps(); } -static void draw_main_pass(const shared::player& localplayer, - client::players& players, - client::world::chunk::map& chunks) noexcept { - for (auto& chunk : chunks) { - if (!chunk.second.has_value()) { - continue; +static void draw_main_pass(const client::player& localplayer, + client::entities_t& entities, + client::world::chunks_t& chunks) noexcept { + + { + // zero means no limit + const unsigned chunks_per_frame = + client::settings::get<unsigned>({"video", "chunks_per_frame"}, 0u); + unsigned chunks_regenerated = 0u; + for (auto& chunk : chunks) { + if (!chunk.second.has_value()) { + continue; + } + + const bool skip = chunks_per_frame == 0 + ? false + : chunks_regenerated >= chunks_per_frame; + chunks_regenerated += chunk.second->draw( + chunks, localplayer, world::chunk::pass::solid, skip); } - chunk.second->draw(chunks, localplayer, - client::world::chunk::pass::solid); } - // Draw players while ignoring the localplayer, which is always 0'th! - // This also handles drawing messages. - for (auto i = 1u; i < std::size(players); ++i) { - players[i].draw_playermodel(localplayer); + for (const auto& [index, entity_ptr] : entities) { + if (index == localplayer.get_index()) { + continue; + } + entity_ptr->draw(localplayer); } } -static void draw_water_pass(const shared::player& localplayer, - client::world::chunk::map& chunks) noexcept { +static void draw_water_pass(const client::player& localplayer, + client::world::chunks_t& chunks) noexcept { for (auto& chunk : chunks) { if (!chunk.second.has_value()) { continue; @@ -209,37 +211,33 @@ static void draw_water_pass(const shared::player& localplayer, } } -static void draw_wts_text(const shared::player& localplayer, - client::players& players) noexcept { - for (auto i = 1u; i < std::size(players); ++i) { - players[i].draw_message(localplayer); +static void draw_wts_text(const client::player& localplayer, + client::entities_t& entities) noexcept { + for (const auto& [index, entity_ptr] : entities) { + if (index == localplayer.get_index()) { + continue; + } + entity_ptr->draw_wts(localplayer); } } static void draw_hud(const client::player& localplayer, - const client::world::chunk::map& chunks) noexcept { - if (client::input::is_key_toggled(SDLK_F3)) { + const client::world::chunks_t& chunks) noexcept { + const auto should_draw_hud = !client::input::is_key_toggled(SDLK_F1); + if (client::input::is_key_toggled(SDLK_F3) && should_draw_hud) { draw_overlay(localplayer); } - if (const auto& msg = localplayer.get_message(); msg.has_value()) { - if (msg->receive_time + player::MSG_SHOW_TIME >= - std::chrono::steady_clock::now()) { - draw_text(msg->text, {.pos = relative_arg{.extent = {0.5f, 0.0f}, - .offset = {0.0f, 2.0f}}, - .extent_height = 0.0165f, - .colour = {1.0f, 1.0f, 1.0f, 1.0f}, - .has_backing = true, - .is_centered = true}); - } + if (client::window::is_open()) { + return; } - if (client::window::is_open()) { + if (!should_draw_hud) { return; } // crosshair - client::draw::draw_rectangle( + client::render::draw_rectangle( {.pos = {.extent = {0.5f, 0.5f}, .offset = {-3.0f, -3.0f}}, .size = {.offset = {6.0f, 6.0f}}, .colour = {1.0f, 1.0f, 1.0f, 1.0f}}); @@ -250,10 +248,10 @@ static void draw_hud(const client::player& localplayer, interact.has_value()) { const auto [world_x, world_z] = [&]() -> std::pair<float, float> { - const float offset_x = - static_cast<float>(interact->first.x - localplayer.chunk_pos.x); - const float offset_z = - static_cast<float>(interact->first.z - localplayer.chunk_pos.z); + const float offset_x = static_cast<float>( + interact->first.x - localplayer.get_chunk_pos().x); + const float offset_z = static_cast<float>( + interact->first.z - localplayer.get_chunk_pos().z); return {offset_x * shared::world::chunk::WIDTH, offset_z * shared::world::chunk::WIDTH}; }(); @@ -275,20 +273,19 @@ static void update_camera(const shared::player& localplayer) noexcept { // bit of maths for calculating the max distance we could can see client::render::camera::get_zfar() = std::hypot( static_cast<float>(world::chunk::HEIGHT), - float(client::state.draw_distance) * std::hypot(16.0f, 16.0f)); + float(client::state::draw_distance) * std::hypot(16.0f, 16.0f)); client::render::camera::get_pos() = - localplayer.local_pos + + localplayer.get_local_pos() + glm::vec3{0.0f, shared::player::EYE_HEIGHT, 0.0f}; - client::render::camera::get_front() = - shared::math::angle_to_dir(localplayer.viewangles); + client::render::camera::get_front() = localplayer.get_angles().to_dir(); client::render::camera::update(); } // We create framebuffer here to enable postprocessing and solve some // rendering problems. -static void draw_buffers(const shared::player& localplayer, - client::players& players, - client::world::chunk::map& chunks) noexcept { +static void draw_buffers(const client::player& localplayer, + client::entities_t& entities, + client::world::chunks_t& chunks) noexcept { const auto make_vbo = [](const auto& vertices) -> GLuint { GLuint vbo = 0; glGenBuffers(1, &vbo); @@ -367,7 +364,7 @@ static void draw_buffers(const shared::player& localplayer, // We render our main scene on the main framebuffer. glBindFramebuffer(GL_FRAMEBUFFER, fbo_main); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - draw_main_pass(localplayer, players, chunks); + draw_main_pass(localplayer, entities, chunks); // We render our water scene on the water framebuffer. glBindFramebuffer(GL_FRAMEBUFFER, fbo_water); @@ -375,7 +372,7 @@ static void draw_buffers(const shared::player& localplayer, const bool is_underwater = [&]() -> bool { const glm::ivec3 pos{client::render::camera::get_pos()}; - const auto find_it = chunks.find(localplayer.chunk_pos); + const auto find_it = chunks.find(localplayer.get_chunk_pos()); if (find_it == std::end(chunks) || !find_it->second.has_value()) { return false; } @@ -412,21 +409,20 @@ static void draw_buffers(const shared::player& localplayer, glEnable(GL_DEPTH_TEST); } -void draw(client::players& players, - client::world::chunk::map& chunks) noexcept { - const auto& localplayer = client::get_localplayer(players); +void draw(entities_t& entities, world::chunks_t& chunks) noexcept { + const auto& localplayer = get_localplayer(); update_camera(localplayer); client::render::update_uniforms(); - draw_buffers(localplayer, players, chunks); + draw_buffers(localplayer, entities, chunks); draw_hud(localplayer, chunks); - draw_wts_text(localplayer, players); + draw_wts_text(localplayer, entities); client::window::draw(); client::render::swap_window(); } -} // namespace draw +} // namespace render } // namespace client diff --git a/src/client/render/draw.hh b/src/client/render/draw.hh new file mode 100644 index 0000000..bf43224 --- /dev/null +++ b/src/client/render/draw.hh @@ -0,0 +1,30 @@ +#ifndef CLIENT_RENDER_DRAW_HH_ +#define CLIENT_RENDER_DRAW_HH_ + +#include <string_view> + +#include <glm/glm.hpp> + +#include "client/entity/player.hh" +#include "client/input.hh" +#include "client/movement/movement.hh" +#include "client/render/camera.hh" +#include "client/render/program.hh" +#include "client/render/render.hh" +#include "client/render/struct.hh" +#include "client/settings.hh" +#include "client/shared.hh" +#include "client/window/hud_window.hh" +#include "client/window/window.hh" +#include "client/world/chunk.hh" + +namespace client { +namespace render { + +void draw(client::entities_t& players, + client::world::chunks_t& chunks) noexcept; + +} // namespace render +} // namespace client + +#endif diff --git a/src/client/render/render.cc b/src/client/render/render.cc index 18aa32a..941988d 100644 --- a/src/client/render/render.cc +++ b/src/client/render/render.cc @@ -27,7 +27,7 @@ SDL_Window* const& get_sdl_window() noexcept { std::bind(SDL_GL_SetAttribute, SDL_GL_CONTEXT_MINOR_VERSION, 2)); SDL_Window* const ret = - SDL_CreateWindow("blockgame_linux", 0, 0, 0, 0, + SDL_CreateWindow("blockgame_linux", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_BORDERLESS); check_sdl_pointer(ret); @@ -124,6 +124,71 @@ void swap_window() noexcept { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } +// Additional sanity checks for our atlas. +static void check_atlas(const client::render::texture& texture) { + if (texture.width % 6) { + throw std::runtime_error("invalid atlas; WIDTH is not divisible by 6"); + } + if (texture.height % (texture.width / 6)) { + throw std::runtime_error( + "invalid atlas, HEIGHT is not divisible by (WIDTH / 6)"); + } +} + +GLuint get_texture_atlas() noexcept { + static const auto atlas = []() -> GLuint { + GLuint texture = 0; + glActiveTexture(GL_TEXTURE1); + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D_ARRAY, texture); + + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, + GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, + GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY, 16.0f); + + const client::render::texture stbi{"res/textures/atlas.png"}; + check_atlas(stbi); + const int face_size = stbi.width / 6; + + // 2D texture array, where our depth is our block face. + glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, face_size, face_size, + 6 * (stbi.height / face_size), 0, + stbi.channels == 3 ? GL_RGB : GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + + // Fill the 2D texture array. + // Because our image has multiple images on the x-axis and opengl + // expects a single image per axis, we must fill it in row by row. + const auto get_pixel_xy = [&stbi](const int x, const int y) { + return stbi.image + 4 * (y * stbi.width + x); + }; + for (int x = 0; x < 6; ++x) { + const int x_pixel = x * face_size; + + for (int y = 0; y < stbi.height / face_size; ++y) { + const int y_pixel = y * face_size; + + for (auto row = 0; row < face_size; ++row) { + glTexSubImage3D( + GL_TEXTURE_2D_ARRAY, 0, 0, row, x + y * 6, face_size, 1, + 1, GL_RGBA, GL_UNSIGNED_BYTE, + get_pixel_xy(x_pixel, row + y_pixel)); // pixel + } + } + } + + glGenerateMipmap(GL_TEXTURE_2D_ARRAY); + + return texture; + }(); + return atlas; +} + void render_cube_outline(const glm::vec3& pos, const glm::vec4& colour) noexcept { const auto generate_vbo = [](const auto& vertices) -> GLuint { @@ -427,6 +492,8 @@ public: font_atlas(const unsigned size) noexcept : font_size(size), texture(make_texture_info(size)) {} ~font_atlas() noexcept { glDeleteTextures(1, &this->texture.texture); } + font_atlas(font_atlas&&) = delete; + font_atlas(const font_atlas&) = delete; GLint get_height() const noexcept { return this->texture.height; } GLint get_width() const noexcept { return this->texture.width; } @@ -578,25 +645,71 @@ void update_uniforms() noexcept { }; static const GLuint ubo = make_ubo(); - ubo_data upload = {.proj = camera::get_proj(), - .view = camera::get_view(), - .frustum = camera::get_frustum(), - .znear = camera::get_znear(), - .zfar = camera::get_zfar(), - .xfov = glm::radians(camera::get_xfov()), - .yfov = glm::radians(camera::get_yfov()), - .time = []() -> unsigned { - static const auto start = - std::chrono::steady_clock::now(); - const auto now = std::chrono::steady_clock::now(); - return static_cast<unsigned>( - (now - start) / std::chrono::milliseconds(1)); - }(), - .xwindow = static_cast<float>(get_window_size().x), - .ywindow = static_cast<float>(get_window_size().y)}; + const ubo_data upload = { + .proj = camera::get_proj(), + .view = camera::get_view(), + .frustum = camera::get_frustum(), + .znear = camera::get_znear(), + .zfar = camera::get_zfar(), + .xfov = glm::radians(camera::get_xfov()), + .yfov = glm::radians(camera::get_yfov()), + .time = []() -> unsigned { + static const auto start = std::chrono::steady_clock::now(); + const auto now = std::chrono::steady_clock::now(); + return static_cast<unsigned>((now - start) / + std::chrono::milliseconds(1)); + }(), + .xwindow = static_cast<float>(get_window_size().x), + .ywindow = static_cast<float>(get_window_size().y)}; glBindBuffer(GL_UNIFORM_BUFFER, ubo); glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(upload), &upload); } +void draw_rectangle(const rectangle_args& ra) noexcept { + const glm::vec2 pos = ra.pos.to_vec2(); + const glm::vec2 size = ra.size.to_vec2(); + + // We want to render from the bottom left corner. + render::render_rectangle(pos + (size / 2.0f), size, ra.colour); +} + +void draw_colour(const glm::vec4& colour) noexcept { + const rectangle_args args = { + .pos = {.extent = {0.0f, 0.0f}}, + .size = {.extent = {1.0f, 1.0f}}, + .colour = colour, + }; + draw_rectangle(args); +} + +void draw_text(const std::string_view text, const text_args& args) noexcept { + const glm::vec2& window = render::get_window_size(); + const float x = floorf(window.x * args.pos.extent.x + args.pos.offset.x); + const float y = floorf(window.y * args.pos.extent.y + args.pos.offset.y); + const unsigned height = static_cast<unsigned>( + args.extent_height * window.y + args.offset_height); + + const auto make_matrix = [&](const float xo, const float yo) -> glm::mat4 { + constexpr auto identity = glm::mat4(1.0f); + const auto proj = glm::ortho(0.f, window.x, 0.f, window.y); + const auto tran = + glm::translate(identity, glm::vec3{x + xo, y + yo, 0.0f}); + return proj * tran; + }; + + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + if (args.has_backing) { + constexpr glm::vec4 black{0.0f, 0.0f, 0.0f, 1.0f}; + render_text(text, height, args.is_centered, args.is_vcentered, black, + make_matrix(2.0f, -2.0f)); + } + glEnable(GL_BLEND); + + render_text(text, height, args.is_centered, args.is_vcentered, args.colour, + make_matrix(0, 0)); + glEnable(GL_DEPTH_TEST); +} + } // namespace render } // namespace client diff --git a/src/client/render/render.hh b/src/client/render/render.hh index ca98e6f..c98043a 100644 --- a/src/client/render/render.hh +++ b/src/client/render/render.hh @@ -27,8 +27,10 @@ #include "client/render/camera.hh" #include "client/render/program.hh" +#include "client/render/struct.hh" +#include "client/render/texture.hh" #include "shared/shared.hh" -#include "shared/world.hh" +#include "shared/world/chunk.hh" namespace client { namespace render { @@ -40,6 +42,7 @@ void quit() noexcept; int get_fps() noexcept; SDL_Window* const& get_sdl_window() noexcept; const glm::ivec2& get_window_size() noexcept; +GLuint get_texture_atlas() noexcept; // Update void update_uniforms() noexcept; @@ -54,7 +57,26 @@ void render_text(const std::string_view text, const unsigned int size, void render_cube_outline(const glm::vec3& pos, const glm::vec4& colour) noexcept; +// Draw is similar to render but more abstracted. +void draw_colour(const glm::vec4& colour) noexcept; +struct rectangle_args { + relative_arg pos; + relative_arg size; + glm::vec4 colour; +}; +void draw_rectangle(const rectangle_args& args) noexcept; + +struct text_args { + relative_arg pos; + float extent_height; // we don't get width here, + float offset_height; + glm::vec4 colour; + bool has_backing; + bool is_centered; + bool is_vcentered; +}; +void draw_text(const std::string_view text, const text_args& args) noexcept; } // namespace render } // namespace client diff --git a/src/client/render/struct.cc b/src/client/render/struct.cc new file mode 100644 index 0000000..a7a1b5d --- /dev/null +++ b/src/client/render/struct.cc @@ -0,0 +1 @@ +#include "client/render/struct.hh" diff --git a/src/client/render/struct.hh b/src/client/render/struct.hh new file mode 100644 index 0000000..fd00ffd --- /dev/null +++ b/src/client/render/struct.hh @@ -0,0 +1,29 @@ +#ifndef CLIENT_RENDER_STRUCT_HH_ +#define CLIENT_RENDER_STRUCT_HH_ + +//#include <client/render/render.hh> +#include <glm/glm.hpp> + +namespace client { +namespace render { + +const glm::ivec2& get_window_size() noexcept; // forward declaration + +// Scale takes the range [0, 1] and represents an amount of the screen. +// Offset is any value that is added after scale. +struct relative_arg { + glm::vec2 extent; + glm::vec2 offset; + + glm::vec2 to_vec2() const noexcept { + const glm::vec2& window = render::get_window_size(); + const float x = this->extent.x * window.x + this->offset.x; + const float y = this->extent.y * window.y + this->offset.y; + return {x, y}; + } +}; + +} // namespace render +} // namespace client + +#endif diff --git a/src/client/settings.cc b/src/client/settings.cc index 8e2d0c9..3253f60 100644 --- a/src/client/settings.cc +++ b/src/client/settings.cc @@ -9,7 +9,7 @@ const char* const config_name = "blockgame_linux.conf"; namespace client { namespace settings { -static std::string get_settings_file() noexcept { +static std::string get_settings_file() { std::filesystem::create_directories(home_dir + ::config_dir); std::ifstream in{home_dir + ::config_dir + config_name, std::fstream::in | std::fstream::app}; @@ -20,7 +20,7 @@ static std::string get_settings_file() noexcept { return file.ends_with('\n') ? file : file + '\n'; } -static void set_settings_file(const std::string_view contents) noexcept { +static void set_settings_file(const std::string_view contents) { std::filesystem::create_directories(home_dir + ::config_dir); std::ofstream out{home_dir + ::config_dir + ::config_name, std::fstream::out | std::fstream::trunc}; @@ -31,7 +31,7 @@ static void set_settings_file(const std::string_view contents) noexcept { // Heading maps setting to headings (ie, "camera -> {map with fov, 100.0f}). using value_map = std::unordered_map<std::string, std::string>; using setting_map = std::unordered_map<std::string, value_map>; -static setting_map& get_settings() noexcept { +static setting_map& get_settings() { static setting_map ret = []() -> setting_map { setting_map settings{}; @@ -50,8 +50,9 @@ static setting_map& get_settings() noexcept { const size_t split_pos = line.find_first_of(':'); if (split_pos == std::string::npos) { - shared::print::warn("client: failed to parse line in settings " - "file, consider manual intervention\n"); + shared::print::warn + << "client: failed to parse line in settings file, " + "consider manual intervention\n"; continue; } @@ -72,10 +73,21 @@ static setting_map& get_settings() noexcept { return ret; } -// TODO fix this bit of code duplication -std::string -get_setting_str(const std::pair<const std::string&, const std::string&> loc, - const std::string default_value) noexcept { +std::optional<std::string> maybe_get_setting_str(const setting_pair_t& loc) { + const auto& [heading, name] = loc; + + setting_map& settings = get_settings(); + value_map& values = settings[heading]; + + const auto it = values.find(name); + if (it == std::end(values)) { + return std::nullopt; + } + return it->second; +} + +std::string get_setting_str(const setting_pair_t& loc, + const std::string default_value) { const auto& [heading, name] = loc; setting_map& settings = get_settings(); @@ -89,9 +101,7 @@ get_setting_str(const std::pair<const std::string&, const std::string&> loc, return find_it->second; } -void set_setting_str( - const std::pair<const std::string&, const std::string&> loc, - const std::string& value) noexcept { +void set_setting_str(const setting_pair_t& loc, const std::string& value) { const auto& [heading, name] = loc; setting_map& settings = get_settings(); @@ -105,7 +115,7 @@ void set_setting_str( find_it->second = value; } -void save() noexcept { +void save() { std::string contents; const setting_map& settings = get_settings(); diff --git a/src/client/settings.hh b/src/client/settings.hh index d65022b..11b785c 100644 --- a/src/client/settings.hh +++ b/src/client/settings.hh @@ -9,7 +9,7 @@ #include <boost/lexical_cast.hpp> -#include "client/shared.hh" +//#include "client/shared.hh" #include "shared/shared.hh" // This settings file just provides functionality for persistent variables, @@ -18,20 +18,20 @@ namespace client { namespace settings { -void set_setting_str( - const std::pair<const std::string&, const std::string&> loc, - const std::string& value) noexcept; +using setting_pair_t = std::pair<std::string, std::string>; -std::string -get_setting_str(const std::pair<const std::string&, const std::string&> loc, - const std::string default_value) noexcept; +std::optional<std::string> maybe_get_setting_str(const setting_pair_t&); + +void set_setting_str(const setting_pair_t& loc, const std::string& value); + +std::string get_setting_str(const setting_pair_t& loc, + const std::string default_value); // Attempts to read the setting in the file, returning it if it exists. // If either name or category does not exist, writes the default value in // the file under the expected headings. template <typename T> -T get(const std::pair<const std::string, const std::string>& loc, - const T& default_value) noexcept { +T get(const setting_pair_t& loc, const T& default_value) { const std::string value = get_setting_str(loc, boost::lexical_cast<std::string>(default_value)); return boost::lexical_cast<T>(value); @@ -40,12 +40,11 @@ T get(const std::pair<const std::string, const std::string>& loc, // Attempts to set the setting in the file, overwriting the pre-existing // value or writing a new entry. template <typename T> -void set(const std::pair<const std::string, const std::string>& loc, - const T& value) noexcept { +void set(const setting_pair_t& loc, const T& value) { set_setting_str(loc, boost::lexical_cast<std::string>(value)); } -void save() noexcept; +void save(); } // namespace settings } // namespace client diff --git a/src/client/shared.cc b/src/client/shared.cc index 3c6a2ec..0346e3d 100644 --- a/src/client/shared.cc +++ b/src/client/shared.cc @@ -1 +1,18 @@ #include "shared.hh" + +namespace client { + +entities_t::iterator get_entity_it(const std::uint32_t& index) noexcept { + return client::state::entities.find(index); +} + +shared::entity& get_entity(const std::uint32_t index) noexcept { + return *get_entity_it(index)->second; +} + +client::player& get_localplayer() noexcept { + return dynamic_cast<client::player&>( + get_entity(*client::state::localplayer_index)); +} + +} // namespace client diff --git a/src/client/shared.hh b/src/client/shared.hh index 557d13a..31c0ded 100644 --- a/src/client/shared.hh +++ b/src/client/shared.hh @@ -1,28 +1,14 @@ #ifndef CLIENT_SHARED_HH_ #define CLIENT_SHARED_HH_ -#include <cstdint> -#include <string_view> - -#include "shared/net/connection.hh" +#include "client/entity/player.hh" +#include "client/state/entities.hh" namespace client { -struct state { - std::string_view address; - std::string_view port; - std::uint64_t seed; - std::uint32_t localplayer; - std::size_t player_count; - std::size_t requested_chunk_count; - std::size_t networked_chunk_count; - std::int32_t draw_distance; - std::uint16_t latency; - - shared::net::connection* connection = nullptr; -}; - -inline state state; +entities_t::iterator get_entity_it(const std::uint32_t& index) noexcept; +shared::entity& get_entity(const std::uint32_t index) noexcept; +client::player& get_localplayer() noexcept; }; // namespace client diff --git a/src/client/state/chunks.cc b/src/client/state/chunks.cc new file mode 100644 index 0000000..470c552 --- /dev/null +++ b/src/client/state/chunks.cc @@ -0,0 +1 @@ +#include "client/state/chunks.hh" diff --git a/src/client/state/chunks.hh b/src/client/state/chunks.hh new file mode 100644 index 0000000..e0d4d82 --- /dev/null +++ b/src/client/state/chunks.hh @@ -0,0 +1,18 @@ +#ifndef CLIENT_STATE_CHUNKS_HH_ +#define CLIENT_STATE_CHUNKS_HH_ + +#include <memory> +#include <unordered_map> + +#include "client/world/chunk.hh" + +namespace client { +namespace state { + +inline client::world::chunks_t chunks{4096, shared::world::chunk::hash, + shared::world::chunk::equal}; + +} // namespace state +} // namespace client + +#endif diff --git a/src/client/state/entities.cc b/src/client/state/entities.cc new file mode 100644 index 0000000..efb3533 --- /dev/null +++ b/src/client/state/entities.cc @@ -0,0 +1 @@ +#include "client/state/entities.hh" diff --git a/src/client/state/entities.hh b/src/client/state/entities.hh new file mode 100644 index 0000000..3c807b9 --- /dev/null +++ b/src/client/state/entities.hh @@ -0,0 +1,21 @@ +#ifndef CLIENT_STATE_ENTITIES_HH_ +#define CLIENT_STATE_ENTITIES_HH_ + +#include <memory> +#include <unordered_map> + +#include "client/entity/entity.hh" + +namespace client { + +using entities_t = std::unordered_map<shared::entity::index_t, + std::unique_ptr<client::entity>>; + +namespace state { + +inline entities_t entities; + +} // namespace state +} // namespace client + +#endif diff --git a/src/client/state/state.cc b/src/client/state/state.cc new file mode 100644 index 0000000..2423c9a --- /dev/null +++ b/src/client/state/state.cc @@ -0,0 +1,15 @@ +#include "client/state/state.hh" + +namespace client { +namespace state { + +shared::time_duration_t get_time_per_tick() noexcept { + return std::chrono::microseconds(1'000'000) / tickrate; +} + +bool has_initialised() noexcept { + return state::localplayer_index.has_value(); +} + +} // namespace state +} // namespace client diff --git a/src/client/state/state.hh b/src/client/state/state.hh new file mode 100644 index 0000000..cfbb440 --- /dev/null +++ b/src/client/state/state.hh @@ -0,0 +1,41 @@ +#ifndef CLIENT_STATE_STATE_HH_ +#define CLIENT_STATE_STATE_HH_ + +#include <optional> + +#include "shared/entity/entity.hh" +#include "shared/net/connection.hh" +#include "shared/shared.hh" + +namespace client { +namespace state { + +inline std::string_view address; +inline std::string_view port; +inline std::uint64_t seed; +inline std::size_t player_count; +inline std::size_t requested_chunk_count; +inline std::size_t networked_chunk_count; +inline std::int32_t draw_distance; +inline std::int32_t chunks_per_frame; +inline std::uint32_t tickrate; + +inline shared::tick_t tick; +inline float delta_ticks; // num ticks since the last tick, [0, inf) +shared::time_duration_t get_time_per_tick() noexcept; + +// Speed of the client's move ticks should be 1.0f but if we're too slow (or the +// server lags and we're too fast) this value should move to speed up the client +// somewhat gracefully. +inline float time_factor = 1.0f; + +inline shared::net::connection* connection = nullptr; + +// non-opt after init +inline std::optional<shared::entity::index_t> localplayer_index; +bool has_initialised() noexcept; + +} // namespace state +} // namespace client + +#endif diff --git a/src/client/window.cc b/src/client/window.cc deleted file mode 100644 index 6a11252..0000000 --- a/src/client/window.cc +++ /dev/null @@ -1,449 +0,0 @@ -#include "window.hh" - -namespace client { -namespace window { - -// All window objects should derive from basic_window, and make use of multiple -// inheritance for specific behaviours. -class basic_window { -protected: - // Colours borrowed from arc-dark(er). - static constexpr glm::vec3 primary_clr{0.21f, 0.23f, 0.29f}; // dark - static constexpr glm::vec3 secondary_clr{0.29f, 0.32f, 0.38f}; // less dark - static constexpr glm::vec3 tertiary_clr{0.48, 0.50, 0.54}; // less dark ^2 - static constexpr glm::vec3 highlight_clr{0.32, 0.58, 0.88}; // light blue - static constexpr glm::vec3 font_colour{0.88, 0.88, 0.88}; // light grey - -protected: - glm::vec2 pos; - glm::vec2 size; - -public: - bool is_inside(const glm::vec2& v) const noexcept { - if (v.x < this->pos.x || v.x > this->pos.x + this->size.x) { - return false; - } - if (v.y < this->pos.y || v.y > this->pos.y + this->size.y) { - return false; - } - return true; - } - -public: - basic_window(const client::draw::relative_arg& p, - const client::draw::relative_arg& s) noexcept - : pos(p.to_vec2()), size(s.to_vec2()) {} - virtual ~basic_window() noexcept {} - - virtual bool maybe_handle_event(const SDL_Event&) noexcept { return false; } - virtual void draw() noexcept { - client::draw::draw_rectangle({.pos = {.offset = this->pos + 6.0f}, - .size = {.offset = this->size}, - .colour = {this->tertiary_clr, 0.9f}}); - client::draw::draw_rectangle({.pos = {.offset = this->pos}, - .size = {.offset = this->size}, - .colour = {this->primary_clr, 1.0f}}); - } -}; - -static void remove_top_layer() noexcept; -class text_input_window : public basic_window { -private: - // text restricted to a size of 32. - static const std::string& get_send_text() noexcept { - auto& text = client::input::state.text_input; - text = std::string{std::begin(text), - std::begin(text) + - static_cast<long>(std::min( - std::size(text), shared::MAX_SAY_LENGTH))}; - return text; - } - static bool maybe_handle_keydown(const SDL_Event& event) noexcept { - if (event.key.keysym.sym == SDLK_BACKSPACE) { - if (!client::input::state.text_input.empty()) { - client::input::state.text_input.pop_back(); - } - return true; - } - - if (event.key.keysym.sym != SDLK_RETURN || event.key.repeat) { - return false; - } - - if (!client::input::state.text_input.empty()) { - client::send_say_packet(get_send_text()); - } - - remove_top_layer(); // DELETE ME - return true; - } - static const glm::vec3& get_draw_colour() noexcept { - if (client::input::state.text_input.length() >= - shared::MAX_SAY_LENGTH) { - return basic_window::highlight_clr; - } - return basic_window::primary_clr; - } - -public: - template <typename... Args> - text_input_window(Args&&... args) noexcept - : basic_window(std::forward<Args>(args)...) { - client::input::set_text_input(true); - } - virtual ~text_input_window() noexcept { - client::input::set_text_input(false); - } - virtual bool maybe_handle_event(const SDL_Event& event) noexcept override { - switch (event.type) { - case SDL_KEYDOWN: - return this->maybe_handle_keydown(event); - } - - return basic_window::maybe_handle_event(event); - } - - virtual void draw() noexcept override { - basic_window::draw(); - - client::draw::draw_rectangle( - {.pos = {.offset = this->pos}, - .size = {.offset = this->size}, - .colour = {this->get_draw_colour(), 1.0f}}); - - client::draw::draw_text( - this->get_send_text(), - {.pos = {.extent = {0.0f, 0.0f}, - .offset = this->pos + (this->size / 2.0f)}, - .offset_height = this->size.y / 2.0f, - .colour = {this->font_colour, 1.0f}, - .has_backing = false, - .is_centered = true, - .is_vcentered = true}); - }; -}; - -class button_window : public basic_window { -protected: - std::string name; - std::function<void()> callback; - bool is_pressed; - -private: - void handle_mousebuttondown(const SDL_Event& event) noexcept { - if (event.button.button != SDL_BUTTON_LEFT) { - return; - } - this->is_pressed = true; - } - void handle_mousebuttonup(const SDL_Event& event) noexcept { - if (event.button.button != SDL_BUTTON_LEFT) { - return; - } - if (!this->is_pressed) { - return; - } - this->is_pressed = false; - std::invoke(this->callback); // edge - } - const glm::vec3& get_draw_colour() noexcept { - if (!this->is_inside(client::input::state.mouse_pos)) { - this->is_pressed = false; - return this->primary_clr; - } - - if (!this->is_pressed) { - return this->secondary_clr; - } - - return this->highlight_clr; - } - -public: - template <typename... Args> - button_window(const std::string_view n, const decltype(callback)& c, - Args&&... args) noexcept - : basic_window(std::forward<Args>(args)...), name(n), callback(c) {} - - virtual bool maybe_handle_event(const SDL_Event& event) noexcept override { - if (this->is_inside(client::input::state.mouse_pos)) { - switch (event.type) { - case SDL_MOUSEBUTTONDOWN: - this->handle_mousebuttondown(event); - return true; - case SDL_MOUSEBUTTONUP: - this->handle_mousebuttonup(event); - return true; - } - } - - return basic_window::maybe_handle_event(event); - } - virtual void draw() noexcept override { - basic_window::draw(); - - client::draw::draw_rectangle( - {.pos = {.offset = this->pos}, - .size = {.offset = this->size}, - .colour = {this->get_draw_colour(), 1.0f}}); - client::draw::draw_text( - this->name, {.pos = {.extent = {0.0f, 0.0f}, - .offset = this->pos + (this->size / 2.0f)}, - .offset_height = this->size.y / 2.0f, - .colour = {this->font_colour, 1.0f}, - .has_backing = false, - .is_centered = true, - .is_vcentered = true}); - }; -}; - -// Sliders are for numerical values of some type T. -// TODO -/* -template <typename T> -class slider_window : public basic_window { -protected: - std::string name; - T min; - T cur; - T max; - T& var; - -private: - void handle_mousebuttondown(const SDL_Event& event) noexcept {} - void handle_mousebuttonup(const SDL_Event& event) noexcept {} - -public: - template <typename... Args> - slider_window(const std::string_view name, const T& min, const T& cur, - const T& max, T& var, Args&&... args) noexcept - : basic_window(std::forward<Args>(args)...), name(name), min(min), - cur(cur), max(max), var(var) {} - - // slider_window( - virtual bool maybe_handle_event(const SDL_Event& event) noexcept -override { switch (event.type) { case SDL_MOUSEBUTTONDOWN: - this->handle_mousebuttondown(event); - return true; - case SDL_MOUSEBUTTONUP: - this->handle_mousebuttonup(event); - return true; - } - return basic_window::maybe_handle_event(event); - } - virtual void draw() noexcept override { basic_window::draw(); } -}; -*/ - -static void handle_event(const SDL_Event& event) noexcept; // ignore - -// All windows go in this list! -using layer = std::forward_list<std::unique_ptr<basic_window>>; -using layers = std::forward_list<layer>; -static layers& get_layers() noexcept { - // We callbacks for our window manager are initialised here too. - static layers ret = []() -> layers { - client::input::register_event_handler(&handle_event); - client::input::set_text_input(false); - client::input::set_mouse_relative(true); - return {}; - }(); - return ret; -} - -static void remove_top_layer() noexcept { - if (!get_layers().empty()) { - get_layers().pop_front(); - } - // Our windows might be empty here, so set our mouse mode accordingly. - if (!client::window::is_open()) { - client::input::set_mouse_relative(true); - } -} - -// Constants used for uniform ui sizes. -constexpr glm::vec2 lsize_extent{0.4, 0.075}; -constexpr glm::vec2 ssize_extent{0.15, 0.075}; - -static void center_mouse_position() noexcept { - const glm::vec2& window = client::render::get_window_size(); - client::input::set_mouse_position({window.x / 2.0f, window.y / 2.0f}); -} - -template <typename T, typename... Args> -void push_window(Args&&... args) noexcept { - get_layers().front().push_front( - std::make_unique<T>(std::forward<Args>(args)...)); -} - -constexpr glm::vec2 center_extent(const glm::vec2 pos, - const glm::vec2 size) noexcept { - return {pos.x, pos.y - size.y / 2.0f}; -} - -static void make_options_menu() noexcept { - get_layers().push_front({}); - - /* - push_window<::slider_window<float>>( - "Field of Vision", 0.0f, - settings::get(std::make_pair("gameplay", "fov"), 100.0f), 145.0f, - remove_top_layer, - client::draw::relative_arg{.extent = - center_extent({0.3, 0.7}, - lsize_extent)}, client::draw::relative_arg{.extent = lsize_extent}); - */ - - push_window<button_window>( - "Back", remove_top_layer, - client::draw::relative_arg{.extent = - center_extent({0.3, 0.3}, ssize_extent)}, - client::draw::relative_arg{.extent = ssize_extent}); -} - -static void make_main_menu() noexcept { - get_layers().push_front({}); - - push_window<button_window>( - "Return to Game", remove_top_layer, - client::draw::relative_arg{.extent = - center_extent({0.3, 0.7}, lsize_extent)}, - client::draw::relative_arg{.extent = lsize_extent}); - - push_window<button_window>( - "Options", make_options_menu, - client::draw::relative_arg{ - .extent = center_extent({0.55, 0.6}, ssize_extent)}, - client::draw::relative_arg{.extent = ssize_extent}); - - push_window<button_window>( - "?", remove_top_layer, - client::draw::relative_arg{ - .extent = center_extent({0.55, 0.5}, ssize_extent)}, - client::draw::relative_arg{.extent = ssize_extent}); - - push_window<button_window>( - "?", remove_top_layer, - client::draw::relative_arg{ - .extent = center_extent({0.55, 0.4}, ssize_extent)}, - client::draw::relative_arg{.extent = ssize_extent}); - - push_window<button_window>( - "Exit Game", [] { shared::should_exit = true; }, - client::draw::relative_arg{.extent = - center_extent({0.3, 0.3}, lsize_extent)}, - client::draw::relative_arg{.extent = lsize_extent}); - - client::input::set_mouse_relative(false); - center_mouse_position(); -} - -static void make_chat_window() noexcept { - get_layers().push_front({}); - - push_window<text_input_window>( - client::draw::relative_arg{.extent = - center_extent({0.3, 0.3}, lsize_extent)}, - client::draw::relative_arg{.extent = lsize_extent}); - - client::input::set_mouse_relative(false); - center_mouse_position(); -} - -static void handle_meta_return() noexcept { - if (!is_open()) { - make_chat_window(); - return; - } -} - -static void handle_meta_escape() noexcept { - if (!is_open()) { - make_main_menu(); - return; - } - - remove_top_layer(); -} - -static void handle_meta_keydown(const SDL_Event& event) noexcept { - if (event.key.repeat) { // only handle keypresses - return; - } - - switch (event.key.keysym.sym) { - case SDLK_ESCAPE: - handle_meta_escape(); - break; - case SDLK_RETURN: - handle_meta_return(); - break; - } -} - -static void handle_meta_mousemotion(const SDL_Event& event) noexcept { - // We convert SDL's weird coordinates into useful ones (0,0 = bottom - // left). - client::input::state.mouse_pos = { - event.motion.x, - static_cast<int>(client::render::get_window_size().y) - event.motion.y}; -} - -static void handle_meta_windowevent(const SDL_Event& event) noexcept { - if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) { - if (!is_open()) { - make_main_menu(); - return; - } - } -} - -static void handle_meta_event(const SDL_Event& event) noexcept { - switch (event.type) { - case SDL_KEYDOWN: - handle_meta_keydown(event); - break; - case SDL_MOUSEMOTION: - handle_meta_mousemotion(event); - break; - case SDL_WINDOWEVENT: - handle_meta_windowevent(event); - break; - } -} - -static void handle_event(const SDL_Event& event) noexcept { - // We ALWAYS update our mouse position. - if (event.type == SDL_MOUSEMOTION) { - handle_meta_mousemotion(event); - } - - // Either a window consumes our event, or no window does - so we send - // the event to our "meta handler" which does things like closing - // windows etc. - if (is_open()) { - for (const auto& window : get_layers().front()) { - if (window->maybe_handle_event(event)) { - return; - } - } - } - - handle_meta_event(event); -} - -void draw() noexcept { - if (!is_open()) { - return; - } - - client::draw::draw_colour({0.0f, 0.0f, 0.0f, 0.10f}); // very light shade - for (const auto& window : get_layers().front()) { - window->draw(); - } -} - -bool is_open() noexcept { return !get_layers().empty(); } - -} // namespace window -} // namespace client diff --git a/src/client/window/basic_window.cc b/src/client/window/basic_window.cc new file mode 100644 index 0000000..c96f94a --- /dev/null +++ b/src/client/window/basic_window.cc @@ -0,0 +1,33 @@ +#include "client/window/basic_window.hh" + +namespace client { +namespace window { + +using bw = basic_window; + +bool bw::is_inside(const glm::vec2& v, const glm::vec2& p, + const glm::vec2& s) noexcept { + if (v.x < p.x || v.x > p.x + s.x) { + return false; + } + if (v.y < p.y || v.y > p.y + s.y) { + return false; + } + return true; +} + +bool bw::is_inside(const glm::vec2& v) const noexcept { + return is_inside(v, this->pos, this->size); +} + +void bw::draw() noexcept { + client::render::draw_rectangle({.pos = {.offset = this->pos + 6.0f}, + .size = {.offset = this->size}, + .colour = {this->tertiary_clr, 0.9f}}); + client::render::draw_rectangle({.pos = {.offset = this->pos}, + .size = {.offset = this->size}, + .colour = {this->primary_clr, 1.0f}}); +} + +} // namespace window +} // namespace client diff --git a/src/client/window/basic_window.hh b/src/client/window/basic_window.hh new file mode 100644 index 0000000..292cc64 --- /dev/null +++ b/src/client/window/basic_window.hh @@ -0,0 +1,52 @@ +#ifndef CLIENT_WINDOW_BASIC_WINDOW_HH_ +#define CLIENT_WINDOW_BASIC_WINDOW_HH_ + +#include <glm/glm.hpp> + +#include "client/entity/player.hh" +#include "client/render/render.hh" +#include "client/render/struct.hh" + +namespace client { +namespace window { + +// All window objects should derive from basic_window, and make use of multiple +// inheritance for specific behaviours. +class basic_window { +protected: + // Colours borrowed from arc-dark(er). + static constexpr glm::vec3 primary_clr{0.21f, 0.23f, 0.29f}; // dark + static constexpr glm::vec3 secondary_clr{0.29f, 0.32f, 0.38f}; // less dark + static constexpr glm::vec3 tertiary_clr{0.48, 0.50, 0.54}; // less dark ^2 + static constexpr glm::vec3 highlight_clr{0.32, 0.58, 0.88}; // light blue + static constexpr glm::vec3 font_colour{0.88, 0.88, 0.88}; // light grey + +protected: + static constexpr float OUTLINE_WIDTH = 2.0f; + float get_item_size() const noexcept { return this->size.x / 10.0f; } + +protected: + glm::vec2 pos; + glm::vec2 size; + +public: + // Test if a vec2 v is inside a square starting at position p with size s. + static bool is_inside(const glm::vec2& v, const glm::vec2& p, + const glm::vec2& s) noexcept; + // Test if v is inside this->pos, this->size + bool is_inside(const glm::vec2& v) const noexcept; + +public: + basic_window(const client::render::relative_arg& pos, + const client::render::relative_arg& size) noexcept + : pos(pos.to_vec2()), size(size.to_vec2()) {} + virtual ~basic_window() noexcept {} + + virtual bool maybe_handle_event(const SDL_Event&) noexcept { return false; } + virtual void draw() noexcept; +}; + +} // namespace window +} // namespace client + +#endif diff --git a/src/client/window/button_window.cc b/src/client/window/button_window.cc new file mode 100644 index 0000000..4743c0a --- /dev/null +++ b/src/client/window/button_window.cc @@ -0,0 +1,71 @@ +#include "client/window/button_window.hh" + +namespace client { +namespace window { + +using bw = class button_window; + +void bw::handle_mousebuttondown(const SDL_Event& event) noexcept { + if (event.button.button != SDL_BUTTON_LEFT) { + return; + } + this->is_pressed = true; +} + +void bw::handle_mousebuttonup(const SDL_Event& event) noexcept { + if (event.button.button != SDL_BUTTON_LEFT) { + return; + } + if (!this->is_pressed) { + return; + } + this->is_pressed = false; + std::invoke(this->callback); // edge +} + +const glm::vec3& bw::get_draw_colour() noexcept { + if (!this->is_inside(client::input::state.mouse_pos)) { + this->is_pressed = false; + return this->primary_clr; + } + + if (!this->is_pressed) { + return this->secondary_clr; + } + + return this->highlight_clr; +} + +bool bw::maybe_handle_event(const SDL_Event& event) noexcept { + if (this->is_inside(client::input::state.mouse_pos)) { + switch (event.type) { + case SDL_MOUSEBUTTONDOWN: + this->handle_mousebuttondown(event); + return true; + case SDL_MOUSEBUTTONUP: + this->handle_mousebuttonup(event); + return true; + } + } + + return basic_window::maybe_handle_event(event); +} + +void bw::draw() noexcept { + basic_window::draw(); + + client::render::draw_rectangle({.pos = {.offset = this->pos}, + .size = {.offset = this->size}, + .colour = {this->get_draw_colour(), 1.0f}}); + client::render::draw_text( + this->name, {.pos = {.extent = {0.0f, 0.0f}, + .offset = this->pos + (this->size / 2.0f)}, + .offset_height = this->size.y / 2.0f, + .colour = {this->font_colour, 1.0f}, + .has_backing = false, + .is_centered = true, + .is_vcentered = true}); +}; + +} // namespace window +} // namespace client diff --git a/src/client/window/button_window.hh b/src/client/window/button_window.hh new file mode 100644 index 0000000..6847696 --- /dev/null +++ b/src/client/window/button_window.hh @@ -0,0 +1,40 @@ +#ifndef CLIENT_WINDOW_BUTTON_WINDOW_HH_ +#define CLIENT_WINDOW_BUTTON_WINDOW_HH_ + +#include <functional> +#include <string> + +#include "client/input.hh" +#include "client/render/render.hh" +#include "client/window/basic_window.hh" + +namespace client { +namespace window { + +class button_window : public basic_window { +protected: + std::string name; + std::function<void()> callback; + bool is_pressed; + +private: + void handle_mousebuttondown(const SDL_Event& event) noexcept; + void handle_mousebuttonup(const SDL_Event& event) noexcept; + const glm::vec3& get_draw_colour() noexcept; + +public: + template <typename... Args> + button_window(const std::string_view n, const decltype(callback)& c, + Args&&... args) noexcept + : basic_window(std::forward<Args>(args)...), name(n), callback(c), + is_pressed(false) {} + virtual ~button_window() noexcept {} + + virtual bool maybe_handle_event(const SDL_Event& event) noexcept override; + virtual void draw() noexcept override; +}; + +} // namespace window +} // namespace client + +#endif diff --git a/src/client/window/hud_window.cc b/src/client/window/hud_window.cc new file mode 100644 index 0000000..52995ca --- /dev/null +++ b/src/client/window/hud_window.cc @@ -0,0 +1,88 @@ +#include "client/window/hud_window.hh" + +namespace client { +namespace window { + +using hw = class hud_window; +bool hw::maybe_handle_keydown(const SDL_Event& event) noexcept { + const auto& sym = event.key.keysym.sym; + const auto index = static_cast<int>(sym); + + const auto min = static_cast<int>(SDLK_0); + const auto max = static_cast<int>(SDLK_9); + if (index < min || index > max) { + return false; + } + + const auto active = + static_cast<std::uint32_t>((((sym - min) - 1) + 10) % 10); + client::get_localplayer().get_mutable_active_item() = active; + return true; +} + +bool hw::maybe_handle_event(const SDL_Event& event) noexcept { + if (is_open()) { + return false; + } + + switch (event.type) { + case SDL_KEYDOWN: + return this->maybe_handle_keydown(event); + default: + break; + } + return false; +} + +void hw::draw() noexcept { + this->basic_window::draw(); + + const float item_size = this->get_item_size(); + + const auto& localplayer = client::get_localplayer(); + client::render::draw_rectangle( + {.pos = {.offset = {this->pos.x + static_cast<float>( + localplayer.get_active_item()) * + item_size, + this->pos.y}}, + .size = {.offset = {item_size, item_size}}, + .colour = {this->secondary_clr, 1.0f}}); + + for (int i = 1; i < shared::player::INVENTORY_COLS; ++i) { + const float off = item_size * static_cast<float>(i); + client::render::draw_rectangle( + {.pos = {.offset = {this->pos.x + off, + this->pos.y + OUTLINE_WIDTH}}, + .size = {.offset = {OUTLINE_WIDTH, + item_size - 2.0f * OUTLINE_WIDTH}}, + .colour = {this->tertiary_clr, 1.0f}}); + } + + const auto& inventory = localplayer.inventory; + for (int x = 0; x < shared::player::INVENTORY_COLS; ++x) { + const auto& item = inventory.contents[static_cast<unsigned long>(x)]; + if (item == nullptr) { + continue; + } + + const glm::vec2 off{glm::vec2{x, 0} * item_size}; + const auto item_ptr = dynamic_cast<client::item::item*>(&*item); + if (item_ptr == nullptr) { + continue; + } + item_ptr->draw(this->pos + off, glm::vec2{item_size, item_size}); + + client::render::draw_text( + std::to_string(item->quantity), + {.pos = {.offset = this->pos + off + + glm::vec2{item_size * 0.75f, item_size * 0.2f}}, + .offset_height = item_size * 0.40f, + .colour = {this->font_colour, 1.0f}, + .has_backing = false, + .is_centered = true, + .is_vcentered = true}); + } +} + +} // namespace window +} // namespace client diff --git a/src/client/window/hud_window.hh b/src/client/window/hud_window.hh new file mode 100644 index 0000000..7c03e0e --- /dev/null +++ b/src/client/window/hud_window.hh @@ -0,0 +1,32 @@ +#ifndef CLIENT_WINDOW_HUD_WINDOW_HH_ +#define CLIENT_WINDOW_HUD_WINDOW_HH_ + +#include "client/render/struct.hh" +#include "client/window/basic_window.hh" +#include "client/window/window.hh" +#include "shared/entity/player.hh" + +namespace client { +namespace window { + +class hud_window : public basic_window { + +private: + bool maybe_handle_keydown(const SDL_Event& event) noexcept; + +public: + template <typename... Args> + hud_window(const float size) noexcept + : basic_window( + client::render::relative_arg{.extent = {0.5f, 0.0f}, + .offset = {-size / 2.0f, 0.0f}}, + client::render::relative_arg{.offset = {size, size / 10.0f}}) {} + + virtual void draw() noexcept override; + virtual bool maybe_handle_event(const SDL_Event& event) noexcept override; +}; + +} // namespace window +} // namespace client + +#endif diff --git a/src/client/window/inventory_window.cc b/src/client/window/inventory_window.cc new file mode 100644 index 0000000..5023f1d --- /dev/null +++ b/src/client/window/inventory_window.cc @@ -0,0 +1,196 @@ +#include "client/window/inventory_window.hh" + +namespace client { +namespace window { + +using iw = class inventory_window; + +bool iw::maybe_handle_mousebuttondown(const SDL_Event& event) noexcept { + + if (event.button.button != SDL_BUTTON_LEFT) { + return false; + } + if (!this->is_inside(client::input::state.mouse_pos)) { + return false; + } + + const auto index = + this->maybe_get_inventory_index(client::input::state.mouse_pos); + if (!index.has_value()) { + return false; + } + + const auto& inventory = client::get_localplayer().inventory; + if (inventory.contents[*index] != nullptr) { + this->grabbed.emplace(*index); + } + return true; +} + +static proto::packet make_swap_packet(const std::uint32_t& a, + const std::uint32_t& b) noexcept { + proto::packet ret; + const auto packet = ret.mutable_item_swap_packet(); + packet->set_index_a(a); + packet->set_index_b(b); + return ret; +} + +bool iw::maybe_handle_mousebuttonup(const SDL_Event& event) noexcept { + + if (event.button.button != SDL_BUTTON_LEFT) { + return false; + } + if (!this->grabbed.has_value()) { + return false; + } + + if (const auto index = + this->maybe_get_inventory_index(input::state.mouse_pos); + index.has_value() && index != *grabbed) { + + auto& inventory = client::get_localplayer().inventory; + std::swap(inventory.contents[*grabbed], inventory.contents[*index]); + + // replicate on server + state::connection->rsend_packet( + make_swap_packet(static_cast<std::uint32_t>(*grabbed), + static_cast<std::uint32_t>(*index))); + } + + this->grabbed.reset(); + return true; +} + +std::optional<unsigned long> +iw::maybe_get_inventory_index(const glm::vec2& pos) const noexcept { + + const float item_size = this->get_item_size(); + for (int x = 0; x < shared::player::INVENTORY_COLS; ++x) { + for (int y = 0; y < shared::player::INVENTORY_ROWS; ++y) { + const glm::vec2 off{this->pos + + glm::vec2{OUTLINE_WIDTH, OUTLINE_WIDTH} + + glm::vec2{x, y} * item_size}; + if (!basic_window::is_inside(pos, off, + glm::vec2{item_size, item_size})) { + continue; + } + return x + y * shared::player::INVENTORY_COLS; + } + } + return std::nullopt; +} + +bool iw::maybe_handle_event(const SDL_Event& event) noexcept { + switch (event.type) { + case SDL_MOUSEBUTTONDOWN: + return this->maybe_handle_mousebuttondown(event); + case SDL_MOUSEBUTTONUP: + return this->maybe_handle_mousebuttonup(event); + } + + return basic_window::maybe_handle_event(event); +} + +void iw::draw() noexcept { + basic_window::draw(); + + const float item_size = this->get_item_size(); + + const auto& localplayer = client::get_localplayer(); + client::render::draw_rectangle( + {.pos = {.offset = {this->pos.x + static_cast<float>( + localplayer.get_active_item()) * + item_size, + this->pos.y}}, + .size = {.offset = {item_size, item_size}}, + .colour = {this->secondary_clr, 1.0f}}); + for (int i = 1; i < shared::player::INVENTORY_COLS; ++i) { + const float off = item_size * static_cast<float>(i); + client::render::draw_rectangle( + {.pos = {.offset = {this->pos.x + off, + this->pos.y + OUTLINE_WIDTH}}, + .size = {.offset = {OUTLINE_WIDTH, + item_size * shared::player::INVENTORY_ROWS - + OUTLINE_WIDTH}}, + .colour = {this->tertiary_clr, 1.0f}}); + + if (i <= shared::player::INVENTORY_ROWS) { + client::render::draw_rectangle( + {.pos = {.offset = {pos.x + OUTLINE_WIDTH, pos.y + off}}, + .size = {.offset = {this->size.x - 2.0f * OUTLINE_WIDTH, + OUTLINE_WIDTH}}, + .colour = {this->tertiary_clr, 1.0f}}); + } + } + + const auto& inventory = localplayer.inventory; + if (const auto index = + this->maybe_get_inventory_index(client::input::state.mouse_pos); + index.has_value()) { + const glm::vec2 off{glm::vec2{*index % shared::player::INVENTORY_COLS, + *index / shared::player::INVENTORY_COLS} * + item_size}; + client::render::draw_rectangle( + {.pos = {.offset = this->pos + off + + glm::vec2{OUTLINE_WIDTH, OUTLINE_WIDTH}}, + .size = {.offset = glm::vec2{item_size, item_size} - + glm::vec2{OUTLINE_WIDTH, OUTLINE_WIDTH}}, + .colour = {this->secondary_clr, 1.0f}}); + } + + for (int x = 0; x < shared::player::INVENTORY_COLS; ++x) { + for (int y = 0; y < shared::player::INVENTORY_ROWS; ++y) { + const auto index = static_cast<unsigned long>( + x + y * shared::player::INVENTORY_COLS); + const auto& item = inventory.contents[index]; + if (item == nullptr) { + continue; + } + if (this->grabbed.has_value() && *this->grabbed == index) { + continue; + } + + const glm::vec2 off{glm::vec2{x, y} * item_size}; + const auto item_ptr = dynamic_cast<client::item::item*>(&*item); + if (item_ptr == nullptr) { + continue; + } + item_ptr->draw(this->pos + off, glm::vec2{item_size, item_size}); + + client::render::draw_text( + std::to_string(item->quantity), + {.pos = {.offset = + this->pos + off + + glm::vec2{item_size * 0.75f, item_size * 0.2f}}, + .offset_height = item_size * 0.40f, + .colour = {this->font_colour, 1.0f}, + .has_backing = false, + .is_centered = true, + .is_vcentered = true}); + } + } + + if (this->grabbed.has_value()) { + const auto item_ptr = + dynamic_cast<client::item::item*>(&*inventory.contents[*grabbed]); + if (item_ptr != nullptr) { + item_ptr->draw(client::input::state.mouse_pos - + glm::vec2{item_size, item_size} * 0.5f, + glm::vec2{item_size, item_size}); + client::render::draw_text( + std::to_string(item_ptr->quantity), + {.pos = {.offset = + client::input::state.mouse_pos + + glm::vec2{item_size * 0.25f, item_size * -0.3f}}, + .offset_height = item_size * 0.40f, + .colour = {this->font_colour, 1.0f}, + .has_backing = false, + .is_centered = true, + .is_vcentered = true}); + } + } +} + +} // namespace window +} // namespace client diff --git a/src/client/window/inventory_window.hh b/src/client/window/inventory_window.hh new file mode 100644 index 0000000..a77ae2f --- /dev/null +++ b/src/client/window/inventory_window.hh @@ -0,0 +1,40 @@ +#ifndef CLIENT_WINDOW_INVENTORY_WINDOW_HH_ +#define CLIENT_WINDOW_INVENTORY_WINDOW_HH_ + +#include <cstdint> +#include <optional> + +#include "client/item/item.hh" +#include "client/render/draw.hh" +#include "client/state/state.hh" +#include "client/window/basic_window.hh" + +namespace client { +namespace window { + +class inventory_window : public basic_window { +private: + std::optional<unsigned long> grabbed; + +private: + std::optional<unsigned long> + maybe_get_inventory_index(const glm::vec2& pos) const noexcept; + +private: + bool maybe_handle_mousebuttondown(const SDL_Event& event) noexcept; + bool maybe_handle_mousebuttonup(const SDL_Event& event) noexcept; + +public: + template <typename... Args> + inventory_window(Args&&... args) noexcept + : basic_window(std::forward<Args>(args)...) {} + virtual ~inventory_window() noexcept {} + + virtual bool maybe_handle_event(const SDL_Event& event) noexcept override; + virtual void draw() noexcept override; +}; + +} // namespace window +} // namespace client + +#endif diff --git a/src/client/window/text_input_window.cc b/src/client/window/text_input_window.cc new file mode 100644 index 0000000..0d556a4 --- /dev/null +++ b/src/client/window/text_input_window.cc @@ -0,0 +1,81 @@ +#include "client/window/text_input_window.hh" + +namespace client { +namespace window { + +using tiw = class text_input_window; + +const std::string& tiw::get_send_text() noexcept { + auto& text = client::input::state.text_input; + text = std::string{std::begin(text), + std::begin(text) + + static_cast<long>(std::min(std::size(text), + shared::MAX_SAY_LENGTH))}; + return text; +} + +proto::packet tiw::make_say_packet() noexcept { + proto::packet packet; + + const auto sub_say_packet = packet.mutable_say_packet(); + sub_say_packet->set_text(get_send_text()); + + return packet; +} + +bool tiw::maybe_handle_keydown(const SDL_Event& event) noexcept { + if (event.key.keysym.sym == SDLK_BACKSPACE) { + if (!client::input::state.text_input.empty()) { + client::input::state.text_input.pop_back(); + } + return true; + } + + if (event.key.keysym.sym != SDLK_RETURN || event.key.repeat) { + return false; + } + + if (!client::input::state.text_input.empty()) { + client::state::connection->rsend_packet(make_say_packet()); + } + + pop_window(); + return true; +} + +const glm::vec3& tiw::get_draw_colour() noexcept { + if (client::input::state.text_input.length() >= shared::MAX_SAY_LENGTH) { + return basic_window::highlight_clr; + } + return basic_window::primary_clr; +} + +bool tiw::maybe_handle_event(const SDL_Event& event) noexcept { + switch (event.type) { + case SDL_KEYDOWN: + return this->maybe_handle_keydown(event); + } + + return basic_window::maybe_handle_event(event); +} + +void tiw::draw() noexcept { + basic_window::draw(); + + client::render::draw_rectangle({.pos = {.offset = this->pos}, + .size = {.offset = this->size}, + .colour = {this->get_draw_colour(), 1.0f}}); + + client::render::draw_text( + this->get_send_text(), + {.pos = {.extent = {0.0f, 0.0f}, + .offset = this->pos + (this->size / 2.0f)}, + .offset_height = this->size.y / 2.0f, + .colour = {this->font_colour, 1.0f}, + .has_backing = false, + .is_centered = true, + .is_vcentered = true}); +}; + +} // namespace window +} // namespace client diff --git a/src/client/window/text_input_window.hh b/src/client/window/text_input_window.hh new file mode 100644 index 0000000..edff672 --- /dev/null +++ b/src/client/window/text_input_window.hh @@ -0,0 +1,43 @@ +#ifndef CLIENT_WINDOW_TEXT_INPUT_WINDOW_HH_ +#define CLIENT_WINDOW_TEXT_INPUT_WINDOW_HH_ + +#include <string> + +#include "client/input.hh" +#include "client/shared.hh" +#include "client/state/state.hh" +#include "client/window/basic_window.hh" +#include "shared/net/proto.hh" + +namespace client { +namespace window { + +void pop_window() noexcept; // forward declaration + +class text_input_window : public basic_window { +private: + // text restricted to a size of 32. + static const std::string& get_send_text() noexcept; + static proto::packet make_say_packet() noexcept; + + static bool maybe_handle_keydown(const SDL_Event& event) noexcept; + static const glm::vec3& get_draw_colour() noexcept; + +public: + template <typename... Args> + text_input_window(Args&&... args) noexcept + : basic_window(std::forward<Args>(args)...) { + client::input::set_text_input(true); + } + virtual ~text_input_window() noexcept { + client::input::set_text_input(false); + } + virtual bool maybe_handle_event(const SDL_Event& event) noexcept override; + + virtual void draw() noexcept override; +}; + +} // namespace window +} // namespace client + +#endif diff --git a/src/client/window/window.cc b/src/client/window/window.cc new file mode 100644 index 0000000..648997f --- /dev/null +++ b/src/client/window/window.cc @@ -0,0 +1,318 @@ +#include "window.hh" + +namespace client { +namespace window { + +// TODO + +// Sliders are for numerical values of some type T. +// TODO +/* +template <typename T> +class slider_window : public basic_window { +protected: + std::string name; + T min; + T cur; + T max; + T& var; + +private: + void handle_mousebuttondown(const SDL_Event& event) noexcept {} + void handle_mousebuttonup(const SDL_Event& event) noexcept {} + +public: + template <typename... Args> + slider_window(const std::string_view name, const T& min, const T& cur, + const T& max, T& var, Args&&... args) noexcept + : basic_window(std::forward<Args>(args)...), name(name), min(min), + cur(cur), max(max), var(var) {} + + // slider_window( + virtual bool maybe_handle_event(const SDL_Event& event) noexcept +override { switch (event.type) { case SDL_MOUSEBUTTONDOWN: + this->handle_mousebuttondown(event); + return true; + case SDL_MOUSEBUTTONUP: + this->handle_mousebuttonup(event); + return true; + } + return basic_window::maybe_handle_event(event); + } + virtual void draw() noexcept override { basic_window::draw(); } +}; +*/ + +static void handle_event(const SDL_Event& event) noexcept; // ignore + +static hud_window& get_hud_window() noexcept { + static hud_window ret = []() { + const auto& window = client::render::get_window_size(); + const float size = std::min(static_cast<float>(window.x) / 2.0f, + static_cast<float>(window.y) / 2.0f); + client::window::hud_window ret{size}; + return ret; + }(); + return ret; +} + +// All dynamic windows go in this list! +using layer = std::forward_list<std::unique_ptr<basic_window>>; +using layers = std::forward_list<layer>; +static layers& get_layers() noexcept { + // We callbacks for our window manager are initialised here too. + static layers ret = []() -> layers { + client::input::register_event_handler(&handle_event); + client::input::set_text_input(false); + client::input::set_mouse_relative(true); + return {}; + }(); + return ret; +} + +void pop_window() noexcept { + if (!get_layers().empty()) { + get_layers().pop_front(); + } + // Our windows might be empty here, so set our mouse mode accordingly. + if (!client::window::is_open()) { + client::input::set_mouse_relative(true); + } +} + +// Constants used for uniform ui sizes. +constexpr glm::vec2 lsize_extent{0.4, 0.075}; +constexpr glm::vec2 ssize_extent{0.15, 0.075}; + +static void center_mouse_position() noexcept { + const glm::vec2& window = client::render::get_window_size(); + client::input::set_mouse_position({window.x / 2.0f, window.y / 2.0f}); +} + +template <typename T, typename... Args> +void push_window(Args&&... args) noexcept { + get_layers().front().push_front( + std::make_unique<T>(std::forward<Args>(args)...)); +} + +constexpr glm::vec2 center_extent(const glm::vec2 pos, + const glm::vec2 size) noexcept { + return {pos.x, pos.y - size.y / 2.0f}; +} + +static void make_options_menu() noexcept { + get_layers().push_front({}); + + /* + push_window<::slider_window<float>>( + "Field of Vision", 0.0f, + settings::get(std::make_pair("gameplay", "fov"), 100.0f), 145.0f, + pop_window, + client::draw::relative_arg{.extent = + center_extent({0.3, 0.7}, + lsize_extent)}, client::draw::relative_arg{.extent = lsize_extent}); + */ + + push_window<button_window>( + "Back", pop_window, + client::render::relative_arg{ + .extent = center_extent({0.3, 0.3}, ssize_extent)}, + client::render::relative_arg{.extent = ssize_extent}); +} + +static void make_main_menu() noexcept { + get_layers().push_front({}); + + push_window<button_window>( + "Return to Game", pop_window, + client::render::relative_arg{ + .extent = center_extent({0.3, 0.7}, lsize_extent)}, + client::render::relative_arg{.extent = lsize_extent}); + + push_window<button_window>( + "Options", make_options_menu, + client::render::relative_arg{ + .extent = center_extent({0.55, 0.6}, ssize_extent)}, + client::render::relative_arg{.extent = ssize_extent}); + + push_window<button_window>( + "?", pop_window, + client::render::relative_arg{ + .extent = center_extent({0.55, 0.5}, ssize_extent)}, + client::render::relative_arg{.extent = ssize_extent}); + + push_window<button_window>( + "?", pop_window, + client::render::relative_arg{ + .extent = center_extent({0.55, 0.4}, ssize_extent)}, + client::render::relative_arg{.extent = ssize_extent}); + + push_window<button_window>( + "Exit Game", [] { shared::should_exit = true; }, + client::render::relative_arg{ + .extent = center_extent({0.3, 0.3}, lsize_extent)}, + client::render::relative_arg{.extent = lsize_extent}); + + client::input::set_mouse_relative(false); + center_mouse_position(); +} + +static void make_chat_window() noexcept { + get_layers().push_front({}); + + push_window<text_input_window>( + client::render::relative_arg{ + .extent = center_extent({0.3, 0.3}, lsize_extent)}, + client::render::relative_arg{.extent = lsize_extent}); + + client::input::set_mouse_relative(false); + center_mouse_position(); +} + +static void make_inventory_window() noexcept { + get_layers().push_front({}); + + const glm::vec2& window = client::render::get_window_size(); + + const float size = std::min(window.x / 2.0f, window.y / 2.0f); + const glm::vec2 pos{(window - size) / 2.0f}; + + push_window<inventory_window>( + client::render::relative_arg{.offset = pos}, + client::render::relative_arg{.offset = glm::vec2{size, size}}); + client::input::set_mouse_relative(false); + center_mouse_position(); +} + +static void handle_meta_return() noexcept { + if (!is_open()) { + make_chat_window(); + return; + } +} + +static void handle_meta_escape() noexcept { + if (!is_open()) { + make_main_menu(); + return; + } + + pop_window(); +} + +static void handle_meta_e() noexcept { + if (!is_open()) { + make_inventory_window(); + return; + } + + // The inventory window must be open. + if (!dynamic_cast<inventory_window*>(&*get_layers().front().front())) { + return; + } + + pop_window(); +} + +static void handle_meta_keydown(const SDL_Event& event) noexcept { + if (event.key.repeat) { // only handle keypresses + return; + } + + switch (event.key.keysym.sym) { + case SDLK_ESCAPE: + handle_meta_escape(); + break; + case SDLK_RETURN: + handle_meta_return(); + break; + case SDLK_e: + handle_meta_e(); + break; + case SDLK_0: + case SDLK_1: + case SDLK_2: + case SDLK_3: + case SDLK_4: + case SDLK_5: + case SDLK_6: + case SDLK_7: + case SDLK_8: + case SDLK_9: + get_hud_window().maybe_handle_event(event); + default: + break; + } +} + +static void handle_meta_mousemotion(const SDL_Event& event) noexcept { + // We convert SDL's weird coordinates into useful ones (0,0 = bottom + // left). + client::input::state.mouse_pos = { + event.motion.x, + static_cast<int>(client::render::get_window_size().y) - event.motion.y}; +} + +static void handle_meta_windowevent(const SDL_Event& event) noexcept { + if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) { + if (!is_open()) { + make_main_menu(); + return; + } + } +} + +static void handle_meta_event(const SDL_Event& event) noexcept { + switch (event.type) { + case SDL_KEYDOWN: + handle_meta_keydown(event); + break; + case SDL_MOUSEMOTION: + handle_meta_mousemotion(event); + break; + case SDL_WINDOWEVENT: + handle_meta_windowevent(event); + break; + } +} + +static void handle_event(const SDL_Event& event) noexcept { + // We ALWAYS update our mouse position. + if (event.type == SDL_MOUSEMOTION) { + handle_meta_mousemotion(event); + } + + // Either a window consumes our event, or no window does - so we send + // the event to our "meta handler" which does things like closing + // windows etc. + if (is_open()) { + for (const auto& window : get_layers().front()) { + if (window->maybe_handle_event(event)) { + return; + } + } + } + + handle_meta_event(event); +} + +void draw() noexcept { + if (!is_open()) { + + if (!client::input::is_key_toggled(SDLK_F1)) { + get_hud_window().draw(); + } + + return; + } + + client::render::draw_colour({0.0f, 0.0f, 0.0f, 0.10f}); // very light shade + for (const auto& window : get_layers().front()) { + window->draw(); + } +} + +bool is_open() noexcept { return !get_layers().empty(); } + +} // namespace window +} // namespace client diff --git a/src/client/window.hh b/src/client/window/window.hh index 094c4ca..d112d18 100644 --- a/src/client/window.hh +++ b/src/client/window/window.hh @@ -1,5 +1,5 @@ -#ifndef CLIENT_WINDOW_HH_ -#define CLIENT_WINDOW_HH_ +#ifndef CLIENT_WINDOW_WINDOW_HH_ +#define CLIENT_WINDOW_WINDOW_HH_ #include <algorithm> #include <forward_list> @@ -13,17 +13,25 @@ #include <glm/glm.hpp> #include "client/client.hh" -#include "client/draw.hh" #include "client/input.hh" +#include "client/render/render.hh" #include "client/settings.hh" +#include "client/window/basic_window.hh" +#include "client/window/button_window.hh" +#include "client/window/hud_window.hh" +#include "client/window/inventory_window.hh" +#include "client/window/text_input_window.hh" #include "shared/shared.hh" namespace client { namespace window { -void draw() noexcept; +// removes the topmost window +void pop_window() noexcept; bool is_open() noexcept; +void draw() noexcept; + } // namespace window } // namespace client diff --git a/src/client/world.cc b/src/client/world.cc deleted file mode 100644 index 1ceb9fb..0000000 --- a/src/client/world.cc +++ /dev/null @@ -1,429 +0,0 @@ -#include "world.hh" - -namespace client { -namespace world { - -// Additional sanity checks for our atlas. -static void check_atlas(const client::render::texture& texture) { - if (texture.width % 6) { - throw std::runtime_error("invalid atlas; WIDTH is not divisible by 6"); - } - if (texture.height % (texture.width / 6)) { - throw std::runtime_error( - "invalid atlas, HEIGHT is not divisible by (WIDTH / 6)"); - } -} - -void chunk::render(const float world_x, const float world_z, - const pass& pass) noexcept { - const auto make_texture = []() -> GLuint { - GLuint texture = 0; - glActiveTexture(GL_TEXTURE1); - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D_ARRAY, texture); - - glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, - GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, - GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, - GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY, 16.0f); - - const client::render::texture stbi{"res/textures/atlas.png"}; - check_atlas(stbi); - const int face_size = stbi.width / 6; - - // 2D texture array, where our depth is our block face. - glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, face_size, face_size, - 6 * (stbi.height / face_size), 0, - stbi.channels == 3 ? GL_RGB : GL_RGBA, GL_UNSIGNED_BYTE, - nullptr); - - // Fill the 2D texture array. - // Because our image has multiple images on the x-axis and opengl - // expects a single image per axis, we must fill it in row by row. - const auto get_pixel_xy = [&stbi](const int x, const int y) { - return stbi.image + 4 * (y * stbi.width + x); - }; - for (int x = 0; x < 6; ++x) { - const int x_pixel = x * face_size; - - for (int y = 0; y < stbi.height / face_size; ++y) { - const int y_pixel = y * face_size; - - for (auto row = 0; row < face_size; ++row) { - glTexSubImage3D( - GL_TEXTURE_2D_ARRAY, 0, 0, row, x + y * 6, face_size, 1, - 1, GL_RGBA, GL_UNSIGNED_BYTE, - get_pixel_xy(x_pixel, row + y_pixel)); // pixel - } - } - } - - glGenerateMipmap(GL_TEXTURE_2D_ARRAY); - - return texture; - }; - const auto make_matrix = [&]() -> glm::mat4 { - const auto& proj = client::render::camera::get_proj(); - const auto& view = client::render::camera::get_view(); - return glm::translate(proj * view, glm::vec3{world_x, 0, world_z}); - }; - static client::render::program program{"res/shaders/face.vs", - "res/shaders/face.fs"}; - static const GLuint texture [[maybe_unused]] = make_texture(); - static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix"); - - glDisable(GL_BLEND); - glEnable(GL_DEPTH_TEST); - glUseProgram(program); - // Our choice of vao depends on which pass we're doing. - const auto [vao, elements] = [&pass, this]() -> std::pair<GLuint, GLuint> { - if (pass == pass::solid) { - return {this->glo->solid_vao, this->glo->solid_elements}; - } - return {this->glo->water_vao, this->glo->water_elements}; - }(); - glBindVertexArray(vao); - - glUniformMatrix4fv(u_matrix, 1, GL_FALSE, glm::value_ptr(make_matrix())); - - glDrawArrays(GL_TRIANGLES, 0, elements); -} - -// This function translates and rotates a set of vertices that describes a -// neutral face and also adds a vector to the texture coords. -std::array<chunk::glface, 6> -chunk::make_glfaces(const glface_args& args) noexcept { - - static constexpr std::array<glface, 6> glfaces = { - glface{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}}, - glface{{0.5f, -0.5f, 0.0f}, {1.0f, 1.0f, 0.0f}}, - glface{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}}, - glface{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}}, - glface{{-0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 0.0f}}, - glface{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}}}; - - // We have to be careful here not to rotate/translate a zero vector. - const glm::mat4 mtranslate = - args.translate == glm::vec3{} - ? glm::mat4{1.0f} - : glm::translate(glm::mat4{1.0f}, args.translate); - const glm::mat4 mrotate = - args.rotate_axis == glm::vec3{} - ? glm::mat4{1.0f} - : glm::rotate(glm::mat4{1.0f}, glm::radians(args.rotate_degrees), - args.rotate_axis); - - std::array<glface, 6> ret; - - std::ranges::transform(glfaces, std::begin(ret), [&](const auto f) { - auto face = f; // unfortunate copy - face.vertice = - glm::vec3(mtranslate * mrotate * glm::vec4{face.vertice, 1.0f}); - face.texture += args.texture_offset; - return face; - }); - - return ret; -} - -const chunk* chunk::get_neighbour(const chunk::map& chunks, - shared::math::coords offset) const noexcept { - const auto find_it = chunks.find(this->pos + offset); - if (find_it == std::end(chunks) || !find_it->second.has_value()) { - return nullptr; - } - return &((*find_it).second.value()); -} - -bool chunk::maybe_regenerate_glo(const chunk::map& chunks) noexcept { - // We need all surrounding chunks to make our vbo, this is why it's called - // "maybe" regenerate vbo. - const auto chunk_forward = this->get_neighbour(chunks, {0, 1}); - const auto chunk_backward = this->get_neighbour(chunks, {0, -1}); - const auto chunk_right = this->get_neighbour(chunks, {1, 0}); - const auto chunk_left = this->get_neighbour(chunks, {-1, 0}); - if (!chunk_forward || !chunk_left || !chunk_backward || !chunk_right) { - return false; - } - - static const auto [atlas_width, - atlas_height] = []() -> std::pair<int, int> { - const client::render::texture texture{"res/textures/atlas.png"}; - check_atlas(texture); - return {texture.width, texture.height}; - }(); - - // Single-axis-outside-chunk-bounds-allowed block access. - const auto get_outside_block = [&](const int x, const int y, - const int z) -> shared::world::block { - if (y < 0 || y >= shared::world::chunk::HEIGHT) { - return shared::world::block::type::air; - } else if (x >= shared::world::chunk::WIDTH) { - return chunk_right->get_block({x - WIDTH, y, z}); - } else if (x < 0) { - return chunk_left->get_block({x + WIDTH, y, z}); - } else if (z >= shared::world::chunk::WIDTH) { - return chunk_forward->get_block({x, y, z - WIDTH}); - } else if (z < 0) { - return chunk_backward->get_block({x, y, z + WIDTH}); - } - return this->get_block({x, y, z}); - }; - - // We fill up two vbos, one for each possible rendering pass. - std::vector<glface> solid_data; - std::vector<glface> water_data; - - // For all blocks in the chunk, check if its neighbours are air. If they - // are, it's possible that we can see the block, so add it to vertices. - // We need to read into the neighbours chunk occasionally. - for (auto x = 0; x < WIDTH; ++x) { - for (auto y = 0; y < HEIGHT; ++y) { - for (auto z = 0; z < WIDTH; ++z) { - const auto& block = this->get_block({x, y, z}); - const auto bv = shared::world::block::get_visibility(block); - - if (bv == shared::world::block::visibility::invisible) { - continue; - } - - const auto& front{get_outside_block(x, y, z + 1)}; - const auto& back{get_outside_block(x, y, z - 1)}; - const auto& right{get_outside_block(x + 1, y, z)}; - const auto& left{get_outside_block(x - 1, y, z)}; - const auto& up{get_outside_block(x, y + 1, z)}; - const auto& down{get_outside_block(x, y - 1, z)}; - - std::vector<glface> glfaces; - glfaces.reserve(6 * 6); - - const auto should_draw_face = [&bv](const auto& other) -> bool { - const auto ov = shared::world::block::get_visibility(other); - if (bv == shared::world::block::visibility::translucent && - ov == shared::world::block::visibility::translucent) { - return false; - } - return ov != shared::world::block::visibility::solid; - }; - // Special shrub block case, ugly I know. - if (block.type == shared::world::block::type::shrub || - block.type == shared::world::block::type::dead_shrub || - block.type == shared::world::block::type::snowy_shrub) { - static const auto front_shrub = - make_glfaces({.translate = {0.0f, 0.0f, 0.0f}, - .rotate_degrees = 45.0f, - .rotate_axis = {0.0f, 1.0f, 0.0f}, - .texture_offset = {0.0f, 0.0f, 0.0f}}); - static const auto right_shrub = - make_glfaces({.translate = {0.0f, 0.0f, 0.0f}, - .rotate_degrees = 135.0f, - .rotate_axis = {0.0f, 1.0f, 0.0f}, - .texture_offset = {0.0f, 0.0f, 0.0f}}); - static const auto back_shrub = - make_glfaces({.translate = {0.0f, 0.0f, 0.0f}, - .rotate_degrees = 225.0f, - .rotate_axis = {0.0f, 1.0f, 0.0f}, - .texture_offset = {0.0f, 0.0f, 0.0f}}); - static const auto left_shrub = - make_glfaces({.translate = {0.0f, 0.0f, 0.0f}, - .rotate_degrees = 315.0f, - .rotate_axis = {0.0f, 1.0f, 0.0f}, - .texture_offset = {0.0f, 0.0f, 0.0f}}); - - std::ranges::copy(front_shrub, std::back_inserter(glfaces)); - std::ranges::copy(right_shrub, std::back_inserter(glfaces)); - std::ranges::copy(back_shrub, std::back_inserter(glfaces)); - std::ranges::copy(left_shrub, std::back_inserter(glfaces)); - - } else { - if (should_draw_face(front)) { - static const auto front_faces = make_glfaces( - {.translate = {0.0f, 0.0f, 0.5f}, - .rotate_degrees = 0.0f, - .rotate_axis = {0.0f, 0.0f, 0.0f}, - .texture_offset = {0.0f, 0.0f, 0.0f}}); - std::ranges::copy(front_faces, - std::back_inserter(glfaces)); - } - if (should_draw_face(right)) { - static const auto right_faces = make_glfaces( - {.translate = {0.5f, 0.0f, 0.0f}, - .rotate_degrees = 90.0f, - .rotate_axis = {0.0f, 1.0f, 0.0f}, - .texture_offset = {0.0f, 0.0f, 1.0f}}); - std::ranges::copy(right_faces, - std::back_inserter(glfaces)); - } - if (should_draw_face(back)) { - static const auto back_faces = make_glfaces( - {.translate = {0.0f, 0.0f, -0.5f}, - .rotate_degrees = 180.0f, - .rotate_axis = {0.0f, 1.0f, 0.0f}, - .texture_offset = {0.0f, 0.0f, 2.0f}}); - std::ranges::copy(back_faces, - std::back_inserter(glfaces)); - } - if (should_draw_face(left)) { - static const auto left_faces = make_glfaces( - {.translate = {-0.5f, 0.0f, 0.0f}, - .rotate_degrees = 270.0f, - .rotate_axis = {0.0f, 1.0f, 0.0f}, - .texture_offset = {0.0f, 0.0f, 3.0f}}); - std::ranges::copy(left_faces, - std::back_inserter(glfaces)); - } - if (should_draw_face(up)) { - static const auto up_faces = make_glfaces( - {.translate = {0.0f, 0.5f, 0.0f}, - .rotate_degrees = -90.0f, - .rotate_axis = {1.0f, 0.0f, 0.0f}, - .texture_offset = {0.0f, 0.0f, 4.0f}}); - std::ranges::copy(up_faces, - std::back_inserter(glfaces)); - } - if (should_draw_face(down)) { - static const auto down_faces = make_glfaces( - {.translate = {0.0f, -0.5f, 0.0f}, - .rotate_degrees = 90.0f, - .rotate_axis = {1.0f, 0.0f, 0.0f}, - .texture_offset = {0.0f, 0.0f, 5.0f}}); - std::ranges::copy(down_faces, - std::back_inserter(glfaces)); - } - } - - // Move the block pos verts to its intended position. - // Move the block texture verts to fit in the atlas. - const glm::vec3 offset_vec3{x, y, z}; - const float tex_yoff = static_cast<float>(block.type) - 1.0f; - - const auto fix_face = [&, atlas_width = std::ref(atlas_width), - atlas_height = - std::ref(atlas_height)](auto& face) { - face.vertice += offset_vec3 + 0.5f; // move to origin too - face.texture.z += tex_yoff * 6.0f; - return face; - }; - - auto& vbo_dest = block.type == shared::world::block::type::water - ? water_data - : solid_data; - std::ranges::transform(glfaces, std::back_inserter(vbo_dest), - fix_face); - } - } - } - - const auto generate_vbo = [](const auto& data) -> GLuint { - GLuint vbo = 0; - glGenBuffers(1, &vbo); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, std::size(data) * sizeof(glface), - std::data(data), GL_STATIC_DRAW); - return vbo; - }; - const auto generate_vao = []() -> GLuint { - GLuint vao = 0; - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); - // position - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, sizeof(glm::vec3) / sizeof(float), GL_FLOAT, - GL_FALSE, sizeof(glface), nullptr); - // texture - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, sizeof(glm::vec3) / sizeof(float), GL_FLOAT, - GL_FALSE, sizeof(glface), - reinterpret_cast<void*>(sizeof(glm::vec3))); - return vao; - }; - // If we were to emplace glo with these there is no guarantee that each - // function will be called in order (at least, for g++ it isn't). Therefore - // we need to call them in order first. - const auto solid_vbo = generate_vbo(solid_data); - const auto solid_vao = generate_vao(); - const auto water_vbo = generate_vbo(water_data); - const auto water_vao = generate_vao(); - this->glo.emplace(std::size(solid_data), solid_vbo, solid_vao, - std::size(water_data), water_vbo, water_vao); - return true; -} - -// http://www.lighthouse3d.com/tutorials/view-frustum-culling/geometric-approach-testing-boxes/ -static bool box_in_frustum(const std::array<glm::vec3, 8>& points) noexcept { - const auto& frustum = client::render::camera::get_frustum(); - - for (const auto& plane : frustum) { - bool inside = false; - bool outside = false; - - for (const auto& point : points) { - const float distance = plane.x * point.x + plane.y * point.y + - plane.z * point.z + plane.w; - if (distance < 0.0f) { - outside = true; - } else { - inside = true; - } - - if (inside && outside) { - break; - } - } - - if (!inside) { - return false; - } - } - - return true; -}; - -static bool is_chunk_visible(const float world_x, - const float world_z) noexcept { - const std::array<glm::vec3, 8> box_vertices = - [&world_x, &world_z]() -> std::array<glm::vec3, 8> { - const float max_world_x = world_x + shared::world::chunk::WIDTH; - const float max_world_z = world_z + shared::world::chunk::WIDTH; - - return {glm::vec3{world_x, 0.0f, world_z}, - {max_world_x, 0.0f, world_z}, - {world_x, 0.0f, max_world_z}, - {max_world_x, 0.0f, max_world_z}, - {world_x, shared::world::chunk::HEIGHT, world_z}, - {max_world_x, shared::world::chunk::HEIGHT, world_z}, - {world_x, shared::world::chunk::HEIGHT, max_world_z}, - {max_world_x, shared::world::chunk::HEIGHT, max_world_z}}; - }(); - - return box_in_frustum(box_vertices); -} - -void chunk::draw(const chunk::map& chunks, const shared::player& lp, - const pass& pass) noexcept { - if (!this->glo.has_value() || this->should_regenerate_vbo) { - if (!maybe_regenerate_glo(chunks)) { - return; - } - this->should_regenerate_vbo = false; - } - - const auto [world_x, world_z] = [&lp, this]() -> std::pair<float, float> { - const float offset_x = static_cast<float>(this->pos.x - lp.chunk_pos.x); - const float offset_z = static_cast<float>(this->pos.z - lp.chunk_pos.z); - return {offset_x * chunk::WIDTH, offset_z * chunk::WIDTH}; - }(); - - if (!is_chunk_visible(world_x, world_z)) { - return; - } - - render(world_x, world_z, pass); -} - -} // namespace world -} // namespace client diff --git a/src/client/world/block.cc b/src/client/world/block.cc new file mode 100644 index 0000000..df3ddcd --- /dev/null +++ b/src/client/world/block.cc @@ -0,0 +1,117 @@ +#include "client/world/block.hh" + +namespace client { +namespace world { + +struct glvert_args { + glm::vec3 translate; + float rotate_degrees; + glm::vec3 rotate_axis; + glm::vec3 texture_offset; +}; +static block::glface_t make_glface(const glvert_args& args) noexcept { + static constexpr block::glface_t glverts = { + block::glvert{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}}, + block::glvert{{0.5f, -0.5f, 0.0f}, {1.0f, 1.0f, 0.0f}}, + block::glvert{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}}, + block::glvert{{0.5f, 0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}}, + block::glvert{{-0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 0.0f}}, + block::glvert{{-0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}}}; + + // We have to be careful here not to rotate/translate a zero vector. + constexpr auto zero = glm::vec3{}; + const glm::mat4 mtranslate = + args.translate == zero + ? glm::mat4{1.0f} + : glm::translate(glm::mat4{1.0f}, args.translate); + const glm::mat4 mrotate = + args.rotate_axis == zero + ? glm::mat4{1.0f} + : glm::rotate(glm::mat4{1.0f}, glm::radians(args.rotate_degrees), + args.rotate_axis); + + block::glface_t ret; + std::ranges::transform(glverts, std::begin(ret), [&](auto f) { + f.vertice = + glm::vec3(mtranslate * mrotate * glm::vec4{f.vertice, 1.0f}); + f.texture += args.texture_offset; + return f; + }); + return ret; +} + +static const block::glfaces_t& get_shrub_faces() noexcept { + static block::glfaces_t faces{ + make_glface({.translate = {0.0f, 0.0f, 0.0f}, + .rotate_degrees = 45.0f, + .rotate_axis = {0.0f, 1.0f, 0.0f}, + .texture_offset = {0.0f, 0.0f, 0.0f}}), + make_glface({.translate = {0.0f, 0.0f, 0.0f}, + .rotate_degrees = 135.0f, + .rotate_axis = {0.0f, 1.0f, 0.0f}, + .texture_offset = {0.0f, 0.0f, 0.0f}}), + make_glface({.translate = {0.0f, 0.0f, 0.0f}, + .rotate_degrees = 225.0f, + .rotate_axis = {0.0f, 1.0f, 0.0f}, + .texture_offset = {0.0f, 0.0f, 0.0f}}), + make_glface({.translate = {0.0f, 0.0f, 0.0f}, + .rotate_degrees = 315.0f, + .rotate_axis = {0.0f, 1.0f, 0.0f}, + .texture_offset = {0.0f, 0.0f, 0.0f}})}; + return faces; +} + +static const block::glfaces_t& get_cube_faces() noexcept { + static block::glfaces_t faces{ + make_glface({.translate = {0.0f, 0.0f, 0.5f}, + .rotate_degrees = 0.0f, + .rotate_axis = {0.0f, 0.0f, 0.0f}, + .texture_offset = {0.0f, 0.0f, 0.0f}}), + make_glface({.translate = {0.5f, 0.0f, 0.0f}, + .rotate_degrees = 90.0f, + .rotate_axis = {0.0f, 1.0f, 0.0f}, + .texture_offset = {0.0f, 0.0f, 1.0f}}), + make_glface({.translate = {0.0f, 0.0f, -0.5f}, + .rotate_degrees = 180.0f, + .rotate_axis = {0.0f, 1.0f, 0.0f}, + .texture_offset = {0.0f, 0.0f, 2.0f}}), + make_glface({.translate = {-0.5f, 0.0f, 0.0f}, + .rotate_degrees = 270.0f, + .rotate_axis = {0.0f, 1.0f, 0.0f}, + .texture_offset = {0.0f, 0.0f, 3.0f}}), + make_glface({.translate = {0.0f, 0.5f, 0.0f}, + .rotate_degrees = -90.0f, + .rotate_axis = {1.0f, 0.0f, 0.0f}, + .texture_offset = {0.0f, 0.0f, 4.0f}}), + make_glface({.translate = {0.0f, -0.5f, 0.0f}, + .rotate_degrees = 90.0f, + .rotate_axis = {1.0f, 0.0f, 0.0f}, + .texture_offset = {0.0f, 0.0f, 5.0f}}), + }; + return faces; +} + +enum block::draw_type +block::get_draw_type(const enum block::type& type) noexcept { + using t = enum shared::world::block::type; + switch (type) { + case t::dead_shrub: + case t::shrub: + case t::snowy_shrub: + return draw_type::custom; + default: + return draw_type::block; + } +} + +const block::glfaces_t& +block::get_glfaces(const enum block::type& type) noexcept { + const auto draw_type = get_draw_type(type); + if (draw_type == block::draw_type::custom) { + return get_shrub_faces(); + } + return get_cube_faces(); +} + +} // namespace world +} // namespace client diff --git a/src/client/world/block.hh b/src/client/world/block.hh new file mode 100644 index 0000000..9dfe303 --- /dev/null +++ b/src/client/world/block.hh @@ -0,0 +1,53 @@ +#ifndef CLIENT_WORLD_BLOCK_HH_ +#define CLIENT_WORLD_BLOCK_HH_ + +#include <algorithm> +#include <array> +#include <vector> + +#include <glm/glm.hpp> +#include <glm/gtc/matrix_access.hpp> +#include <glm/gtc/matrix_transform.hpp> + +#include "shared/world/block.hh" + +namespace client { +namespace world { + +// Doesn't add any data, just information for rendering. +class block : public shared::world::block { +public: + struct glvert { + glm::vec3 vertice; + glm::vec3 texture; + }; + using glface_t = std::array<glvert, 6>; // array of verts (a face) + using glfaces_t = std::vector<glface_t>; // vector of faces (a block) + + // Render types refer to how the block should be culled when making the vbo. + enum class draw_type { + block, // face testing + custom, // no testing + }; + +public: + static enum draw_type get_draw_type(const enum block::type& type) noexcept; + static const glfaces_t& get_glfaces(const enum block::type& type) noexcept; + +public: + template <typename... Args> + block(Args&&... args) noexcept : block(std::forward<Args>(args)...) {} + +public: + enum draw_type get_draw_type() const noexcept { + return get_draw_type(this->type); + } + const glfaces_t& get_glfaces() const noexcept { + return get_glfaces(this->type); + } +}; + +} // namespace world +} // namespace client + +#endif diff --git a/src/client/world/chunk.cc b/src/client/world/chunk.cc new file mode 100644 index 0000000..99720cb --- /dev/null +++ b/src/client/world/chunk.cc @@ -0,0 +1,255 @@ +#include "client/world/chunk.hh" + +namespace client { +namespace world { + +void chunk::render(const float world_x, const float world_z, + const pass& pass) noexcept { + const auto make_matrix = [&]() -> glm::mat4 { + const auto& proj = client::render::camera::get_proj(); + const auto& view = client::render::camera::get_view(); + return glm::translate(proj * view, glm::vec3{world_x, 0, world_z}); + }; + static client::render::program program{"res/shaders/face.vs", + "res/shaders/face.fs"}; + static const GLint u_matrix = glGetUniformLocation(program, "_u_matrix"); + + const GLuint texture [[maybe_unused]] = client::render::get_texture_atlas(); + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + glUseProgram(program); + // Our choice of vao depends on which pass we're doing. + const auto [vao, elements] = [&pass, this]() -> std::pair<GLuint, GLuint> { + if (pass == pass::solid) { + return {this->glo->solid_vao, this->glo->solid_elements}; + } + return {this->glo->water_vao, this->glo->water_elements}; + }(); + glBindVertexArray(vao); + + glUniformMatrix4fv(u_matrix, 1, GL_FALSE, glm::value_ptr(make_matrix())); + + glDrawArrays(GL_TRIANGLES, 0, elements); +} + +const chunk* chunk::get_neighbour(const chunks_t& chunks, + shared::math::coords offset) const noexcept { + const auto find_it = chunks.find(this->pos + offset); + if (find_it == std::end(chunks) || !find_it->second.has_value()) { + return nullptr; + } + return &((*find_it).second.value()); +} + +bool chunk::maybe_regenerate_glo(const chunks_t& chunks) noexcept { + // We need all surrounding chunks to make our vbo, so early out with false + // if we can't do that yet. + const auto chunk_forward = this->get_neighbour(chunks, {0, 1}); + const auto chunk_backward = this->get_neighbour(chunks, {0, -1}); + const auto chunk_right = this->get_neighbour(chunks, {1, 0}); + const auto chunk_left = this->get_neighbour(chunks, {-1, 0}); + if (!chunk_forward || !chunk_left || !chunk_backward || !chunk_right) { + return false; + } + + // Single-axis-outside-chunk-bounds-allowed block access. + const auto get_outside_block = [&](const int x, const int y, + const int z) -> shared::world::block { + if (y < 0 || y >= shared::world::chunk::HEIGHT) { + return shared::world::block::type::air; + } else if (x >= shared::world::chunk::WIDTH) { + return chunk_right->get_block({x - WIDTH, y, z}); + } else if (x < 0) { + return chunk_left->get_block({x + WIDTH, y, z}); + } else if (z >= shared::world::chunk::WIDTH) { + return chunk_forward->get_block({x, y, z - WIDTH}); + } else if (z < 0) { + return chunk_backward->get_block({x, y, z + WIDTH}); + } + return this->get_block({x, y, z}); + }; + + // We fill up two vbos, one for each possible rendering pass. + std::vector<block::glvert> solid_data; + std::vector<block::glvert> water_data; + + // For all blocks in the chunk, check if its neighbours are air. If they + // are, it's possible that we can see the block, so add it to vertices. + // We need to read into the neighbours chunk occasionally. + for (auto x = 0; x < WIDTH; ++x) { + for (auto y = 0; y < HEIGHT; ++y) { + for (auto z = 0; z < WIDTH; ++z) { + const auto& block = this->get_block({x, y, z}); + const auto bv = shared::world::block::get_visibility(block); + + if (bv == shared::world::block::visibility::invisible) { + continue; + } + + std::vector<block::glvert> glverts; + glverts.reserve(6 * 6); + + const auto draw_type = block::get_draw_type(block.type); + + const block::glfaces_t& faces = block::get_glfaces(block.type); + if (draw_type == block::draw_type::block) { + const std::array<shared::world::block, 6> around{ + get_outside_block(x, y, z + 1), + get_outside_block(x + 1, y, z), + get_outside_block(x, y, z - 1), + get_outside_block(x - 1, y, z), + get_outside_block(x, y + 1, z), + get_outside_block(x, y - 1, z), + }; + + for (auto i = 0ul; i < std::size(faces); ++i) { + const auto ov = block::get_visibility(around[i]); + if (bv == block::visibility::translucent && + ov == block::visibility::translucent) { + continue; + } + if (ov == block::visibility::solid) { + continue; + } + + std::ranges::copy(faces[i], + std::back_inserter(glverts)); + } + } else if (draw_type == block::draw_type::custom) { + for (const auto& face : faces) { + std::ranges::copy(face, std::back_inserter(glverts)); + } + } + + // Move the block pos verts to its intended position. + // Move the block texture verts to fit in the atlas. + const glm::vec3 offset_vec3{x, y, z}; + const float tex_yoff = static_cast<float>(block.type) - 1.0f; + const auto fix_face = [&](auto& face) { + face.vertice += offset_vec3 + 0.5f; // move to origin too + face.texture.z += tex_yoff * 6.0f; + return face; + }; + + auto& vbo_dest = block.type == shared::world::block::type::water + ? water_data + : solid_data; + std::ranges::transform(glverts, std::back_inserter(vbo_dest), + fix_face); + } + } + } + + const auto generate_vbo = [](const auto& data) -> GLuint { + GLuint vbo = 0; + glGenBuffers(1, &vbo); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, std::size(data) * sizeof(block::glvert), + std::data(data), GL_STATIC_DRAW); + return vbo; + }; + const auto generate_vao = []() -> GLuint { + GLuint vao = 0; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + // position + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, sizeof(glm::vec3) / sizeof(float), GL_FLOAT, + GL_FALSE, sizeof(block::glvert), nullptr); + // texture + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, sizeof(glm::vec3) / sizeof(float), GL_FLOAT, + GL_FALSE, sizeof(block::glvert), + reinterpret_cast<void*>(sizeof(glm::vec3))); + return vao; + }; + // If we were to emplace glo with these there is no guarantee that each + // function will be called in order (at least, for g++ it isn't). Therefore + // we need to call them in order first. + const auto solid_vbo = generate_vbo(solid_data); + const auto solid_vao = generate_vao(); + const auto water_vbo = generate_vbo(water_data); + const auto water_vao = generate_vao(); + this->glo.emplace(std::size(solid_data), solid_vbo, solid_vao, + std::size(water_data), water_vbo, water_vao); + return true; +} + +// http://www.lighthouse3d.com/tutorials/view-frustum-culling/geometric-approach-testing-boxes/ +static bool box_in_frustum(const std::array<glm::vec3, 8>& points) noexcept { + const auto& frustum = client::render::camera::get_frustum(); + + for (const auto& plane : frustum) { + bool inside = false; + bool outside = false; + + for (const auto& point : points) { + const float distance = plane.x * point.x + plane.y * point.y + + plane.z * point.z + plane.w; + if (distance < 0.0f) { + outside = true; + } else { + inside = true; + } + + if (inside && outside) { + break; + } + } + + if (!inside) { + return false; + } + } + + return true; +}; + +static bool is_chunk_visible(const float world_x, + const float world_z) noexcept { + const std::array<glm::vec3, 8> box_vertices = + [&world_x, &world_z]() -> std::array<glm::vec3, 8> { + const float max_world_x = world_x + shared::world::chunk::WIDTH; + const float max_world_z = world_z + shared::world::chunk::WIDTH; + + return {glm::vec3{world_x, 0.0f, world_z}, + {max_world_x, 0.0f, world_z}, + {world_x, 0.0f, max_world_z}, + {max_world_x, 0.0f, max_world_z}, + {world_x, shared::world::chunk::HEIGHT, world_z}, + {max_world_x, shared::world::chunk::HEIGHT, world_z}, + {world_x, shared::world::chunk::HEIGHT, max_world_z}, + {max_world_x, shared::world::chunk::HEIGHT, max_world_z}}; + }(); + + return box_in_frustum(box_vertices); +} + +bool chunk::draw(const chunks_t& chunks, const shared::player& lp, + const pass& pass, const bool skip_regen) noexcept { + bool did_regen = false; + if (!this->glo.has_value() || this->should_regenerate_vbo) { + if (skip_regen || !maybe_regenerate_glo(chunks)) { + return false; + } + this->should_regenerate_vbo = false; + did_regen = true; + } + + const auto [world_x, world_z] = [&lp, this]() -> std::pair<float, float> { + const float offset_x = + static_cast<float>(this->pos.x - lp.get_chunk_pos().x); + const float offset_z = + static_cast<float>(this->pos.z - lp.get_chunk_pos().z); + return {offset_x * chunk::WIDTH, offset_z * chunk::WIDTH}; + }(); + + if (is_chunk_visible(world_x, world_z)) { + render(world_x, world_z, pass); + } + + return did_regen; +} + +} // namespace world +} // namespace client diff --git a/src/client/world.hh b/src/client/world/chunk.hh index 067f74c..5436525 100644 --- a/src/client/world.hh +++ b/src/client/world/chunk.hh @@ -1,5 +1,5 @@ -#ifndef CLIENT_WORLD_HH_ -#define CLIENT_WORLD_HH_ +#ifndef CLIENT_WORLD_CHUNK_HH_ +#define CLIENT_WORLD_CHUNK_HH_ #include <algorithm> #include <optional> @@ -9,19 +9,22 @@ #include "client/render/render.hh" #include "client/render/texture.hh" -#include "shared/player.hh" -#include "shared/world.hh" +#include "client/world/block.hh" +#include "shared/entity/player.hh" +#include "shared/world/chunk.hh" namespace client { namespace world { +class chunk; +using chunks_t = std::unordered_map<shared::math::coords, + std::optional<client::world::chunk>, + decltype(&shared::world::chunk::hash), + decltype(&shared::world::chunk::equal)>; + // client::world::chunk is a renderable shared::world::chunk. class chunk : public shared::world::chunk { public: - using map = std::unordered_map<shared::math::coords, - std::optional<client::world::chunk>, - decltype(&shared::world::chunk::hash), - decltype(&shared::world::chunk::equal)>; // Which part to draw when we call draw. enum class pass { solid, water }; @@ -53,35 +56,24 @@ private: std::optional<gl_objects> glo; private: - const chunk* get_neighbour(const chunk::map& chunks, + const chunk* get_neighbour(const chunks_t& chunks, shared::math::coords offset) const noexcept; - struct glface { - glm::vec3 vertice; - glm::vec3 texture; - }; - struct glface_args { - glm::vec3 translate; - float rotate_degrees; - glm::vec3 rotate_axis; - glm::vec3 texture_offset; - }; - static std::array<glface, 6> make_glfaces(const glface_args& args) noexcept; void render(const float x_offset, const float z_offset, const pass& pass) noexcept; - bool maybe_regenerate_glo(const chunk::map& chunks) noexcept; + bool maybe_regenerate_glo(const chunks_t& chunks) noexcept; public: template <typename... Args> chunk(Args&&... args) noexcept : shared::world::chunk(std::forward<Args>(args)...) {} + virtual ~chunk() noexcept = default; - void draw(const map& chunks, const shared::player& lp, - const pass& pass) noexcept; + // true if we regen'd, false otherwise + bool draw(const chunks_t& chunks, const shared::player& lp, + const pass& pass, const bool skip_regen = false) noexcept; - bool can_draw() const noexcept { - return this->glo.has_value(); - } + bool can_draw() const noexcept { return this->glo.has_value(); } }; } // namespace world diff --git a/src/client/world/world.dat b/src/client/world/world.dat Binary files differnew file mode 100644 index 0000000..e210d11 --- /dev/null +++ b/src/client/world/world.dat |
