diff options
Diffstat (limited to 'src/game')
187 files changed, 15593 insertions, 0 deletions
diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt new file mode 100644 index 0000000..c6c32bf --- /dev/null +++ b/src/game/CMakeLists.txt @@ -0,0 +1,11 @@ +if(BUILD_VCLIENT) + add_subdirectory(client) +endif() + +if(BUILD_VSERVER) + add_subdirectory(server) +endif() + +if(BUILD_VCLIENT OR BUILD_VSERVER) + add_subdirectory(shared) +endif() diff --git a/src/game/client/CMakeLists.txt b/src/game/client/CMakeLists.txt new file mode 100644 index 0000000..ac40148 --- /dev/null +++ b/src/game/client/CMakeLists.txt @@ -0,0 +1,126 @@ +add_executable(vclient + "${CMAKE_CURRENT_LIST_DIR}/background.cc" + "${CMAKE_CURRENT_LIST_DIR}/background.hh" + "${CMAKE_CURRENT_LIST_DIR}/bother.cc" + "${CMAKE_CURRENT_LIST_DIR}/bother.hh" + "${CMAKE_CURRENT_LIST_DIR}/camera.cc" + "${CMAKE_CURRENT_LIST_DIR}/camera.hh" + "${CMAKE_CURRENT_LIST_DIR}/chat.cc" + "${CMAKE_CURRENT_LIST_DIR}/chat.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk_mesher.cc" + "${CMAKE_CURRENT_LIST_DIR}/chunk_mesher.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk_quad.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk_renderer.cc" + "${CMAKE_CURRENT_LIST_DIR}/chunk_renderer.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk_vbo.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk_visibility.cc" + "${CMAKE_CURRENT_LIST_DIR}/chunk_visibility.hh" + "${CMAKE_CURRENT_LIST_DIR}/const.hh" + "${CMAKE_CURRENT_LIST_DIR}/crosshair.cc" + "${CMAKE_CURRENT_LIST_DIR}/crosshair.hh" + "${CMAKE_CURRENT_LIST_DIR}/direct_connection.cc" + "${CMAKE_CURRENT_LIST_DIR}/direct_connection.hh" + "${CMAKE_CURRENT_LIST_DIR}/experiments.cc" + "${CMAKE_CURRENT_LIST_DIR}/experiments.hh" + "${CMAKE_CURRENT_LIST_DIR}/factory.cc" + "${CMAKE_CURRENT_LIST_DIR}/factory.hh" + "${CMAKE_CURRENT_LIST_DIR}/game.cc" + "${CMAKE_CURRENT_LIST_DIR}/game.hh" + "${CMAKE_CURRENT_LIST_DIR}/gamepad_axis.cc" + "${CMAKE_CURRENT_LIST_DIR}/gamepad_axis.hh" + "${CMAKE_CURRENT_LIST_DIR}/gamepad_button.cc" + "${CMAKE_CURRENT_LIST_DIR}/gamepad_button.hh" + "${CMAKE_CURRENT_LIST_DIR}/gamepad.cc" + "${CMAKE_CURRENT_LIST_DIR}/gamepad.hh" + "${CMAKE_CURRENT_LIST_DIR}/glfw.hh" + "${CMAKE_CURRENT_LIST_DIR}/globals.cc" + "${CMAKE_CURRENT_LIST_DIR}/globals.hh" + "${CMAKE_CURRENT_LIST_DIR}/gui_screen.hh" + "${CMAKE_CURRENT_LIST_DIR}/hotbar.cc" + "${CMAKE_CURRENT_LIST_DIR}/hotbar.hh" + "${CMAKE_CURRENT_LIST_DIR}/imdraw_ext.cc" + "${CMAKE_CURRENT_LIST_DIR}/imdraw_ext.hh" + "${CMAKE_CURRENT_LIST_DIR}/interpolation.cc" + "${CMAKE_CURRENT_LIST_DIR}/interpolation.hh" + "${CMAKE_CURRENT_LIST_DIR}/keybind.cc" + "${CMAKE_CURRENT_LIST_DIR}/keybind.hh" + "${CMAKE_CURRENT_LIST_DIR}/language.cc" + "${CMAKE_CURRENT_LIST_DIR}/language.hh" + "${CMAKE_CURRENT_LIST_DIR}/listener.cc" + "${CMAKE_CURRENT_LIST_DIR}/listener.hh" + "${CMAKE_CURRENT_LIST_DIR}/main_menu.cc" + "${CMAKE_CURRENT_LIST_DIR}/main_menu.hh" + "${CMAKE_CURRENT_LIST_DIR}/main.cc" + "${CMAKE_CURRENT_LIST_DIR}/message_box.cc" + "${CMAKE_CURRENT_LIST_DIR}/message_box.hh" + "${CMAKE_CURRENT_LIST_DIR}/metrics.cc" + "${CMAKE_CURRENT_LIST_DIR}/metrics.hh" + "${CMAKE_CURRENT_LIST_DIR}/outline.cc" + "${CMAKE_CURRENT_LIST_DIR}/outline.hh" + "${CMAKE_CURRENT_LIST_DIR}/pch.hh" + "${CMAKE_CURRENT_LIST_DIR}/play_menu.cc" + "${CMAKE_CURRENT_LIST_DIR}/play_menu.hh" + "${CMAKE_CURRENT_LIST_DIR}/player_look.cc" + "${CMAKE_CURRENT_LIST_DIR}/player_look.hh" + "${CMAKE_CURRENT_LIST_DIR}/player_move.cc" + "${CMAKE_CURRENT_LIST_DIR}/player_move.hh" + "${CMAKE_CURRENT_LIST_DIR}/player_target.cc" + "${CMAKE_CURRENT_LIST_DIR}/player_target.hh" + "${CMAKE_CURRENT_LIST_DIR}/program.cc" + "${CMAKE_CURRENT_LIST_DIR}/program.hh" + "${CMAKE_CURRENT_LIST_DIR}/progress_bar.cc" + "${CMAKE_CURRENT_LIST_DIR}/progress_bar.hh" + "${CMAKE_CURRENT_LIST_DIR}/receive.cc" + "${CMAKE_CURRENT_LIST_DIR}/receive.hh" + "${CMAKE_CURRENT_LIST_DIR}/scoreboard.cc" + "${CMAKE_CURRENT_LIST_DIR}/scoreboard.hh" + "${CMAKE_CURRENT_LIST_DIR}/screenshot.cc" + "${CMAKE_CURRENT_LIST_DIR}/screenshot.hh" + "${CMAKE_CURRENT_LIST_DIR}/session.cc" + "${CMAKE_CURRENT_LIST_DIR}/session.hh" + "${CMAKE_CURRENT_LIST_DIR}/settings.cc" + "${CMAKE_CURRENT_LIST_DIR}/settings.hh" + "${CMAKE_CURRENT_LIST_DIR}/skybox.cc" + "${CMAKE_CURRENT_LIST_DIR}/skybox.hh" + "${CMAKE_CURRENT_LIST_DIR}/sound_effect.cc" + "${CMAKE_CURRENT_LIST_DIR}/sound_effect.hh" + "${CMAKE_CURRENT_LIST_DIR}/sound_emitter.cc" + "${CMAKE_CURRENT_LIST_DIR}/sound_emitter.hh" + "${CMAKE_CURRENT_LIST_DIR}/sound.cc" + "${CMAKE_CURRENT_LIST_DIR}/sound.hh" + "${CMAKE_CURRENT_LIST_DIR}/splash.cc" + "${CMAKE_CURRENT_LIST_DIR}/splash.hh" + "${CMAKE_CURRENT_LIST_DIR}/status_lines.cc" + "${CMAKE_CURRENT_LIST_DIR}/status_lines.hh" + "${CMAKE_CURRENT_LIST_DIR}/texture_gui.cc" + "${CMAKE_CURRENT_LIST_DIR}/texture_gui.hh" + "${CMAKE_CURRENT_LIST_DIR}/toggles.cc" + "${CMAKE_CURRENT_LIST_DIR}/toggles.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel_anims.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel_anims.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel_atlas.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel_atlas.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel_sounds.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel_sounds.hh" + "${CMAKE_CURRENT_LIST_DIR}/window_title.cc" + "${CMAKE_CURRENT_LIST_DIR}/window_title.hh") +target_compile_features(vclient PUBLIC cxx_std_20) +target_compile_definitions(vclient PUBLIC GLFW_INCLUDE_NONE) +target_include_directories(vclient PUBLIC "${DEPS_INCLUDE_DIR}") +target_include_directories(vclient PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_include_directories(vclient PRIVATE "${PROJECT_SOURCE_DIR}/src/game") +target_precompile_headers(vclient PRIVATE "${CMAKE_CURRENT_LIST_DIR}/pch.hh") +target_link_libraries(vclient PUBLIC shared dr_libs glad glfw3 imgui imgui_glfw imgui_opengl3 salad) + +if(WIN32 AND MSVC) + # GLFW defines APIENTRY and ENet includes + # Windows API headers which also define APIENTRY + target_compile_options(vclient PRIVATE /wd4005) +endif() + +if(WIN32) + enable_language(RC) + target_sources(vclient PRIVATE "${CMAKE_CURRENT_LIST_DIR}/vclient.rc") +endif() + +install(TARGETS vclient RUNTIME DESTINATION ".") diff --git a/src/game/client/background.cc b/src/game/client/background.cc new file mode 100644 index 0000000..415b63c --- /dev/null +++ b/src/game/client/background.cc @@ -0,0 +1,37 @@ +#include "client/pch.hh" + +#include "client/background.hh" + +#include "core/constexpr.hh" +#include "core/resource.hh" + +#include "client/globals.hh" +#include "client/texture_gui.hh" + +static resource_ptr<TextureGUI> texture; + +void background::init(void) +{ + texture = resource::load<TextureGUI>("textures/gui/background.png", TEXTURE_GUI_LOAD_VFLIP); + + if(texture == nullptr) { + spdlog::critical("background: texture load failed"); + std::terminate(); + } +} + +void background::deinit(void) +{ + texture = nullptr; +} + +void background::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetBackgroundDrawList(); + + auto scaled_width = 0.75f * static_cast<float>(globals::width / globals::gui_scale); + auto scaled_height = 0.75f * static_cast<float>(globals::height / globals::gui_scale); + auto scale_uv = ImVec2(scaled_width / static_cast<float>(texture->size.x), scaled_height / static_cast<float>(texture->size.y)); + draw_list->AddImage(texture->handle, ImVec2(0.0f, 0.0f), viewport->Size, ImVec2(0.0f, 0.0f), scale_uv); +} diff --git a/src/game/client/background.hh b/src/game/client/background.hh new file mode 100644 index 0000000..634caf5 --- /dev/null +++ b/src/game/client/background.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_BACKGROUND_HH +#define CLIENT_BACKGROUND_HH 1 +#pragma once + +namespace background +{ +void init(void); +void deinit(void); +void layout(void); +} // namespace background + +#endif /* CLIENT_BACKGROUND_HH */ diff --git a/src/game/client/bother.cc b/src/game/client/bother.cc new file mode 100644 index 0000000..8bd7182 --- /dev/null +++ b/src/game/client/bother.cc @@ -0,0 +1,163 @@ +#include "client/pch.hh" + +#include "client/bother.hh" + +#include "shared/protocol.hh" + +#include "client/globals.hh" + +// Maximum amount of peers used for bothering +constexpr static std::size_t BOTHER_PEERS = 4; + +struct BotherQueueItem final { + unsigned int identity; + std::string hostname; + std::uint16_t port; +}; + +static ENetHost* bother_host; +static entt::dispatcher bother_dispatcher; +static std::unordered_set<unsigned int> bother_set; +static std::deque<BotherQueueItem> bother_queue; + +static void on_status_response_packet(const protocol::StatusResponse& packet) +{ + auto identity = static_cast<unsigned int>(reinterpret_cast<std::uintptr_t>(packet.peer->data)); + + bother_set.erase(identity); + + BotherResponseEvent event; + event.identity = identity; + event.is_server_unreachable = false; + event.protocol_version = packet.version; + event.num_players = packet.num_players; + event.max_players = packet.max_players; + event.motd = packet.motd; + globals::dispatcher.trigger(event); + + enet_peer_disconnect(packet.peer, protocol::CHANNEL); +} + +void bother::init(void) +{ + bother_host = enet_host_create(nullptr, BOTHER_PEERS, 1, 0, 0); + bother_dispatcher.clear(); + bother_set.clear(); + + bother_dispatcher.sink<protocol::StatusResponse>().connect<&on_status_response_packet>(); +} + +void bother::deinit(void) +{ + enet_host_destroy(bother_host); + bother_dispatcher.clear(); + bother_set.clear(); +} + +void bother::update_late(void) +{ + unsigned int free_peers = 0U; + + // Figure out how much times we can call + // enet_host_connect and reallistically succeed + for(unsigned int i = 0U; i < bother_host->peerCount; ++i) { + if(bother_host->peers[i].state != ENET_PEER_STATE_DISCONNECTED) { + continue; + } + + free_peers += 1U; + } + + for(unsigned int i = 0U; (i < free_peers) && bother_queue.size(); ++i) { + const auto& item = bother_queue.front(); + + ENetAddress address; + enet_address_set_host(&address, item.hostname.c_str()); + address.port = enet_uint16(item.port); + + if(auto peer = enet_host_connect(bother_host, &address, 1, 0)) { + peer->data = reinterpret_cast<void*>(static_cast<std::uintptr_t>(item.identity)); + bother_set.insert(item.identity); + enet_host_flush(bother_host); + } + + bother_queue.pop_front(); + } + + ENetEvent enet_event; + + if(0 < enet_host_service(bother_host, &enet_event, 0)) { + if(enet_event.type == ENET_EVENT_TYPE_CONNECT) { + protocol::StatusRequest packet; + packet.version = protocol::VERSION; + protocol::send(enet_event.peer, protocol::encode(packet)); + return; + } + + if(enet_event.type == ENET_EVENT_TYPE_RECEIVE) { + protocol::decode(bother_dispatcher, enet_event.packet, enet_event.peer); + enet_packet_destroy(enet_event.packet); + return; + } + + if(enet_event.type == ENET_EVENT_TYPE_DISCONNECT) { + auto identity = static_cast<unsigned int>(reinterpret_cast<std::uintptr_t>(enet_event.peer->data)); + + if(bother_set.count(identity)) { + BotherResponseEvent event; + event.identity = identity; + event.is_server_unreachable = true; + globals::dispatcher.trigger(event); + } + + bother_set.erase(identity); + + return; + } + } +} + +void bother::ping(unsigned int identity, const char* host, std::uint16_t port) +{ + if(bother_set.count(identity)) { + // Already in the process + return; + } + + for(const auto& item : bother_queue) { + if(item.identity == identity) { + // Already in the queue + return; + } + } + + BotherQueueItem item; + item.identity = identity; + item.hostname = std::string(host); + item.port = port; + + bother_queue.push_back(item); +} + +void bother::cancel(unsigned int identity) +{ + bother_set.erase(identity); + + auto item = bother_queue.cbegin(); + + while(item != bother_queue.cend()) { + if(item->identity == identity) { + item = bother_queue.erase(item); + continue; + } + + item = std::next(item); + } + + for(unsigned int i = 0U; i < bother_host->peerCount; ++i) { + if(bother_host->peers[i].data == reinterpret_cast<void*>(static_cast<std::uintptr_t>(identity))) { + enet_peer_reset(&bother_host->peers[i]); + break; + } + } +} diff --git a/src/game/client/bother.hh b/src/game/client/bother.hh new file mode 100644 index 0000000..5fbf247 --- /dev/null +++ b/src/game/client/bother.hh @@ -0,0 +1,23 @@ +#ifndef CLIENT_BOTHER_HH +#define CLIENT_BOTHER_HH 1 +#pragma once + +struct BotherResponseEvent final { + unsigned int identity; + bool is_server_unreachable; + std::uint32_t protocol_version; + std::uint16_t num_players; + std::uint16_t max_players; + std::string motd; +}; + +namespace bother +{ +void init(void); +void deinit(void); +void update_late(void); +void ping(unsigned int identity, const char* host, std::uint16_t port); +void cancel(unsigned int identity); +} // namespace bother + +#endif /* CLIENT_BOTHER_HH */ diff --git a/src/game/client/camera.cc b/src/game/client/camera.cc new file mode 100644 index 0000000..724ae66 --- /dev/null +++ b/src/game/client/camera.cc @@ -0,0 +1,107 @@ +#include "client/pch.hh" + +#include "client/camera.hh" + +#include "core/angles.hh" +#include "core/config.hh" + +#include "shared/dimension.hh" +#include "shared/head.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" + +#include "client/const.hh" +#include "client/globals.hh" +#include "client/player_move.hh" +#include "client/session.hh" +#include "client/settings.hh" +#include "client/toggles.hh" + +ConfigFloat camera::roll_angle(2.0f, 0.0f, 4.0f); +ConfigFloat camera::vertical_fov(90.0f, 45.0f, 110.0f); +ConfigUnsigned camera::view_distance(16U, 4U, 32U); + +glm::fvec3 camera::angles; +glm::fvec3 camera::direction; +glm::fmat4x4 camera::matrix; +chunk_pos camera::position_chunk; +glm::fvec3 camera::position_local; + +static void reset_camera(void) +{ + camera::angles = glm::fvec3(0.0f, 0.0f, 0.0f); + camera::direction = DIR_FORWARD<float>; + camera::matrix = glm::identity<glm::fmat4x4>(); + camera::position_chunk = chunk_pos(0, 0, 0); + camera::position_local = glm::fvec3(0.0f, 0.0f, 0.0f); +} + +// Gracefully contributed by PQCraft himself in 2024 +// making PlatinumSrc and Voxelius kind of related to each other +static glm::fmat4x4 platinumsrc_viewmatrix(const glm::fvec3& position, const glm::fvec3& angles) +{ + glm::fvec3 forward, up; + cxangles::vectors(angles, &forward, nullptr, &up); + + auto result = glm::identity<glm::fmat4x4>(); + result[0][0] = forward.y * up.z - forward.z * up.y; + result[1][0] = forward.z * up.x - forward.x * up.z; + result[2][0] = forward.x * up.y - forward.y * up.x; + result[3][0] = -result[0][0] * position.x - result[1][0] * position.y - result[2][0] * position.z; + result[0][1] = up.x; + result[1][1] = up.y; + result[2][1] = up.z; + result[3][1] = -up.x * position.x - up.y * position.y - up.z * position.z; + result[0][2] = -forward.x; + result[1][2] = -forward.y; + result[2][2] = -forward.z; + result[3][2] = forward.x * position.x + forward.y * position.y + forward.z * position.z; + return result; +} + +void camera::init(void) +{ + globals::client_config.add_value("camera.roll_angle", camera::roll_angle); + globals::client_config.add_value("camera.vertical_fov", camera::vertical_fov); + globals::client_config.add_value("camera.view_distance", camera::view_distance); + + settings::add_slider(1, camera::vertical_fov, settings_location::GENERAL, "camera.vertical_fov", true, "%.0f"); + settings::add_slider(0, camera::view_distance, settings_location::VIDEO, "camera.view_distance", false); + settings::add_slider(10, camera::roll_angle, settings_location::VIDEO, "camera.roll_angle", true, "%.01f"); + + reset_camera(); +} + +void camera::update(void) +{ + if(!session::is_ingame()) { + reset_camera(); + return; + } + + const auto& head = globals::dimension->entities.get<HeadComponentIntr>(globals::player); + const auto& transform = globals::dimension->entities.get<TransformComponentIntr>(globals::player); + const auto& velocity = globals::dimension->entities.get<VelocityComponent>(globals::player); + + camera::angles = transform.angles + head.angles; + camera::position_chunk = transform.chunk; + camera::position_local = transform.local + head.offset; + + glm::fvec3 right_vector, up_vector; + cxangles::vectors(camera::angles, &camera::direction, &right_vector, &up_vector); + + auto client_angles = camera::angles; + + if(!toggles::get(TOGGLE_PM_FLIGHT)) { + // Apply the quake-like view rolling + client_angles[2] = vx::radians(-camera::roll_angle.get_value() * glm::dot(velocity.value / PMOVE_MAX_SPEED_GROUND, right_vector)); + } + + const auto z_near = 0.01f; + const auto z_far = 1.25f * static_cast<float>(CHUNK_SIZE * camera::view_distance.get_value()); + + auto proj = glm::perspective(vx::radians(camera::vertical_fov.get_value()), globals::aspect, z_near, z_far); + auto view = platinumsrc_viewmatrix(camera::position_local, client_angles); + + camera::matrix = proj * view; +} diff --git a/src/game/client/camera.hh b/src/game/client/camera.hh new file mode 100644 index 0000000..9718720 --- /dev/null +++ b/src/game/client/camera.hh @@ -0,0 +1,32 @@ +#ifndef CLIENT_CAMERA_HH +#define CLIENT_CAMERA_HH 1 +#pragma once + +#include "shared/types.hh" + +class ConfigFloat; +class ConfigUnsigned; + +namespace camera +{ +extern ConfigFloat roll_angle; +extern ConfigFloat vertical_fov; +extern ConfigUnsigned view_distance; +} // namespace camera + +namespace camera +{ +extern glm::fvec3 angles; +extern glm::fvec3 direction; +extern glm::fmat4x4 matrix; +extern chunk_pos position_chunk; +extern glm::fvec3 position_local; +} // namespace camera + +namespace camera +{ +void init(void); +void update(void); +} // namespace camera + +#endif /* CLIENT_CAMERA_HH */ diff --git a/src/game/client/chat.cc b/src/game/client/chat.cc new file mode 100644 index 0000000..0a0c75d --- /dev/null +++ b/src/game/client/chat.cc @@ -0,0 +1,260 @@ +#include "client/pch.hh" + +#include "client/chat.hh" + +#include "core/config.hh" +#include "core/resource.hh" +#include "core/strtools.hh" + +#include "shared/protocol.hh" + +#include "client/game.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/keybind.hh" +#include "client/language.hh" +#include "client/session.hh" +#include "client/settings.hh" +#include "client/sound.hh" +#include "client/sound_effect.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; +constexpr static unsigned int MAX_HISTORY_SIZE = 128U; + +struct GuiChatMessage final { + std::uint64_t spawn; + std::string text; + ImVec4 color; +}; + +static ConfigKeyBind key_chat(GLFW_KEY_ENTER); +static ConfigUnsigned history_size(32U, 0U, MAX_HISTORY_SIZE); + +static std::deque<GuiChatMessage> history; +static std::string chat_input; +static bool needs_focus; + +static resource_ptr<SoundEffect> sfx_chat_message; + +static void append_text_message(const std::string& sender, const std::string& text) +{ + GuiChatMessage message; + message.spawn = globals::curtime; + message.text = std::format("<{}> {}", sender, text); + message.color = ImGui::GetStyleColorVec4(ImGuiCol_Text); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} + +static void append_player_join(const std::string& sender) +{ + GuiChatMessage message; + message.spawn = globals::curtime; + message.text = std::format("{} {}", sender, language::resolve("chat.client_join")); + message.color = ImGui::GetStyleColorVec4(ImGuiCol_DragDropTarget); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} + +static void append_player_leave(const std::string& sender, const std::string& reason) +{ + GuiChatMessage message; + message.spawn = globals::curtime; + message.text = std::format("{} {} ({})", sender, language::resolve("chat.client_left"), language::resolve(reason.c_str())); + message.color = ImGui::GetStyleColorVec4(ImGuiCol_DragDropTarget); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} + +static void on_chat_message_packet(const protocol::ChatMessage& packet) +{ + if(packet.type == protocol::ChatMessage::TEXT_MESSAGE) { + append_text_message(packet.sender, packet.message); + return; + } + + if(packet.type == protocol::ChatMessage::PLAYER_JOIN) { + append_player_join(packet.sender); + return; + } + + if(packet.type == protocol::ChatMessage::PLAYER_LEAVE) { + append_player_leave(packet.sender, packet.message); + return; + } +} + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if(event.action == GLFW_PRESS) { + if((event.key == GLFW_KEY_ENTER) && (globals::gui_screen == GUI_CHAT)) { + if(!strtools::is_whitespace(chat_input)) { + protocol::ChatMessage packet; + packet.type = protocol::ChatMessage::TEXT_MESSAGE; + packet.sender = client_game::username.get(); + packet.message = chat_input; + + protocol::send(session::peer, protocol::encode(packet)); + } + + globals::gui_screen = GUI_SCREEN_NONE; + + chat_input.clear(); + + return; + } + + if((event.key == GLFW_KEY_ESCAPE) && (globals::gui_screen == GUI_CHAT)) { + globals::gui_screen = GUI_SCREEN_NONE; + return; + } + + if(key_chat.equals(event.key) && !globals::gui_screen) { + globals::gui_screen = GUI_CHAT; + needs_focus = true; + return; + } + } +} + +void client_chat::init(void) +{ + globals::client_config.add_value("chat.key", key_chat); + globals::client_config.add_value("chat.history_size", history_size); + + settings::add_keybind(2, key_chat, settings_location::KEYBOARD_MISC, "key.chat"); + settings::add_slider(1, history_size, settings_location::VIDEO_GUI, "chat.history_size", false); + + globals::dispatcher.sink<protocol::ChatMessage>().connect<&on_chat_message_packet>(); + globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>(); + + sfx_chat_message = resource::load<SoundEffect>("sounds/ui/chat_message.wav"); +} + +void client_chat::init_late(void) +{ +} + +void client_chat::deinit(void) +{ + sfx_chat_message = nullptr; +} + +void client_chat::update(void) +{ + while(history.size() > history_size.get_value()) { + history.pop_front(); + } +} + +void client_chat::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto window_start = ImVec2(0.0f, 0.0f); + auto window_size = ImVec2(0.75f * viewport->Size.x, viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + ImGui::PushFont(globals::font_chat); + + if(!ImGui::Begin("###chat", nullptr, WINDOW_FLAGS)) { + ImGui::End(); + return; + } + + auto& padding = ImGui::GetStyle().FramePadding; + auto& spacing = ImGui::GetStyle().ItemSpacing; + auto font = ImGui::GetFont(); + + auto draw_list = ImGui::GetWindowDrawList(); + + // The text input widget occupies the bottom part + // of the chat window, we need to reserve some space for it + auto ypos = window_size.y - 2.5f * font->FontSize - 2.0f * padding.y - 2.0f * spacing.y; + + if(globals::gui_screen == GUI_CHAT) { + if(needs_focus) { + ImGui::SetKeyboardFocusHere(); + needs_focus = false; + } + + ImGui::SetNextItemWidth(window_size.x + 32.0f * padding.x); + ImGui::SetCursorScreenPos(ImVec2(padding.x, ypos)); + ImGui::InputText("###chat.input", &chat_input); + } + + if(!client_game::hide_hud && ((globals::gui_screen == GUI_SCREEN_NONE) || (globals::gui_screen == GUI_CHAT))) { + for(auto it = history.crbegin(); it < history.crend(); ++it) { + auto text_size = ImGui::CalcTextSize(it->text.c_str(), it->text.c_str() + it->text.size(), false, window_size.x); + auto rect_size = ImVec2(window_size.x, text_size.y + 2.0f * padding.y); + + auto rect_pos = ImVec2(padding.x, ypos - text_size.y - 2.0f * padding.y); + auto rect_end = ImVec2(rect_pos.x + rect_size.x, rect_pos.y + rect_size.y); + auto text_pos = ImVec2(rect_pos.x + padding.x, rect_pos.y + padding.y); + + auto fadeout_seconds = 10.0f; + auto fadeout = std::exp(-1.0f * std::pow(1.0e-6 * static_cast<float>(globals::curtime - it->spawn) / fadeout_seconds, 10.0f)); + + float rect_alpha; + float text_alpha; + + if(globals::gui_screen == GUI_CHAT) { + rect_alpha = 0.75f; + text_alpha = 1.00f; + } else { + rect_alpha = 0.50f * fadeout; + text_alpha = 1.00f * fadeout; + } + + auto rect_col = ImGui::GetColorU32(ImGuiCol_FrameBg, rect_alpha); + auto text_col = ImGui::GetColorU32(ImVec4(it->color.x, it->color.y, it->color.z, it->color.w * text_alpha)); + + draw_list->AddRectFilled(rect_pos, rect_end, rect_col); + draw_list->AddText( + font, font->FontSize, text_pos, text_col, it->text.c_str(), it->text.c_str() + it->text.size(), window_size.x); + + ypos -= rect_size.y; + } + } + + ImGui::End(); + ImGui::PopFont(); +} + +void client_chat::clear(void) +{ + history.clear(); +} + +void client_chat::refresh_timings(void) +{ + for(auto it = history.begin(); it < history.end(); ++it) { + // Reset the spawn time so the fadeout timer + // is reset; SpawnPlayer handler might call this + it->spawn = globals::curtime; + } +} + +void client_chat::print(const std::string& text) +{ + GuiChatMessage message = {}; + message.spawn = globals::curtime; + message.text = text; + message.color = ImGui::GetStyleColorVec4(ImGuiCol_Text); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} diff --git a/src/game/client/chat.hh b/src/game/client/chat.hh new file mode 100644 index 0000000..f2ee4de --- /dev/null +++ b/src/game/client/chat.hh @@ -0,0 +1,21 @@ +#ifndef CLIENT_CHAT_HH +#define CLIENT_CHAT_HH 1 +#pragma once + +namespace client_chat +{ +void init(void); +void init_late(void); +void deinit(void); +void update(void); +void layout(void); +} // namespace client_chat + +namespace client_chat +{ +void clear(void); +void refresh_timings(void); +void print(const std::string& string); +} // namespace client_chat + +#endif /* CLIENT_CHAT_HH */ diff --git a/src/game/client/chunk_mesher.cc b/src/game/client/chunk_mesher.cc new file mode 100644 index 0000000..0271931 --- /dev/null +++ b/src/game/client/chunk_mesher.cc @@ -0,0 +1,467 @@ +#include "client/pch.hh" + +#include "client/chunk_mesher.hh" + +#include "core/crc64.hh" + +#include "shared/chunk.hh" +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/threading.hh" +#include "shared/voxel_registry.hh" + +#include "client/chunk_quad.hh" +#include "client/globals.hh" +#include "client/session.hh" +#include "client/voxel_atlas.hh" + +using QuadBuilder = std::vector<ChunkQuad>; + +using CachedChunkCoord = unsigned short; +constexpr static CachedChunkCoord CPOS_ITSELF = 0x0000; +constexpr static CachedChunkCoord CPOS_NORTH = 0x0001; +constexpr static CachedChunkCoord CPOS_SOUTH = 0x0002; +constexpr static CachedChunkCoord CPOS_EAST = 0x0003; +constexpr static CachedChunkCoord CPOS_WEST = 0x0004; +constexpr static CachedChunkCoord CPOS_TOP = 0x0005; +constexpr static CachedChunkCoord CPOS_BOTTOM = 0x0006; +constexpr static const size_t NUM_CACHED_CPOS = 7; + +static const CachedChunkCoord get_cached_cpos(const chunk_pos& pivot, const chunk_pos& cpos) +{ + static const CachedChunkCoord nx[3] = { CPOS_WEST, 0, CPOS_EAST }; + static const CachedChunkCoord ny[3] = { CPOS_BOTTOM, 0, CPOS_TOP }; + static const CachedChunkCoord nz[3] = { CPOS_NORTH, 0, CPOS_SOUTH }; + + if(pivot != cpos) { + chunk_pos delta = pivot - cpos; + delta[0] = vx::clamp<std::int64_t>(delta[0], -1, 1); + delta[1] = vx::clamp<std::int64_t>(delta[1], -1, 1); + delta[2] = vx::clamp<std::int64_t>(delta[2], -1, 1); + + if(delta[0]) { + return nx[delta[0] + 1]; + } else if(delta[1]) { + return ny[delta[1] + 1]; + } else { + return nz[delta[2] + 1]; + } + } + + return CPOS_ITSELF; +} + +static voxel_facing get_facing(voxel_face face, voxel_type type) +{ + if(type == voxel_type::CROSS) { + switch(face) { + case voxel_face::CROSS_NESW: + return voxel_facing::NESW; + case voxel_face::CROSS_NWSE: + return voxel_facing::NWSE; + default: + return voxel_facing::NORTH; + } + } + + switch(face) { + case voxel_face::CUBE_NORTH: + return voxel_facing::NORTH; + case voxel_face::CUBE_SOUTH: + return voxel_facing::SOUTH; + case voxel_face::CUBE_EAST: + return voxel_facing::EAST; + case voxel_face::CUBE_WEST: + return voxel_facing::WEST; + case voxel_face::CUBE_TOP: + return voxel_facing::UP; + case voxel_face::CUBE_BOTTOM: + return voxel_facing::DOWN; + default: + return voxel_facing::NORTH; + } +} + +class GL_MeshingTask final : public Task { +public: + explicit GL_MeshingTask(entt::entity entity, const chunk_pos& cpos); + virtual ~GL_MeshingTask(void) = default; + virtual void process(void) override; + virtual void finalize(void) override; + +private: + bool vis_test(voxel_id voxel, const VoxelInfo* info, const local_pos& lpos) const; + void push_quad_a(const VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, voxel_face face); + void push_quad_v(const VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, voxel_face face, std::size_t entropy); + void make_cube(voxel_id voxel, const VoxelInfo* info, const local_pos& lpos, voxel_vis vis, std::size_t entropy); + void cache_chunk(const chunk_pos& cpos); + +private: + std::array<VoxelStorage, NUM_CACHED_CPOS> m_cache; + std::vector<QuadBuilder> m_quads_b; // blending + std::vector<QuadBuilder> m_quads_s; // solid + entt::entity m_entity; + chunk_pos m_cpos; +}; + +GL_MeshingTask::GL_MeshingTask(entt::entity entity, const chunk_pos& cpos) +{ + m_entity = entity; + m_cpos = cpos; + + cache_chunk(m_cpos); + cache_chunk(m_cpos + DIR_NORTH<chunk_pos::value_type>); + cache_chunk(m_cpos + DIR_SOUTH<chunk_pos::value_type>); + cache_chunk(m_cpos + DIR_EAST<chunk_pos::value_type>); + cache_chunk(m_cpos + DIR_WEST<chunk_pos::value_type>); + cache_chunk(m_cpos + DIR_DOWN<chunk_pos::value_type>); + cache_chunk(m_cpos + DIR_UP<chunk_pos::value_type>); +} + +void GL_MeshingTask::process(void) +{ + m_quads_b.resize(voxel_atlas::plane_count()); + m_quads_s.resize(voxel_atlas::plane_count()); + + const auto& voxels = m_cache.at(CPOS_ITSELF); + + for(std::size_t i = 0; i < CHUNK_VOLUME; ++i) { + if(m_status == task_status::CANCELLED) { + m_quads_b.clear(); + m_quads_s.clear(); + return; + } + + const auto voxel = voxels[i]; + const auto lpos = coord::to_local(i); + + const auto info = voxel_registry::find(voxel); + + if(info == nullptr) { + // Either a NULL_VOXEL_ID or something went + // horribly wrong and we don't what this is + continue; + } + + voxel_vis vis = 0; + + if(vis_test(voxel, info, lpos + DIR_NORTH<local_pos::value_type>)) { + vis |= VIS_NORTH; + } + + if(vis_test(voxel, info, lpos + DIR_SOUTH<local_pos::value_type>)) { + vis |= VIS_SOUTH; + } + + if(vis_test(voxel, info, lpos + DIR_EAST<local_pos::value_type>)) { + vis |= VIS_EAST; + } + + if(vis_test(voxel, info, lpos + DIR_WEST<local_pos::value_type>)) { + vis |= VIS_WEST; + } + + if(vis_test(voxel, info, lpos + DIR_UP<local_pos::value_type>)) { + vis |= VIS_UP; + } + + if(vis_test(voxel, info, lpos + DIR_DOWN<local_pos::value_type>)) { + vis |= VIS_DOWN; + } + + const auto vpos = coord::to_voxel(m_cpos, lpos); + const auto entropy_src = vpos[0] * vpos[1] * vpos[2]; + const auto entropy = crc64::get(&entropy_src, sizeof(entropy_src)); + + // FIXME: handle different voxel types + make_cube(voxel, info, lpos, vis, entropy); + } +} + +void GL_MeshingTask::finalize(void) +{ + if(!globals::dimension || !globals::dimension->chunks.valid(m_entity)) { + // We either disconnected or something + // else happened that invalidated the entity + return; + } + + auto& component = globals::dimension->chunks.emplace_or_replace<ChunkMeshComponent>(m_entity); + + const std::size_t plane_count_nb = m_quads_s.size(); + const std::size_t plane_count_b = m_quads_b.size(); + + bool has_no_submeshes_b = true; + bool has_no_submeshes_nb = true; + + component.quad_nb.resize(plane_count_nb); + component.quad_b.resize(plane_count_b); + + for(std::size_t plane = 0; plane < plane_count_nb; ++plane) { + QuadBuilder& builder = m_quads_s[plane]; + ChunkVBO& buffer = component.quad_nb[plane]; + + if(builder.empty()) { + if(buffer.handle) { + glDeleteBuffers(1, &buffer.handle); + buffer.handle = 0; + buffer.size = 0; + } + } else { + if(!buffer.handle) { + glGenBuffers(1, &buffer.handle); + } + + glBindBuffer(GL_ARRAY_BUFFER, buffer.handle); + glBufferData(GL_ARRAY_BUFFER, sizeof(ChunkQuad) * builder.size(), builder.data(), GL_STATIC_DRAW); + buffer.size = builder.size(); + has_no_submeshes_nb = false; + } + } + + for(std::size_t plane = 0; plane < plane_count_b; ++plane) { + QuadBuilder& builder = m_quads_b[plane]; + ChunkVBO& buffer = component.quad_b[plane]; + + if(builder.empty()) { + if(buffer.handle) { + glDeleteBuffers(1, &buffer.handle); + buffer.handle = 0; + buffer.size = 0; + } + } else { + if(!buffer.handle) { + glGenBuffers(1, &buffer.handle); + } + + glBindBuffer(GL_ARRAY_BUFFER, buffer.handle); + glBufferData(GL_ARRAY_BUFFER, sizeof(ChunkQuad) * builder.size(), builder.data(), GL_STATIC_DRAW); + buffer.size = builder.size(); + has_no_submeshes_b = false; + } + } + + if(has_no_submeshes_b && has_no_submeshes_nb) { + globals::dimension->chunks.remove<ChunkMeshComponent>(m_entity); + } +} + +bool GL_MeshingTask::vis_test(voxel_id voxel, const VoxelInfo* info, const local_pos& lpos) const +{ + const auto pvpos = coord::to_voxel(m_cpos, lpos); + const auto pcpos = coord::to_chunk(pvpos); + const auto plpos = coord::to_local(pvpos); + const auto index = coord::to_index(plpos); + + const auto cached_cpos = get_cached_cpos(m_cpos, pcpos); + const auto& voxels = m_cache.at(cached_cpos); + const auto neighbour = voxels[index]; + + bool result; + + if(neighbour == NULL_VOXEL_ID) { + result = true; + } else if(neighbour == voxel) { + result = false; + } else if(auto neighbour_info = voxel_registry::find(neighbour)) { + if(neighbour_info->blending != info->blending) { + // Voxel types that use blending are semi-transparent; + // this means they're rendered using a different setup + // and they must have visible faces with opaque voxels + result = neighbour_info->blending; + } else { + result = false; + } + } else { + result = false; + } + + return result; +} + +void GL_MeshingTask::push_quad_a(const VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, voxel_face face) +{ + const voxel_facing facing = get_facing(face, info->type); + const VoxelTexture& vtex = info->textures[static_cast<std::size_t>(face)]; + + if(info->blending) { + m_quads_b[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset, vtex.paths.size())); + } else { + m_quads_s[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset, vtex.paths.size())); + } +} + +void GL_MeshingTask::push_quad_v(const VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, voxel_face face, std::size_t entropy) +{ + const voxel_facing facing = get_facing(face, info->type); + const VoxelTexture& vtex = info->textures[static_cast<std::size_t>(face)]; + const std::size_t entropy_mod = entropy % vtex.paths.size(); + + if(info->blending) { + m_quads_b[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset + entropy_mod, 0)); + } else { + m_quads_s[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset + entropy_mod, 0)); + } +} + +void GL_MeshingTask::make_cube(voxel_id voxel, const VoxelInfo* info, const local_pos& lpos, voxel_vis vis, std::size_t entropy) +{ + const glm::fvec3 fpos = glm::fvec3(lpos); + const glm::fvec2 fsize = glm::fvec2(1.0f, 1.0f); + + if(info->animated) { + if(vis & VIS_NORTH) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_NORTH); + } + + if(vis & VIS_SOUTH) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_SOUTH); + } + + if(vis & VIS_EAST) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_EAST); + } + + if(vis & VIS_WEST) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_WEST); + } + + if(vis & VIS_UP) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_TOP); + } + + if(vis & VIS_DOWN) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_BOTTOM); + } + } else { + if(vis & VIS_NORTH) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_NORTH, entropy); + } + + if(vis & VIS_SOUTH) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_SOUTH, entropy); + } + + if(vis & VIS_EAST) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_EAST, entropy); + } + + if(vis & VIS_WEST) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_WEST, entropy); + } + + if(vis & VIS_UP) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_TOP, entropy); + } + + if(vis & VIS_DOWN) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_BOTTOM, entropy); + } + } +} + +void GL_MeshingTask::cache_chunk(const chunk_pos& cpos) +{ + const auto index = get_cached_cpos(m_cpos, cpos); + + if(const auto chunk = globals::dimension->find_chunk(cpos)) { + m_cache[index] = chunk->get_voxels(); + return; + } +} + +// Bogus internal flag component +struct NeedsMeshingComponent final {}; + +static void on_chunk_create(const ChunkCreateEvent& event) +{ + const std::array<chunk_pos, 6> neighbours = { + event.cpos + DIR_NORTH<chunk_pos::value_type>, + event.cpos + DIR_SOUTH<chunk_pos::value_type>, + event.cpos + DIR_EAST<chunk_pos::value_type>, + event.cpos + DIR_WEST<chunk_pos::value_type>, + event.cpos + DIR_UP<chunk_pos::value_type>, + event.cpos + DIR_DOWN<chunk_pos::value_type>, + }; + + globals::dimension->chunks.emplace_or_replace<NeedsMeshingComponent>(event.chunk->get_entity()); + + for(const chunk_pos& cpos : neighbours) { + if(const Chunk* chunk = globals::dimension->find_chunk(cpos)) { + globals::dimension->chunks.emplace_or_replace<NeedsMeshingComponent>(chunk->get_entity()); + continue; + } + } +} + +static void on_chunk_update(const ChunkUpdateEvent& event) +{ + const std::array<chunk_pos, 6> neighbours = { + event.cpos + DIR_NORTH<chunk_pos::value_type>, + event.cpos + DIR_SOUTH<chunk_pos::value_type>, + event.cpos + DIR_EAST<chunk_pos::value_type>, + event.cpos + DIR_WEST<chunk_pos::value_type>, + event.cpos + DIR_UP<chunk_pos::value_type>, + event.cpos + DIR_DOWN<chunk_pos::value_type>, + }; + + globals::dimension->chunks.emplace_or_replace<NeedsMeshingComponent>(event.chunk->get_entity()); + + for(const chunk_pos& cpos : neighbours) { + if(const Chunk* chunk = globals::dimension->find_chunk(cpos)) { + globals::dimension->chunks.emplace_or_replace<NeedsMeshingComponent>(chunk->get_entity()); + continue; + } + } +} + +static void on_voxel_set(const VoxelSetEvent& event) +{ + globals::dimension->chunks.emplace_or_replace<NeedsMeshingComponent>(event.chunk->get_entity()); + + std::vector<chunk_pos> neighbours; + + for(int dim = 0; dim < 3; dim += 1) { + chunk_pos offset = chunk_pos(0, 0, 0); + offset[dim] = 1; + + if(event.lpos[dim] == 0) { + neighbours.push_back(event.cpos - offset); + continue; + } + + if(event.lpos[dim] == (CHUNK_SIZE - 1)) { + neighbours.push_back(event.cpos + offset); + continue; + } + } + + for(const chunk_pos& cpos : neighbours) { + if(const Chunk* chunk = globals::dimension->find_chunk(cpos)) { + globals::dimension->chunks.emplace_or_replace<NeedsMeshingComponent>(chunk->get_entity()); + continue; + } + } +} + +void chunk_mesher::init(void) +{ + globals::dispatcher.sink<ChunkCreateEvent>().connect<&on_chunk_create>(); + globals::dispatcher.sink<ChunkUpdateEvent>().connect<&on_chunk_update>(); + globals::dispatcher.sink<VoxelSetEvent>().connect<&on_voxel_set>(); +} + +void chunk_mesher::deinit(void) +{ +} + +void chunk_mesher::update(void) +{ + if(session::is_ingame()) { + const auto group = globals::dimension->chunks.group<NeedsMeshingComponent>(entt::get<ChunkComponent>); + for(const auto [entity, chunk] : group.each()) { + globals::dimension->chunks.remove<NeedsMeshingComponent>(entity); + threading::submit<GL_MeshingTask>(entity, chunk.cpos); + } + } +} diff --git a/src/game/client/chunk_mesher.hh b/src/game/client/chunk_mesher.hh new file mode 100644 index 0000000..36580ac --- /dev/null +++ b/src/game/client/chunk_mesher.hh @@ -0,0 +1,19 @@ +#ifndef CLIENT_CHUNK_MESHER_HH +#define CLIENT_CHUNK_MESHER_HH 1 +#pragma once + +#include "client/chunk_vbo.hh" + +struct ChunkMeshComponent final { + std::vector<ChunkVBO> quad_nb; + std::vector<ChunkVBO> quad_b; +}; + +namespace chunk_mesher +{ +void init(void); +void deinit(void); +void update(void); +} // namespace chunk_mesher + +#endif /* CLIENT_CHUNK_MESHER_HH */ diff --git a/src/game/client/chunk_quad.hh b/src/game/client/chunk_quad.hh new file mode 100644 index 0000000..337bb1e --- /dev/null +++ b/src/game/client/chunk_quad.hh @@ -0,0 +1,39 @@ +#ifndef CLIENT_CHUNK_QUAD_HH +#define CLIENT_CHUNK_QUAD_HH 1 +#pragma once + +#include "core/constexpr.hh" + +#include "shared/voxel_registry.hh" + +// [0] XXXXXXXXYYYYYYYYZZZZZZZZWWWWHHHH +// [1] FFFFTTTTTTTTTTTAAAAA------------ +using ChunkQuad = std::array<std::uint32_t, 2>; + +constexpr inline static ChunkQuad make_chunk_quad( + const glm::fvec3& position, const glm::fvec2& size, voxel_facing facing, std::size_t texture, std::size_t frames) +{ + ChunkQuad result = {}; + result[0] = 0x00000000; + result[1] = 0x00000000; + + // [0] XXXXXXXXYYYYYYYYZZZZZZZZ-------- + result[0] |= (0x000000FFU & static_cast<std::uint32_t>(position.x * 16.0f)) << 24U; + result[0] |= (0x000000FFU & static_cast<std::uint32_t>(position.y * 16.0f)) << 16U; + result[0] |= (0x000000FFU & static_cast<std::uint32_t>(position.z * 16.0f)) << 8U; + + // [0] ------------------------WWWWHHHH + result[0] |= (0x0000000FU & static_cast<std::uint32_t>(size.x * 16.0f - 1.0f)) << 4U; + result[0] |= (0x0000000FU & static_cast<std::uint32_t>(size.y * 16.0f - 1.0f)); + + // [1] FFFF---------------------------- + result[1] |= (0x0000000FU & static_cast<std::uint32_t>(facing)) << 28U; + + // [1] ----TTTTTTTTTTTAAAAA------------ + result[1] |= (0x000007FFU & static_cast<std::uint32_t>(texture)) << 17U; + result[1] |= (0x0000001FU & static_cast<std::uint32_t>(frames)) << 12U; + + return result; +} + +#endif /* CLIENT_CHUNK_QUAD_HH */ diff --git a/src/game/client/chunk_renderer.cc b/src/game/client/chunk_renderer.cc new file mode 100644 index 0000000..54239af --- /dev/null +++ b/src/game/client/chunk_renderer.cc @@ -0,0 +1,196 @@ +#include "client/pch.hh" + +#include "client/chunk_renderer.hh" + +#include "core/config.hh" + +#include "shared/chunk.hh" +#include "shared/coord.hh" +#include "shared/dimension.hh" + +#include "client/camera.hh" +#include "client/chunk_mesher.hh" +#include "client/chunk_quad.hh" +#include "client/game.hh" +#include "client/globals.hh" +#include "client/outline.hh" +#include "client/program.hh" +#include "client/settings.hh" +#include "client/skybox.hh" +#include "client/toggles.hh" +#include "client/voxel_anims.hh" +#include "client/voxel_atlas.hh" + +// ONLY TOUCH THESE IF THE RESPECTIVE SHADER +// VARIANT MACRO DECLARATIONS LAYOUT CHANGED AS WELL +constexpr static unsigned int WORLD_CURVATURE = 0U; +constexpr static unsigned int WORLD_FOG = 1U; + +static ConfigBoolean depth_sort_chunks(true); + +static GL_Program quad_program; +static std::size_t u_quad_vproj_matrix; +static std::size_t u_quad_world_position; +static std::size_t u_quad_timings; +static std::size_t u_quad_fog_color; +static std::size_t u_quad_view_distance; +static std::size_t u_quad_textures; +static GLuint quad_vaobj; +static GLuint quad_vbo; + +void chunk_renderer::init(void) +{ + globals::client_config.add_value("chunk_renderer.depth_sort_chunks", depth_sort_chunks); + + settings::add_checkbox(5, depth_sort_chunks, settings_location::VIDEO, "chunk_renderer.depth_sort_chunks", false); + + if(!quad_program.setup("shaders/chunk_quad.vert", "shaders/chunk_quad.frag")) { + spdlog::critical("chunk_renderer: quad_program: setup failed"); + std::terminate(); + } + + u_quad_vproj_matrix = quad_program.add_uniform("u_ViewProjMatrix"); + u_quad_world_position = quad_program.add_uniform("u_WorldPosition"); + u_quad_timings = quad_program.add_uniform("u_Timings"); + u_quad_fog_color = quad_program.add_uniform("u_FogColor"); + u_quad_view_distance = quad_program.add_uniform("u_ViewDistance"); + u_quad_textures = quad_program.add_uniform("u_Textures"); + + const glm::fvec3 vertices[4] = { + glm::fvec3(1.0f, 0.0f, 1.0f), + glm::fvec3(1.0f, 0.0f, 0.0f), + glm::fvec3(0.0f, 0.0f, 1.0f), + glm::fvec3(0.0f, 0.0f, 0.0f), + }; + + glGenVertexArrays(1, &quad_vaobj); + glBindVertexArray(quad_vaobj); + + glGenBuffers(1, &quad_vbo); + glBindBuffer(GL_ARRAY_BUFFER, quad_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glEnableVertexAttribArray(0); + glVertexAttribDivisor(0, 0); + glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(glm::fvec3), nullptr); +} + +void chunk_renderer::deinit(void) +{ + glDeleteBuffers(1, &quad_vbo); + glDeleteVertexArrays(1, &quad_vaobj); + quad_program.destroy(); +} + +void chunk_renderer::render(void) +{ + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); + glLineWidth(1.0f); + + if(toggles::get(TOGGLE_WIREFRAME)) { + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } else { + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } + + quad_program.set_variant_vert(WORLD_CURVATURE, client_game::world_curvature.get_value()); + quad_program.set_variant_vert(WORLD_FOG, client_game::fog_mode.get_value()); + quad_program.set_variant_frag(WORLD_FOG, client_game::fog_mode.get_value()); + + if(!quad_program.update()) { + spdlog::critical("chunk_renderer: quad_program: update failed"); + quad_program.destroy(); + std::terminate(); + } + + GLuint timings[3]; + timings[0] = globals::window_frametime; + timings[1] = globals::window_frametime_avg; + timings[2] = voxel_anims::frame; + + const auto group = globals::dimension->chunks.group<ChunkComponent>(entt::get<ChunkMeshComponent>); + + if(depth_sort_chunks.get_value()) { + // FIXME: speed! sorting every frame doesn't look + // like a good idea. Can we store the group elsewhere and + // still have all the up-to-date chunk things inside? + group.sort([](entt::entity ea, entt::entity eb) { + const auto dir_a = globals::dimension->chunks.get<ChunkComponent>(ea).cpos - camera::position_chunk; + const auto dir_b = globals::dimension->chunks.get<ChunkComponent>(eb).cpos - camera::position_chunk; + + const auto da = dir_a[0] * dir_a[0] + dir_a[1] * dir_a[1] + dir_a[2] * dir_a[2]; + const auto db = dir_b[0] * dir_b[0] + dir_b[1] * dir_b[1] + dir_b[2] * dir_b[2]; + + return da > db; + }); + } + + for(std::size_t plane_id = 0; plane_id < voxel_atlas::plane_count(); ++plane_id) { + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D_ARRAY, voxel_atlas::plane_texture(plane_id)); + + glBindVertexArray(quad_vaobj); + + glUseProgram(quad_program.handle); + glUniformMatrix4fv(quad_program.uniforms[u_quad_vproj_matrix].location, 1, false, glm::value_ptr(camera::matrix)); + glUniform3uiv(quad_program.uniforms[u_quad_timings].location, 1, timings); + glUniform4fv(quad_program.uniforms[u_quad_fog_color].location, 1, glm::value_ptr(skybox::fog_color)); + glUniform1f(quad_program.uniforms[u_quad_view_distance].location, camera::view_distance.get_value() * CHUNK_SIZE); + glUniform1i(quad_program.uniforms[u_quad_textures].location, 0); // GL_TEXTURE0 + + glDisable(GL_BLEND); + + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CCW); + + for(const auto [entity, chunk, mesh] : group.each()) { + if(plane_id < mesh.quad_nb.size() && mesh.quad_nb[plane_id].handle && mesh.quad_nb[plane_id].size) { + const auto wpos = coord::to_fvec3(chunk.cpos - camera::position_chunk); + glUniform3fv(quad_program.uniforms[u_quad_world_position].location, 1, glm::value_ptr(wpos)); + + glBindBuffer(GL_ARRAY_BUFFER, mesh.quad_nb[plane_id].handle); + + glEnableVertexAttribArray(1); + glVertexAttribDivisor(1, 1); + glVertexAttribIPointer(1, 2, GL_UNSIGNED_INT, sizeof(ChunkQuad), nullptr); + + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, mesh.quad_nb[plane_id].size); + + globals::num_drawcalls += 1; + globals::num_triangles += 2 * mesh.quad_nb[plane_id].size; + } + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + for(const auto [entity, chunk, mesh] : group.each()) { + if(plane_id < mesh.quad_b.size() && mesh.quad_b[plane_id].handle && mesh.quad_b[plane_id].size) { + const auto wpos = coord::to_fvec3(chunk.cpos - camera::position_chunk); + glUniform3fv(quad_program.uniforms[u_quad_world_position].location, 1, glm::value_ptr(wpos)); + + glBindBuffer(GL_ARRAY_BUFFER, mesh.quad_b[plane_id].handle); + + glEnableVertexAttribArray(1); + glVertexAttribDivisor(1, 1); + glVertexAttribIPointer(1, 2, GL_UNSIGNED_INT, sizeof(ChunkQuad), nullptr); + + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, mesh.quad_b[plane_id].size); + + globals::num_drawcalls += 1; + globals::num_triangles += 2 * mesh.quad_b[plane_id].size; + } + } + } + + if(toggles::get(TOGGLE_CHUNK_AABB)) { + outline::prepare(); + + for(const auto [entity, chunk, mesh] : group.each()) { + const auto size = glm::fvec3(CHUNK_SIZE, CHUNK_SIZE, CHUNK_SIZE); + outline::cube(chunk.cpos, glm::fvec3(0.0f, 0.0f, 0.0f), size, 1.0f, glm::fvec4(1.0f, 1.0f, 0.0f, 1.0f)); + } + } +} diff --git a/src/game/client/chunk_renderer.hh b/src/game/client/chunk_renderer.hh new file mode 100644 index 0000000..3ebcf76 --- /dev/null +++ b/src/game/client/chunk_renderer.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_CHUNK_RENDERER_HH +#define CLIENT_CHUNK_RENDERER_HH 1 +#pragma once + +namespace chunk_renderer +{ +void init(void); +void deinit(void); +void render(void); +} // namespace chunk_renderer + +#endif /* CLIENT_CHUNK_RENDERER_HH */ diff --git a/src/game/client/chunk_vbo.hh b/src/game/client/chunk_vbo.hh new file mode 100644 index 0000000..ba27552 --- /dev/null +++ b/src/game/client/chunk_vbo.hh @@ -0,0 +1,23 @@ +#ifndef CLIENT_CHUNK_VBO_HH +#define CLIENT_CHUNK_VBO_HH 1 +#pragma once + +class ChunkVBO final { +public: + std::size_t size; + GLuint handle; + +public: + inline ~ChunkVBO(void) + { + // The ChunkVBO structure is meant to be a part + // of the ChunkMesh component within the EnTT registry; + // When the registry is cleaned or a chunk is removed, components + // are expected to be safely disposed of so we need a destructor; + if(handle) { + glDeleteBuffers(1, &handle); + } + } +}; + +#endif /* CLIENT_CHUNK_VBO_HH */ diff --git a/src/game/client/chunk_visibility.cc b/src/game/client/chunk_visibility.cc new file mode 100644 index 0000000..f832529 --- /dev/null +++ b/src/game/client/chunk_visibility.cc @@ -0,0 +1,87 @@ +#include "client/pch.hh" + +#include "client/chunk_visibility.hh" + +#include "core/config.hh" +#include "core/vectors.hh" + +#include "shared/chunk.hh" +#include "shared/chunk_aabb.hh" +#include "shared/dimension.hh" +#include "shared/protocol.hh" + +#include "client/camera.hh" +#include "client/globals.hh" +#include "client/session.hh" + +// Sending a somewhat large amount of network packets +// can easily overwhelm both client, server and the network +// channel created between the two. To prevent this from happening +// we throttle the client's ever increasing itch for new chunks +constexpr static unsigned int MAX_CHUNKS_REQUESTS_PER_FRAME = 16U; + +static ChunkAABB current_view_box; +static ChunkAABB previous_view_box; +static std::vector<chunk_pos> requests; + +static void update_requests(void) +{ + requests.clear(); + + for(auto cx = current_view_box.min.x; cx != current_view_box.max.x; cx += 1) + for(auto cy = current_view_box.min.y; cy != current_view_box.max.y; cy += 1) + for(auto cz = current_view_box.min.z; cz != current_view_box.max.z; cz += 1) { + auto cpos = chunk_pos(cx, cy, cz); + + if(!globals::dimension->find_chunk(cpos)) { + requests.push_back(cpos); + } + } + + std::sort(requests.begin(), requests.end(), [](const chunk_pos& cpos_a, const chunk_pos& cpos_b) { + auto da = vx::distance2(cpos_a, camera::position_chunk); + auto db = vx::distance2(cpos_b, camera::position_chunk); + return da > db; + }); +} + +void chunk_visibility::update_late(void) +{ + current_view_box.min = camera::position_chunk - static_cast<chunk_pos::value_type>(camera::view_distance.get_value()); + current_view_box.max = camera::position_chunk + static_cast<chunk_pos::value_type>(camera::view_distance.get_value()); + + if(!session::is_ingame()) { + // This makes sure the previous view box + // is always different from the current one + previous_view_box.min = chunk_pos(INT32_MIN, INT32_MIN, INT32_MIN); + previous_view_box.max = chunk_pos(INT32_MAX, INT32_MAX, INT32_MAX); + return; + } + + if((current_view_box.min != previous_view_box.min) || (current_view_box.max != previous_view_box.max)) { + update_requests(); + } + + for(unsigned int i = 0U; i < MAX_CHUNKS_REQUESTS_PER_FRAME; ++i) { + if(requests.empty()) { + // Done sending requests + break; + } + + protocol::RequestChunk packet; + packet.cpos = requests.back(); + protocol::send(session::peer, protocol::encode(packet)); + + requests.pop_back(); + } + + auto view = globals::dimension->chunks.view<ChunkComponent>(); + + for(const auto [entity, chunk] : view.each()) { + if(!current_view_box.contains(chunk.cpos)) { + globals::dimension->remove_chunk(entity); + } + } + + previous_view_box = current_view_box; +} diff --git a/src/game/client/chunk_visibility.hh b/src/game/client/chunk_visibility.hh new file mode 100644 index 0000000..70352c9 --- /dev/null +++ b/src/game/client/chunk_visibility.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_CHUNK_VISIBILITY_HH +#define CLIENT_CHUNK_VISIBILITY_HH 1 +#pragma once + +#include "shared/types.hh" + +namespace chunk_visibility +{ +void update_late(void); +} // namespace chunk_visibility + +#endif /* CLIENT_CHUNK_VISIBILITY_HH */ diff --git a/src/game/client/const.hh b/src/game/client/const.hh new file mode 100644 index 0000000..9bd8346 --- /dev/null +++ b/src/game/client/const.hh @@ -0,0 +1,27 @@ +#ifndef CLIENT_CONST_HH +#define CLIENT_CONST_HH 1 +#pragma once + +#include "shared/const.hh" + +// This key is then going to be reserved for only +// the debug toggles and users won't be able to +// use this key for conventional gameplay things +constexpr static int DEBUG_KEY = GLFW_KEY_F3; + +constexpr static int BASE_WIDTH = 320; +constexpr static int BASE_HEIGHT = 240; + +constexpr static int MIN_WIDTH = 2 * BASE_WIDTH; +constexpr static int MIN_HEIGHT = 2 * BASE_HEIGHT; + +constexpr static int DEFAULT_WIDTH = 720; +constexpr static int DEFAULT_HEIGHT = 480; + +static_assert(DEFAULT_WIDTH >= MIN_WIDTH); +static_assert(DEFAULT_HEIGHT >= MIN_HEIGHT); + +constexpr static float MIN_PITCH = 0.0625f; +constexpr static float MAX_PITCH = 10.0f; + +#endif /* CLIENT_CONST_HH */ diff --git a/src/game/client/crosshair.cc b/src/game/client/crosshair.cc new file mode 100644 index 0000000..84a9a73 --- /dev/null +++ b/src/game/client/crosshair.cc @@ -0,0 +1,41 @@ +#include "client/pch.hh" + +#include "client/crosshair.hh" + +#include "core/constexpr.hh" +#include "core/resource.hh" + +#include "client/globals.hh" +#include "client/session.hh" +#include "client/texture_gui.hh" + +static resource_ptr<TextureGUI> texture; + +void crosshair::init(void) +{ + texture = resource::load<TextureGUI>( + "textures/gui/hud_crosshair.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T | TEXTURE_GUI_LOAD_VFLIP); + + if(texture == nullptr) { + spdlog::critical("crosshair: texture load failed"); + std::terminate(); + } +} + +void crosshair::deinit(void) +{ + texture = nullptr; +} + +void crosshair::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetForegroundDrawList(); + + auto scaled_width = vx::max<int>(texture->size.x, globals::gui_scale * texture->size.x / 2); + auto scaled_height = vx::max<int>(texture->size.y, globals::gui_scale * texture->size.y / 2); + auto start = ImVec2( + static_cast<int>(0.5f * viewport->Size.x) - (scaled_width / 2), static_cast<int>(0.5f * viewport->Size.y) - (scaled_height / 2)); + auto end = ImVec2(start.x + scaled_width, start.y + scaled_height); + draw_list->AddImage(texture->handle, start, end); +} diff --git a/src/game/client/crosshair.hh b/src/game/client/crosshair.hh new file mode 100644 index 0000000..6525792 --- /dev/null +++ b/src/game/client/crosshair.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_CROSSHAIR_HH +#define CLIENT_CROSSHAIR_HH 1 +#pragma once + +namespace crosshair +{ +void init(void); +void deinit(void); +void layout(void); +} // namespace crosshair + +#endif /* CLIENT_CROSSHAIR_HH */ diff --git a/src/game/client/direct_connection.cc b/src/game/client/direct_connection.cc new file mode 100644 index 0000000..c2efc4e --- /dev/null +++ b/src/game/client/direct_connection.cc @@ -0,0 +1,140 @@ +#include "client/pch.hh" + +#include "client/direct_connection.hh" + +#include "core/config.hh" +#include "core/strtools.hh" + +#include "shared/protocol.hh" + +#include "client/game.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/language.hh" +#include "client/session.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +static std::string str_title; +static std::string str_connect; +static std::string str_cancel; + +static std::string str_hostname; +static std::string str_password; + +static std::string direct_hostname; +static std::string direct_password; + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if((event.key == GLFW_KEY_ESCAPE) && (event.action == GLFW_PRESS)) { + if(globals::gui_screen == GUI_DIRECT_CONNECTION) { + globals::gui_screen = GUI_PLAY_MENU; + return; + } + } +} + +static void on_language_set(const LanguageSetEvent& event) +{ + str_title = language::resolve("direct_connection.title"); + str_connect = language::resolve_gui("direct_connection.connect"); + str_cancel = language::resolve_gui("direct_connection.cancel"); + + str_hostname = language::resolve("direct_connection.hostname"); + str_password = language::resolve("direct_connection.password"); +} + +static void connect_to_server(void) +{ + auto parts = strtools::split(direct_hostname, ":"); + std::string parsed_hostname; + std::uint16_t parsed_port; + + if(!parts[0].empty()) { + parsed_hostname = parts[0]; + } else { + parsed_hostname = std::string("localhost"); + } + + if(parts.size() >= 2) { + parsed_port = vx::clamp<std::uint16_t>(strtoul(parts[1].c_str(), nullptr, 10), 1024, UINT16_MAX); + } else { + parsed_port = protocol::PORT; + } + + session::connect(parsed_hostname.c_str(), parsed_port, direct_password.c_str()); +} + +void direct_connection::init(void) +{ + globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<LanguageSetEvent>().connect<&on_language_set>(); +} + +void direct_connection::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto window_start = ImVec2(0.25f * viewport->Size.x, 0.20f * viewport->Size.y); + auto window_size = ImVec2(0.50f * viewport->Size.x, 0.80f * viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###UIDirectConnect", nullptr, WINDOW_FLAGS)) { + const float title_width = ImGui::CalcTextSize(str_title.c_str()).x; + ImGui::SetCursorPosX(0.5f * (window_size.x - title_width)); + ImGui::TextUnformatted(str_title.c_str()); + + ImGui::Dummy(ImVec2(0.0f, 16.0f * globals::gui_scale)); + + ImGuiInputTextFlags hostname_flags = ImGuiInputTextFlags_CharsNoBlank; + + if(client_game::streamer_mode.get_value()) { + // Hide server hostname to avoid things like + // followers flooding the server that is streamed online + hostname_flags |= ImGuiInputTextFlags_Password; + } + + auto avail_width = ImGui::GetContentRegionAvail().x; + + ImGui::PushItemWidth(avail_width); + + ImGui::InputText("###UIDirectConnect_hostname", &direct_hostname, hostname_flags); + + if(ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 16.0f); + ImGui::TextUnformatted(str_hostname.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + ImGui::InputText("###UIDirectConnect_password", &direct_password, ImGuiInputTextFlags_Password); + + if(ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 16.0f); + ImGui::TextUnformatted(str_password.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + ImGui::PopItemWidth(); + + ImGui::Dummy(ImVec2(0.0f, 4.0f * globals::gui_scale)); + + ImGui::BeginDisabled(strtools::is_whitespace(direct_hostname)); + + if(ImGui::Button(str_connect.c_str(), ImVec2(avail_width, 0.0f))) { + connect_to_server(); + } + + ImGui::EndDisabled(); + + if(ImGui::Button(str_cancel.c_str(), ImVec2(avail_width, 0.0f))) { + globals::gui_screen = GUI_PLAY_MENU; + } + } + + ImGui::End(); +} diff --git a/src/game/client/direct_connection.hh b/src/game/client/direct_connection.hh new file mode 100644 index 0000000..f94bcaf --- /dev/null +++ b/src/game/client/direct_connection.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_DIRECT_CONNECTION_HH +#define CLIENT_DIRECT_CONNECTION_HH 1 +#pragma once + +namespace direct_connection +{ +void init(void); +void layout(void); +} // namespace direct_connection + +#endif /* CLIENT_DIRECT_CONNECTION_HH */ diff --git a/src/game/client/experiments.cc b/src/game/client/experiments.cc new file mode 100644 index 0000000..70353b5 --- /dev/null +++ b/src/game/client/experiments.cc @@ -0,0 +1,77 @@ +#include "client/pch.hh" + +#include "client/experiments.hh" + +#include "shared/dimension.hh" +#include "shared/game_items.hh" +#include "shared/game_voxels.hh" +#include "shared/item_registry.hh" + +#include "client/chat.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/hotbar.hh" +#include "client/player_target.hh" +#include "client/session.hh" +#include "client/status_lines.hh" + +static void on_glfw_mouse_button(const GlfwMouseButtonEvent& event) +{ + if(!globals::gui_screen && session::is_ingame()) { + if((event.action == GLFW_PRESS) && (player_target::voxel != NULL_VOXEL_ID)) { + if(event.button == GLFW_MOUSE_BUTTON_LEFT) { + experiments::attack(); + return; + } + + if(event.button == GLFW_MOUSE_BUTTON_RIGHT) { + experiments::interact(); + return; + } + } + } +} + +void experiments::init(void) +{ + globals::dispatcher.sink<GlfwMouseButtonEvent>().connect<&on_glfw_mouse_button>(); +} + +void experiments::init_late(void) +{ + hotbar::slots[0] = game_items::cobblestone; + hotbar::slots[1] = game_items::stone; + hotbar::slots[2] = game_items::dirt; + hotbar::slots[3] = game_items::grass; + hotbar::slots[4] = game_items::oak_leaves; + hotbar::slots[5] = game_items::oak_planks; + hotbar::slots[6] = game_items::oak_log; + hotbar::slots[7] = game_items::glass; + hotbar::slots[8] = game_items::slime; +} + +void experiments::deinit(void) +{ +} + +void experiments::update(void) +{ +} + +void experiments::update_late(void) +{ +} + +void experiments::attack(void) +{ + globals::dimension->set_voxel(NULL_VOXEL_ID, player_target::coord); +} + +void experiments::interact(void) +{ + if(auto info = item_registry::find(hotbar::slots[hotbar::active_slot])) { + if(info->place_voxel != NULL_VOXEL_ID) { + globals::dimension->set_voxel(info->place_voxel, player_target::coord + player_target::normal); + } + } +} diff --git a/src/game/client/experiments.hh b/src/game/client/experiments.hh new file mode 100644 index 0000000..ae20426 --- /dev/null +++ b/src/game/client/experiments.hh @@ -0,0 +1,20 @@ +#ifndef CLIENT_EXPERIMENTS_HH +#define CLIENT_EXPERIMENTS_HH 1 +#pragma once + +namespace experiments +{ +void init(void); +void init_late(void); +void deinit(void); +void update(void); +void update_late(void); +} // namespace experiments + +namespace experiments +{ +void attack(void); +void interact(void); +} // namespace experiments + +#endif /* CLIENT_EXPERIMENTS_HH */ diff --git a/src/game/client/factory.cc b/src/game/client/factory.cc new file mode 100644 index 0000000..4c1c24e --- /dev/null +++ b/src/game/client/factory.cc @@ -0,0 +1,28 @@ +#include "client/pch.hh" + +#include "client/factory.hh" + +#include "shared/dimension.hh" +#include "shared/factory.hh" +#include "shared/head.hh" +#include "shared/transform.hh" + +#include "client/globals.hh" +#include "client/sound_emitter.hh" + +void client_factory::create_player(Dimension* dimension, entt::entity entity) +{ + shared_factory::create_player(dimension, entity); + + const auto& head = dimension->entities.get<HeadComponent>(entity); + dimension->entities.emplace_or_replace<HeadComponentIntr>(entity, head); + dimension->entities.emplace_or_replace<HeadComponentPrev>(entity, head); + + const auto& transform = dimension->entities.get<TransformComponent>(entity); + dimension->entities.emplace_or_replace<TransformComponentIntr>(entity, transform); + dimension->entities.emplace_or_replace<TransformComponentPrev>(entity, transform); + + if(globals::sound_ctx) { + dimension->entities.emplace_or_replace<SoundEmitterComponent>(entity); + } +} diff --git a/src/game/client/factory.hh b/src/game/client/factory.hh new file mode 100644 index 0000000..6f883c2 --- /dev/null +++ b/src/game/client/factory.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_FACTORY_HH +#define CLIENT_FACTORY_HH 1 +#pragma once + +class Dimension; + +namespace client_factory +{ +void create_player(Dimension* dimension, entt::entity entity); +} // namespace client_factory + +#endif /* CLIENT_FACTORY_HH */ diff --git a/src/game/client/game.cc b/src/game/client/game.cc new file mode 100644 index 0000000..c930f38 --- /dev/null +++ b/src/game/client/game.cc @@ -0,0 +1,684 @@ +#include "client/pch.hh" + +#include "client/game.hh" + +#include "core/angles.hh" +#include "core/binfile.hh" +#include "core/config.hh" +#include "core/resource.hh" + +#include "shared/collision.hh" +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/game_items.hh" +#include "shared/game_voxels.hh" +#include "shared/gravity.hh" +#include "shared/head.hh" +#include "shared/item_registry.hh" +#include "shared/player.hh" +#include "shared/protocol.hh" +#include "shared/ray_dda.hh" +#include "shared/stasis.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" +#include "shared/voxel_registry.hh" + +#include "client/background.hh" +#include "client/bother.hh" +#include "client/camera.hh" +#include "client/chat.hh" +#include "client/chunk_mesher.hh" +#include "client/chunk_renderer.hh" +#include "client/chunk_visibility.hh" +#include "client/const.hh" +#include "client/crosshair.hh" +#include "client/direct_connection.hh" +#include "client/experiments.hh" +#include "client/gamepad.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/hotbar.hh" +#include "client/interpolation.hh" +#include "client/keybind.hh" +#include "client/language.hh" +#include "client/listener.hh" +#include "client/main_menu.hh" +#include "client/message_box.hh" +#include "client/metrics.hh" +#include "client/outline.hh" +#include "client/play_menu.hh" +#include "client/player_look.hh" +#include "client/player_move.hh" +#include "client/player_target.hh" +#include "client/progress_bar.hh" +#include "client/receive.hh" +#include "client/scoreboard.hh" +#include "client/screenshot.hh" +#include "client/session.hh" +#include "client/settings.hh" +#include "client/skybox.hh" +#include "client/sound.hh" +#include "client/sound_emitter.hh" +#include "client/splash.hh" +#include "client/status_lines.hh" +#include "client/texture_gui.hh" +#include "client/toggles.hh" +#include "client/voxel_anims.hh" +#include "client/voxel_atlas.hh" +#include "client/voxel_sounds.hh" +#include "client/window_title.hh" + +ConfigBoolean client_game::streamer_mode(false); +ConfigBoolean client_game::vertical_sync(true); +ConfigBoolean client_game::world_curvature(true); +ConfigUnsigned client_game::fog_mode(1U, 0U, 2U); +ConfigString client_game::username("player"); + +bool client_game::hide_hud = false; + +static ConfigKeyBind hide_hud_toggle(GLFW_KEY_F1); + +static resource_ptr<BinFile> bin_unscii16; +static resource_ptr<BinFile> bin_unscii8; + +static void on_glfw_framebuffer_size(const GlfwFramebufferSizeEvent& event) +{ + auto width_float = static_cast<float>(event.size.x); + auto height_float = static_cast<float>(event.size.y); + auto wscale = vx::max(1U, vx::floor<unsigned int>(width_float / static_cast<float>(BASE_WIDTH))); + auto hscale = vx::max(1U, vx::floor<unsigned int>(height_float / static_cast<float>(BASE_HEIGHT))); + auto scale = vx::min(wscale, hscale); + + if(globals::gui_scale != scale) { + auto& io = ImGui::GetIO(); + auto& style = ImGui::GetStyle(); + + ImFontConfig font_config; + font_config.FontDataOwnedByAtlas = false; + + io.Fonts->Clear(); + + ImFontGlyphRangesBuilder builder; + + // This should cover a hefty range of glyph ranges. + // UNDONE: just slap the whole UNICODE Plane-0 here? + builder.AddRanges(io.Fonts->GetGlyphRangesDefault()); + builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic()); + builder.AddRanges(io.Fonts->GetGlyphRangesGreek()); + builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); + + ImVector<ImWchar> ranges = {}; + builder.BuildRanges(&ranges); + + globals::font_default = io.Fonts->AddFontFromMemoryTTF( + bin_unscii16->buffer, bin_unscii16->size, 16.0f * scale, &font_config, ranges.Data); + globals::font_chat = io.Fonts->AddFontFromMemoryTTF( + bin_unscii16->buffer, bin_unscii16->size, 8.0f * scale, &font_config, ranges.Data); + globals::font_debug = io.Fonts->AddFontFromMemoryTTF(bin_unscii8->buffer, bin_unscii8->size, 4.0f * scale, &font_config); + + // Re-assign the default font + io.FontDefault = globals::font_default; + + // This should be here!!! Just calling Build() + // on the font atlas does not invalidate internal + // device objects defined by the implementation!!! + ImGui_ImplOpenGL3_CreateDeviceObjects(); + + if(globals::gui_scale) { + // Well, ImGuiStyle::ScaleAllSizes indeed takes + // the scale values as a RELATIVE scaling, not as + // absolute. So I have to make a special crutch + style.ScaleAllSizes(static_cast<float>(scale) / static_cast<float>(globals::gui_scale)); + } + + globals::gui_scale = scale; + } + + if(globals::world_fbo) { + glDeleteRenderbuffers(1, &globals::world_fbo_depth); + glDeleteTextures(1, &globals::world_fbo_color); + glDeleteFramebuffers(1, &globals::world_fbo); + } + + glGenFramebuffers(1, &globals::world_fbo); + glGenTextures(1, &globals::world_fbo_color); + glGenRenderbuffers(1, &globals::world_fbo_depth); + + glBindTexture(GL_TEXTURE_2D, globals::world_fbo_color); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, event.size.x, event.size.y, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + + glBindRenderbuffer(GL_RENDERBUFFER, globals::world_fbo_depth); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, event.size.x, event.size.y); + + glBindFramebuffer(GL_FRAMEBUFFER, globals::world_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, globals::world_fbo_color, 0); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, globals::world_fbo_depth); + + if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + spdlog::critical("opengl: world framebuffer is incomplete"); + glDeleteRenderbuffers(1, &globals::world_fbo_depth); + glDeleteTextures(1, &globals::world_fbo_color); + glDeleteFramebuffers(1, &globals::world_fbo); + std::terminate(); + } +} + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if(!globals::gui_keybind_ptr && hide_hud_toggle.equals(event.key) && (event.action == GLFW_PRESS)) { + client_game::hide_hud = !client_game::hide_hud; + } +} + +void client_game::init(void) +{ + bin_unscii16 = resource::load<BinFile>("fonts/unscii-16.ttf"); + bin_unscii8 = resource::load<BinFile>("fonts/unscii-8.ttf"); + + if((bin_unscii16 == nullptr) || (bin_unscii8 == nullptr)) { + spdlog::critical("client_game: font loading failed"); + std::terminate(); + } + + client_splash::init(); + client_splash::render(); + + globals::client_config.add_value("game.streamer_mode", client_game::streamer_mode); + globals::client_config.add_value("game.vertical_sync", client_game::vertical_sync); + globals::client_config.add_value("game.world_curvature", client_game::world_curvature); + globals::client_config.add_value("game.fog_mode", client_game::fog_mode); + globals::client_config.add_value("game.username", client_game::username); + globals::client_config.add_value("game.key.toggle_hide_hud", hide_hud_toggle); + + settings::init(); + + settings::add_checkbox(0, client_game::streamer_mode, settings_location::VIDEO_GUI, "game.streamer_mode", true); + settings::add_checkbox(5, client_game::vertical_sync, settings_location::VIDEO, "game.vertical_sync", false); + settings::add_checkbox(4, client_game::world_curvature, settings_location::VIDEO, "game.world_curvature", true); + settings::add_stepper(3, client_game::fog_mode, settings_location::VIDEO, "game.fog_mode", false); + settings::add_input(1, client_game::username, settings_location::GENERAL, "game.username", true, false); + settings::add_keybind(4, hide_hud_toggle, settings_location::KEYBOARD_MISC, "game.key.toggle_hide_hud"); + + globals::client_host = enet_host_create(nullptr, 1, 1, 0, 0); + + if(!globals::client_host) { + spdlog::critical("game: unable to setup an ENet host"); + std::terminate(); + } + + language::init(); + + session::init(); + + player_look::init(); + player_move::init(); + player_target::init(); + + gamepad::init(); + + camera::init(); + + voxel_anims::init(); + + outline::init(); + chunk_mesher::init(); + chunk_renderer::init(); + + globals::world_fbo = 0; + globals::world_fbo_color = 0; + globals::world_fbo_depth = 0; + + voxel_sounds::init(); + + skybox::init(); + + ImGuiStyle& style = ImGui::GetStyle(); + + // Black buttons on a dark background + // may be harder to read than the text on them + style.FrameBorderSize = 1.0; + style.TabBorderSize = 1.0; + + // Rounding on elements looks cool but I am + // aiming for a more or less blocky and + // visually simple HiDPI-friendly UI style + style.TabRounding = 0.0f; + style.GrabRounding = 0.0f; + style.ChildRounding = 0.0f; + style.FrameRounding = 0.0f; + style.PopupRounding = 0.0f; + style.WindowRounding = 0.0f; + style.ScrollbarRounding = 0.0f; + + style.Colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + style.Colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + style.Colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); + style.Colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + style.Colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); + style.Colors[ImGuiCol_Border] = ImVec4(0.79f, 0.79f, 0.79f, 0.50f); + style.Colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + style.Colors[ImGuiCol_FrameBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.54f); + style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(0.36f, 0.36f, 0.36f, 0.40f); + style.Colors[ImGuiCol_FrameBgActive] = ImVec4(0.63f, 0.63f, 0.63f, 0.67f); + style.Colors[ImGuiCol_TitleBg] = ImVec4(0.04f, 0.04f, 0.04f, 1.00f); + style.Colors[ImGuiCol_TitleBgActive] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + style.Colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); + style.Colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + style.Colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f); + style.Colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.00f, 0.00f, 0.00f, 0.75f); + style.Colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f); + style.Colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + style.Colors[ImGuiCol_CheckMark] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + style.Colors[ImGuiCol_SliderGrab] = ImVec4(0.81f, 0.81f, 0.81f, 0.75f); + style.Colors[ImGuiCol_SliderGrabActive] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + style.Colors[ImGuiCol_Button] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f); + style.Colors[ImGuiCol_ButtonActive] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + style.Colors[ImGuiCol_Header] = ImVec4(0.00f, 0.00f, 0.00f, 0.75f); + style.Colors[ImGuiCol_HeaderHovered] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f); + style.Colors[ImGuiCol_HeaderActive] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + style.Colors[ImGuiCol_Separator] = ImVec4(0.49f, 0.49f, 0.49f, 0.50f); + style.Colors[ImGuiCol_SeparatorHovered] = ImVec4(0.56f, 0.56f, 0.56f, 0.78f); + style.Colors[ImGuiCol_SeparatorActive] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + style.Colors[ImGuiCol_ResizeGrip] = ImVec4(0.34f, 0.34f, 0.34f, 0.20f); + style.Colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.57f, 0.57f, 0.57f, 0.67f); + style.Colors[ImGuiCol_ResizeGripActive] = ImVec4(1.00f, 1.00f, 1.00f, 0.95f); + style.Colors[ImGuiCol_Tab] = ImVec4(0.00f, 0.00f, 0.00f, 0.75f); + style.Colors[ImGuiCol_TabHovered] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f); + style.Colors[ImGuiCol_TabActive] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + style.Colors[ImGuiCol_TabUnfocused] = ImVec4(0.13f, 0.13f, 0.13f, 0.97f); + style.Colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.44f, 0.44f, 0.44f, 1.00f); + style.Colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); + style.Colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.69f, 0.00f, 0.00f, 1.00f); + style.Colors[ImGuiCol_PlotHistogram] = ImVec4(0.00f, 1.00f, 0.20f, 1.00f); + style.Colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + style.Colors[ImGuiCol_TableHeaderBg] = ImVec4(0.19f, 0.19f, 0.20f, 1.00f); + style.Colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.35f, 1.00f); + style.Colors[ImGuiCol_TableBorderLight] = ImVec4(0.23f, 0.23f, 0.25f, 1.00f); + style.Colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + style.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.06f); + style.Colors[ImGuiCol_TextSelectedBg] = ImVec4(0.61f, 0.61f, 0.61f, 0.35f); + style.Colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 1.00f); + style.Colors[ImGuiCol_NavHighlight] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + style.Colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + style.Colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + style.Colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); + + // Making my own Game UI for Source Engine + // taught me one important thing: dimensions + // of UI elements must be calculated at semi-runtime + // so there's simply no point for an INI file. + ImGui::GetIO().IniFilename = nullptr; + + toggles::init(); + + background::init(); + + scoreboard::init(); + + client_chat::init(); + + bother::init(); + + main_menu::init(); + play_menu::init(); + progress_bar::init(); + message_box::init(); + direct_connection::init(); + + crosshair::init(); + hotbar::init(); + metrics::init(); + status_lines::init(); + + screenshot::init(); + + globals::gui_keybind_ptr = nullptr; + globals::gui_scale = 0U; + globals::gui_screen = GUI_MAIN_MENU; + + sound::init_config(); + + if(globals::sound_ctx) { + sound::init(); + } + + client_receive::init(); + + experiments::init(); + + globals::dispatcher.sink<GlfwFramebufferSizeEvent>().connect<&on_glfw_framebuffer_size>(); + globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>(); +} + +void client_game::init_late(void) +{ + toggles::init_late(); + + if(globals::sound_ctx) { + sound::init_late(); + } + + language::init_late(); + + settings::init_late(); + + client_chat::init_late(); + + status_lines::init_late(); + + game_voxels::populate(); + game_items::populate(); + + std::size_t max_texture_count = 0; + + // Figure out the total texture count + // NOTE: this is very debug, early and a quite + // conservative limit choice; there must be a better + // way to make this limit way smaller than it currently is + for(const std::shared_ptr<VoxelInfo>& info : voxel_registry::voxels) { + for(const VoxelTexture& vtex : info->textures) { + max_texture_count += vtex.paths.size(); + } + } + + // UNDONE: asset packs for non-16x16 stuff + voxel_atlas::create(16, 16, max_texture_count); + + for(std::shared_ptr<VoxelInfo>& info : voxel_registry::voxels) { + for(VoxelTexture& vtex : info->textures) { + if(auto strip = voxel_atlas::find_or_load(vtex.paths)) { + vtex.cached_offset = strip->offset; + vtex.cached_plane = strip->plane; + continue; + } + + spdlog::critical("client_gl: {}: failed to load atlas strips", info->name); + std::terminate(); + } + } + + voxel_atlas::generate_mipmaps(); + + for(std::shared_ptr<ItemInfo>& info : item_registry::items) { + info->cached_texture = resource::load<TextureGUI>(info->texture.c_str(), TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + } + + experiments::init_late(); + + client_splash::init_late(); + + window_title::update(); +} + +void client_game::deinit(void) +{ + voxel_sounds::deinit(); + + experiments::deinit(); + + session::deinit(); + + if(globals::sound_ctx) { + sound::deinit(); + } + + hotbar::deinit(); + main_menu::deinit(); + play_menu::deinit(); + + bother::deinit(); + + client_chat::deinit(); + + background::deinit(); + + crosshair::deinit(); + + delete globals::dimension; + globals::player = entt::null; + globals::dimension = nullptr; + + item_registry::purge(); + voxel_registry::purge(); + + voxel_atlas::destroy(); + + glDeleteRenderbuffers(1, &globals::world_fbo_depth); + glDeleteTextures(1, &globals::world_fbo_color); + glDeleteFramebuffers(1, &globals::world_fbo); + + outline::deinit(); + chunk_renderer::deinit(); + chunk_mesher::deinit(); + + enet_host_destroy(globals::client_host); + + bin_unscii8 = nullptr; + bin_unscii16 = nullptr; +} + +void client_game::fixed_update(void) +{ + player_move::fixed_update(); + + // Only update world simulation gamesystems + // if the player can actually observe all the + // changes these gamesystems cause visually + if(session::is_ingame()) { + CollisionComponent::fixed_update(globals::dimension); + VelocityComponent::fixed_update(globals::dimension); + TransformComponent::fixed_update(globals::dimension); + GravityComponent::fixed_update(globals::dimension); + StasisComponent::fixed_update(globals::dimension); + } +} + +void client_game::fixed_update_late(void) +{ + if(session::is_ingame()) { + const auto& head = globals::dimension->entities.get<HeadComponent>(globals::player); + const auto& transform = globals::dimension->entities.get<TransformComponent>(globals::player); + const auto& velocity = globals::dimension->entities.get<VelocityComponent>(globals::player); + + protocol::EntityHead head_packet; + head_packet.entity = entt::null; // ignored by server + head_packet.angles = head.angles; + + protocol::EntityTransform transform_packet; + transform_packet.entity = entt::null; // ignored by server + transform_packet.angles = transform.angles; + transform_packet.chunk = transform.chunk; + transform_packet.local = transform.local; + + protocol::EntityVelocity velocity_packet; + velocity_packet.entity = entt::null; // ignored by server + velocity_packet.value = velocity.value; + + protocol::send(session::peer, protocol::encode(head_packet)); + protocol::send(session::peer, protocol::encode(transform_packet)); + protocol::send(session::peer, protocol::encode(velocity_packet)); + } +} + +void client_game::update(void) +{ + if(session::is_ingame()) { + if(toggles::get(TOGGLE_PM_FLIGHT)) { + globals::dimension->entities.remove<GravityComponent>(globals::player); + } else { + globals::dimension->entities.emplace_or_replace<GravityComponent>(globals::player); + } + } + + if(globals::sound_ctx) { + sound::update(); + + listener::update(); + + SoundEmitterComponent::update(); + } + + interpolation::update(); + + player_target::update(); + + camera::update(); + + voxel_anims::update(); + + chunk_mesher::update(); + + client_chat::update(); + + experiments::update(); +} + +void client_game::update_late(void) +{ + ENetEvent enet_event; + + while(0 < enet_host_service(globals::client_host, &enet_event, 0)) { + switch(enet_event.type) { + case ENET_EVENT_TYPE_CONNECT: + session::send_login_request(); + break; + case ENET_EVENT_TYPE_DISCONNECT: + session::invalidate(); + break; + case ENET_EVENT_TYPE_RECEIVE: + protocol::decode(globals::dispatcher, enet_event.packet, enet_event.peer); + enet_packet_destroy(enet_event.packet); + break; + } + } + + player_look::update_late(); + player_move::update_late(); + + play_menu::update_late(); + + bother::update_late(); + + experiments::update_late(); + + gamepad::update_late(); + + chunk_visibility::update_late(); + + if(client_game::vertical_sync.get_value()) { + glfwSwapInterval(1); + } else { + glfwSwapInterval(0); + } +} + +void client_game::render(void) +{ + glViewport(0, 0, globals::width, globals::height); + glBindFramebuffer(GL_FRAMEBUFFER, globals::world_fbo); + glClearColor(skybox::fog_color.r, skybox::fog_color.g, skybox::fog_color.b, 1.000f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + if(globals::dimension) { + chunk_renderer::render(); + } + + glEnable(GL_DEPTH_TEST); + + player_target::render(); + + if(globals::dimension) { + auto group = globals::dimension->entities.group( + entt::get<PlayerComponent, CollisionComponent, HeadComponentIntr, TransformComponentIntr>); + + outline::prepare(); + + for(const auto [entity, collision, head, transform] : group.each()) { + if(entity == globals::player) { + // Don't render ourselves + continue; + } + + glm::fvec3 forward; + cxangles::vectors(transform.angles + head.angles, forward); + forward *= 2.0f; + + glm::fvec3 hull_size = collision.aabb.max - collision.aabb.min; + glm::fvec3 hull_fpos = transform.local + collision.aabb.min; + glm::fvec3 look = transform.local + head.offset; + + outline::cube(transform.chunk, hull_fpos, hull_size, 1.0f, glm::fvec4(1.0f, 0.0f, 0.0f, 1.0f)); + outline::line(transform.chunk, look, forward, 1.0f, glm::fvec4(0.9f, 0.9f, 0.9f, 1.0f)); + } + } + + glEnable(GL_DEPTH_TEST); + + glViewport(0, 0, globals::width, globals::height); + glClearColor(0.000f, 0.000f, 0.000f, 1.000f); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glClear(GL_COLOR_BUFFER_BIT); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, globals::world_fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBlitFramebuffer(0, 0, globals::width, globals::height, 0, 0, globals::width, globals::height, GL_COLOR_BUFFER_BIT, GL_NEAREST); +} + +void client_game::layout(void) +{ + if(!session::is_ingame()) { + background::layout(); + } + + if(!globals::gui_screen || (globals::gui_screen == GUI_CHAT)) { + if(toggles::get(TOGGLE_METRICS_UI) && !client_game::hide_hud) { + // This contains Minecraft-esque debug information + // about the hardware, world state and other + // things that might be uesful + metrics::layout(); + } + } + + if(session::is_ingame()) { + client_chat::layout(); + scoreboard::layout(); + + if(!globals::gui_screen && !client_game::hide_hud) { + hotbar::layout(); + status_lines::layout(); + crosshair::layout(); + } + } + + if(globals::gui_screen) { + if(session::is_ingame() && (globals::gui_screen != GUI_CHAT)) { + const float width_f = static_cast<float>(globals::width); + const float height_f = static_cast<float>(globals::height); + const ImU32 darken = ImGui::GetColorU32(ImVec4(0.00f, 0.00f, 0.00f, 0.75f)); + ImGui::GetBackgroundDrawList()->AddRectFilled(ImVec2(), ImVec2(width_f, height_f), darken); + } + + switch(globals::gui_screen) { + case GUI_MAIN_MENU: + main_menu::layout(); + break; + case GUI_PLAY_MENU: + play_menu::layout(); + break; + case GUI_SETTINGS: + settings::layout(); + break; + case GUI_PROGRESS_BAR: + progress_bar::layout(); + break; + case GUI_MESSAGE_BOX: + message_box::layout(); + break; + case GUI_DIRECT_CONNECTION: + direct_connection::layout(); + break; + } + } +} diff --git a/src/game/client/game.hh b/src/game/client/game.hh new file mode 100644 index 0000000..f3c6fc4 --- /dev/null +++ b/src/game/client/game.hh @@ -0,0 +1,36 @@ +#ifndef CLIENT_GAME_HH +#define CLIENT_GAME_HH 1 +#pragma once + +class ConfigBoolean; +class ConfigString; +class ConfigUnsigned; + +namespace client_game +{ +extern ConfigBoolean streamer_mode; +extern ConfigBoolean vertical_sync; +extern ConfigBoolean world_curvature; +extern ConfigUnsigned fog_mode; +extern ConfigString username; +} // namespace client_game + +namespace client_game +{ +extern bool hide_hud; +} // namespace client_game + +namespace client_game +{ +void init(void); +void init_late(void); +void deinit(void); +void fixed_update(void); +void fixed_update_late(void); +void update(void); +void update_late(void); +void render(void); +void layout(void); +} // namespace client_game + +#endif /* CLIENT_GAME_HH */ diff --git a/src/game/client/gamepad.cc b/src/game/client/gamepad.cc new file mode 100644 index 0000000..6cbcb3f --- /dev/null +++ b/src/game/client/gamepad.cc @@ -0,0 +1,168 @@ +#include "client/pch.hh" + +#include "client/gamepad.hh" + +#include "core/cmdline.hh" +#include "core/config.hh" +#include "core/constexpr.hh" + +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/settings.hh" +#include "client/toggles.hh" + +constexpr static int INVALID_GAMEPAD_ID = INT_MAX; +constexpr static std::size_t NUM_AXES = static_cast<std::size_t>(GLFW_GAMEPAD_AXIS_LAST + 1); +constexpr static std::size_t NUM_BUTTONS = static_cast<std::size_t>(GLFW_GAMEPAD_BUTTON_LAST + 1); +constexpr static float GAMEPAD_AXIS_EVENT_THRESHOLD = 0.5f; + +static int active_gamepad_id; + +bool gamepad::available = false; +ConfigFloat gamepad::deadzone(0.00f, 0.00f, 0.66f); +ConfigBoolean gamepad::active(false); +GLFWgamepadstate gamepad::state; +GLFWgamepadstate gamepad::last_state; + +static void on_toggle_enable(const ToggleEnabledEvent& event) +{ + if(event.type == TOGGLE_USE_GAMEPAD) { + gamepad::active.set_value(true); + return; + } +} + +static void on_toggle_disable(const ToggleDisabledEvent& event) +{ + if(event.type == TOGGLE_USE_GAMEPAD) { + gamepad::active.set_value(false); + return; + } +} + +static void on_glfw_joystick_event(const GlfwJoystickEvent& event) +{ + if((event.event_type == GLFW_CONNECTED) && glfwJoystickIsGamepad(event.joystick_id) && (active_gamepad_id == INVALID_GAMEPAD_ID)) { + gamepad::available = true; + + active_gamepad_id = event.joystick_id; + + for(int i = 0; i < NUM_AXES; gamepad::last_state.axes[i++] = 0.0f) + ; + for(int i = 0; i < NUM_BUTTONS; gamepad::last_state.buttons[i++] = GLFW_RELEASE) + ; + + spdlog::info("gamepad: detected gamepad: {}", glfwGetGamepadName(event.joystick_id)); + + return; + } + + if((event.event_type == GLFW_DISCONNECTED) && (active_gamepad_id == event.joystick_id)) { + gamepad::available = false; + + active_gamepad_id = INVALID_GAMEPAD_ID; + + for(int i = 0; i < NUM_AXES; gamepad::last_state.axes[i++] = 0.0f) + ; + for(int i = 0; i < NUM_BUTTONS; gamepad::last_state.buttons[i++] = GLFW_RELEASE) + ; + + spdlog::warn("gamepad: disconnected"); + + return; + } +} + +void gamepad::init(void) +{ + gamepad::available = false; + + active_gamepad_id = INVALID_GAMEPAD_ID; + + globals::client_config.add_value("gamepad.deadzone", gamepad::deadzone); + globals::client_config.add_value("gamepad.active", gamepad::active); + + settings::add_checkbox(0, gamepad::active, settings_location::GAMEPAD, "gamepad.active", true); + settings::add_slider(1, gamepad::deadzone, settings_location::GAMEPAD, "gamepad.deadzone", true, "%.03f"); + + auto mappings_path = cmdline::get("gpmap", "misc/gamecontrollerdb.txt"); + auto mappings_file = PHYSFS_openRead(mappings_path); + + if(mappings_file) { + spdlog::info("gamepad: using mappings from {}", mappings_path); + auto mappings_string = std::string(PHYSFS_fileLength(mappings_file), char(0x00)); + PHYSFS_readBytes(mappings_file, mappings_string.data(), mappings_string.size()); + glfwUpdateGamepadMappings(mappings_string.c_str()); + PHYSFS_close(mappings_file); + } + + for(int joystick = 0; joystick <= GLFW_JOYSTICK_LAST; joystick += 1) { + if(glfwJoystickIsGamepad(joystick)) { + gamepad::available = true; + + active_gamepad_id = joystick; + + for(int i = 0; i < NUM_AXES; gamepad::last_state.axes[i++] = 0.0f) + ; + for(int i = 0; i < NUM_BUTTONS; gamepad::last_state.buttons[i++] = GLFW_RELEASE) + ; + + spdlog::info("gamepad: detected gamepad: {}", glfwGetGamepadName(joystick)); + + break; + } + } + + for(int i = 0; i < NUM_AXES; gamepad::state.axes[i++] = 0.0f) + ; + for(int i = 0; i < NUM_BUTTONS; gamepad::state.buttons[i++] = GLFW_RELEASE) + ; + + globals::dispatcher.sink<ToggleEnabledEvent>().connect<&on_toggle_enable>(); + globals::dispatcher.sink<ToggleDisabledEvent>().connect<&on_toggle_disable>(); + globals::dispatcher.sink<GlfwJoystickEvent>().connect<&on_glfw_joystick_event>(); +} + +void gamepad::update_late(void) +{ + if(active_gamepad_id == INVALID_GAMEPAD_ID) { + // No active gamepad found + return; + } + + if(glfwGetGamepadState(active_gamepad_id, &gamepad::state)) { + for(int i = 0; i < NUM_AXES; ++i) { + if((vx::abs(gamepad::state.axes[i]) > GAMEPAD_AXIS_EVENT_THRESHOLD) + && (vx::abs(gamepad::last_state.axes[i]) <= GAMEPAD_AXIS_EVENT_THRESHOLD)) { + GamepadAxisEvent event; + event.action = GLFW_PRESS; + event.axis = i; + globals::dispatcher.enqueue(event); + continue; + } + + if((vx::abs(gamepad::state.axes[i]) <= GAMEPAD_AXIS_EVENT_THRESHOLD) + && (vx::abs(gamepad::last_state.axes[i]) > GAMEPAD_AXIS_EVENT_THRESHOLD)) { + GamepadAxisEvent event; + event.action = GLFW_RELEASE; + event.axis = i; + globals::dispatcher.enqueue(event); + continue; + } + } + + for(int i = 0; i < NUM_BUTTONS; ++i) { + if(gamepad::state.buttons[i] == gamepad::last_state.buttons[i]) { + // Nothing happens + continue; + } + + GamepadButtonEvent event; + event.action = gamepad::state.buttons[i]; + event.button = i; + globals::dispatcher.enqueue(event); + } + } + + gamepad::last_state = gamepad::state; +} diff --git a/src/game/client/gamepad.hh b/src/game/client/gamepad.hh new file mode 100644 index 0000000..d2483b7 --- /dev/null +++ b/src/game/client/gamepad.hh @@ -0,0 +1,45 @@ +#ifndef CLIENT_GAMEPAD_HH +#define CLIENT_GAMEPAD_HH 1 +#pragma once + +constexpr static int INVALID_GAMEPAD_AXIS = INT_MAX; +constexpr static int INVALID_GAMEPAD_BUTTON = INT_MAX; + +class ConfigBoolean; +class ConfigFloat; + +struct GLFWgamepadstate; + +namespace gamepad +{ +extern bool available; +extern ConfigFloat deadzone; +extern ConfigBoolean active; +extern GLFWgamepadstate state; +extern GLFWgamepadstate last_state; +} // namespace gamepad + +namespace gamepad +{ +void init(void); +void update_late(void); +} // namespace gamepad + +// This simulates buttons using axes. When an axis +// value exceeds 1.5 times the deadzone, the event is +// queued with a GLFW_PRESS action, when it falls back +// below the threshold, the event is queued with GLFW_RELEASE action +struct GamepadAxisEvent final { + int action; + int axis; +}; + +// This smears GLFW event sugar over gamepad polling +// system. Whenever it detects a state change, the event +// is queued with an appropriate action +struct GamepadButtonEvent final { + int action; + int button; +}; + +#endif /* CLIENT_GAMEPAD_HH */ diff --git a/src/game/client/gamepad_axis.cc b/src/game/client/gamepad_axis.cc new file mode 100644 index 0000000..546c647 --- /dev/null +++ b/src/game/client/gamepad_axis.cc @@ -0,0 +1,114 @@ +#include "client/pch.hh" + +#include "client/gamepad_axis.hh" + +#include "core/constexpr.hh" + +#include "client/gamepad.hh" + +constexpr static const char* UNKNOWN_AXIS_NAME = "UNKNOWN"; + +static const std::pair<int, const char*> axis_names[] = { + { GLFW_GAMEPAD_AXIS_LEFT_X, "LEFT_X" }, + { GLFW_GAMEPAD_AXIS_LEFT_Y, "LEFT_Y" }, + { GLFW_GAMEPAD_AXIS_RIGHT_X, "RIGHT_X" }, + { GLFW_GAMEPAD_AXIS_RIGHT_Y, "RIGHT_Y" }, + { GLFW_GAMEPAD_AXIS_LEFT_TRIGGER, "LEFT_TRIG" }, + { GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER, "RIGHT_TRIG" }, +}; + +static const char* get_axis_name(int axis) +{ + for(const auto& it : axis_names) { + if(it.first != axis) { + continue; + } + + return it.second; + } + + return UNKNOWN_AXIS_NAME; +} + +ConfigGamepadAxis::ConfigGamepadAxis(void) : ConfigGamepadAxis(INVALID_GAMEPAD_AXIS, false) +{ +} + +ConfigGamepadAxis::ConfigGamepadAxis(int axis, bool inverted) +{ + m_inverted = inverted; + m_gamepad_axis = axis; + m_name = get_axis_name(axis); + m_full_string = std::format("{}:{}", m_name, m_inverted ? 1U : 0U); +} + +const char* ConfigGamepadAxis::get(void) const +{ + return m_full_string.c_str(); +} + +void ConfigGamepadAxis::set(const char* value) +{ + char new_name[64]; + unsigned int new_invert; + + if(2 == std::sscanf(value, "%63[^:]:%u", new_name, &new_invert)) { + for(const auto& it : axis_names) { + if(!std::strcmp(it.second, new_name)) { + m_inverted = new_invert; + m_gamepad_axis = it.first; + m_name = get_axis_name(m_gamepad_axis); + m_full_string = std::format("{}:{}", m_name, m_inverted ? 1U : 0U); + return; + } + } + } + + m_inverted = false; + m_gamepad_axis = INVALID_GAMEPAD_AXIS; + m_name = UNKNOWN_AXIS_NAME; + m_full_string = std::format("{}:{}", m_name, m_inverted ? 1U : 0U); +} + +int ConfigGamepadAxis::get_axis(void) const +{ + return m_gamepad_axis; +} + +void ConfigGamepadAxis::set_axis(int axis) +{ + m_gamepad_axis = axis; + m_name = get_axis_name(axis); + m_full_string = std::format("{}:{}", m_name, m_inverted ? 1U : 0U); +} + +bool ConfigGamepadAxis::is_inverted(void) const +{ + return m_inverted; +} + +void ConfigGamepadAxis::set_inverted(bool inverted) +{ + m_inverted = inverted; + m_full_string = std::format("{}:{}", m_name, m_inverted ? 1U : 0U); +} + +float ConfigGamepadAxis::get_value(const GLFWgamepadstate& state, float deadzone) const +{ + if(m_gamepad_axis <= vx::array_size(state.axes)) { + auto value = state.axes[m_gamepad_axis]; + + if(vx::abs(value) > deadzone) { + return m_inverted ? -value : value; + } + + return 0.0f; + } + + return 0.0f; +} + +const char* ConfigGamepadAxis::get_name(void) const +{ + return m_name; +} diff --git a/src/game/client/gamepad_axis.hh b/src/game/client/gamepad_axis.hh new file mode 100644 index 0000000..c0ed6ee --- /dev/null +++ b/src/game/client/gamepad_axis.hh @@ -0,0 +1,39 @@ +#ifndef CLIENT_GAMEPAD_AXIS_HH +#define CLIENT_GAMEPAD_AXIS_HH 1 +#pragma once + +#include "core/config.hh" + +struct GLFWgamepadstate; + +class ConfigGamepadAxis final : public IConfigValue { +public: + explicit ConfigGamepadAxis(void); + explicit ConfigGamepadAxis(int axis, bool inverted); + virtual ~ConfigGamepadAxis(void) = default; + + virtual const char* get(void) const override; + virtual void set(const char* value) override; + + int get_axis(void) const; + void set_axis(int axis); + + bool is_inverted(void) const; + void set_inverted(bool inverted); + + float get_value(const GLFWgamepadstate& state, float deadzone = 0.0f) const; + + // Conventional get/set methods implemented by + // this configuration value actually contain the + // inversion flag. Since we're updating that flag + // in the UI by means of a separate checkbox, we only need the name here + const char* get_name(void) const; + +private: + bool m_inverted; + int m_gamepad_axis; + std::string m_full_string; + const char* m_name; +}; + +#endif /* CLIENT_GAMEPAD_AXIS_HH */ diff --git a/src/game/client/gamepad_button.cc b/src/game/client/gamepad_button.cc new file mode 100644 index 0000000..dd3dca7 --- /dev/null +++ b/src/game/client/gamepad_button.cc @@ -0,0 +1,90 @@ +#include "client/pch.hh" + +#include "client/gamepad_button.hh" + +#include "core/constexpr.hh" + +#include "client/gamepad.hh" + +constexpr static const char* UNKNOWN_BUTTON_NAME = "UNKNOWN"; + +static const std::pair<int, const char*> button_names[] = { + { GLFW_GAMEPAD_BUTTON_A, "A" }, + { GLFW_GAMEPAD_BUTTON_B, "B" }, + { GLFW_GAMEPAD_BUTTON_X, "X" }, + { GLFW_GAMEPAD_BUTTON_Y, "Y" }, + { GLFW_GAMEPAD_BUTTON_LEFT_BUMPER, "L_BUMP" }, + { GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER, "R_BUMP" }, + { GLFW_GAMEPAD_BUTTON_BACK, "BACK" }, + { GLFW_GAMEPAD_BUTTON_START, "START" }, + { GLFW_GAMEPAD_BUTTON_GUIDE, "GUIDE" }, + { GLFW_GAMEPAD_BUTTON_LEFT_THUMB, "L_THUMB" }, + { GLFW_GAMEPAD_BUTTON_RIGHT_THUMB, "R_THUMB" }, + { GLFW_GAMEPAD_BUTTON_DPAD_UP, "DPAD_UP" }, + { GLFW_GAMEPAD_BUTTON_DPAD_RIGHT, "DPAD_RIGHT" }, + { GLFW_GAMEPAD_BUTTON_DPAD_DOWN, "DPAD_DOWN" }, + { GLFW_GAMEPAD_BUTTON_DPAD_LEFT, "DPAD_LEFT" }, +}; + +static const char* get_button_name(int button) +{ + for(const auto& it : button_names) { + if(it.first == button) { + return it.second; + } + } + + return UNKNOWN_BUTTON_NAME; +} + +ConfigGamepadButton::ConfigGamepadButton(void) +{ + m_gamepad_button = INVALID_GAMEPAD_BUTTON; + m_name = UNKNOWN_BUTTON_NAME; +} + +ConfigGamepadButton::ConfigGamepadButton(int button) +{ + m_gamepad_button = button; + m_name = get_button_name(button); +} + +const char* ConfigGamepadButton::get(void) const +{ + return m_name; +} + +void ConfigGamepadButton::set(const char* value) +{ + for(const auto& it : button_names) { + if(!std::strcmp(it.second, value)) { + m_gamepad_button = it.first; + m_name = it.second; + return; + } + } + + m_gamepad_button = INVALID_GAMEPAD_BUTTON; + m_name = UNKNOWN_BUTTON_NAME; +} + +int ConfigGamepadButton::get_button(void) const +{ + return m_gamepad_button; +} + +void ConfigGamepadButton::set_button(int button) +{ + m_gamepad_button = button; + m_name = get_button_name(button); +} + +bool ConfigGamepadButton::equals(int button) const +{ + return m_gamepad_button == button; +} + +bool ConfigGamepadButton::is_pressed(const GLFWgamepadstate& state) const +{ + return m_gamepad_button < vx::array_size(state.buttons) && state.buttons[m_gamepad_button] == GLFW_PRESS; +} diff --git a/src/game/client/gamepad_button.hh b/src/game/client/gamepad_button.hh new file mode 100644 index 0000000..04b3a41 --- /dev/null +++ b/src/game/client/gamepad_button.hh @@ -0,0 +1,29 @@ +#ifndef CLIENT_GAMEPAD_BUTTON_HH +#define CLIENT_GAMEPAD_BUTTON_HH 1 +#pragma once + +#include "core/config.hh" + +struct GLFWgamepadstate; + +class ConfigGamepadButton final : public IConfigValue { +public: + explicit ConfigGamepadButton(void); + explicit ConfigGamepadButton(int button); + virtual ~ConfigGamepadButton(void) = default; + + virtual const char* get(void) const override; + virtual void set(const char* value) override; + + int get_button(void) const; + void set_button(int button); + + bool equals(int button) const; + bool is_pressed(const GLFWgamepadstate& state) const; + +private: + int m_gamepad_button; + const char* m_name; +}; + +#endif /* CLIENT_GAMEPAD_BUTTON_HH */ diff --git a/src/game/client/glfw.hh b/src/game/client/glfw.hh new file mode 100644 index 0000000..9cdf734 --- /dev/null +++ b/src/game/client/glfw.hh @@ -0,0 +1,37 @@ +#ifndef CLIENTFW +#define CLIENTFW 1 +#pragma once + +struct GlfwCursorPosEvent final { + glm::fvec2 pos; +}; + +struct GlfwFramebufferSizeEvent final { + glm::ivec2 size; + float aspect; +}; + +struct GlfwJoystickEvent final { + int joystick_id; + int event_type; +}; + +struct GlfwKeyEvent final { + int key { GLFW_KEY_UNKNOWN }; + int scancode; + int action; + int mods; +}; + +struct GlfwMouseButtonEvent final { + int button { GLFW_KEY_UNKNOWN }; + int action; + int mods; +}; + +struct GlfwScrollEvent final { + float dx; + float dy; +}; + +#endif /* CLIENTFW */ diff --git a/src/game/client/globals.cc b/src/game/client/globals.cc new file mode 100644 index 0000000..6e00680 --- /dev/null +++ b/src/game/client/globals.cc @@ -0,0 +1,48 @@ +#include "client/pch.hh" + +#include "client/globals.hh" + +#include "core/config.hh" + +#include "client/gui_screen.hh" + +Config globals::client_config; + +GLFWwindow* globals::window; + +float globals::window_frametime; +float globals::window_frametime_avg; +std::uint64_t globals::window_frametime_us; +std::uint64_t globals::window_framecount; + +std::uint64_t globals::fixed_accumulator; + +int globals::width; +int globals::height; +float globals::aspect; + +GLuint globals::world_fbo; +GLuint globals::world_fbo_color; +GLuint globals::world_fbo_depth; + +std::size_t globals::num_drawcalls; +std::size_t globals::num_triangles; + +ENetHost* globals::client_host; + +Dimension* globals::dimension = nullptr; +entt::entity globals::player; + +ImFont* globals::font_debug; +ImFont* globals::font_default; +ImFont* globals::font_chat; + +ConfigKeyBind* globals::gui_keybind_ptr = nullptr; +ConfigGamepadAxis* globals::gui_gamepad_axis_ptr = nullptr; +ConfigGamepadButton* globals::gui_gamepad_button_ptr = nullptr; + +unsigned int globals::gui_scale = 0U; +unsigned int globals::gui_screen = GUI_SCREEN_NONE; + +ALCdevice* globals::sound_dev; +ALCcontext* globals::sound_ctx; diff --git a/src/game/client/globals.hh b/src/game/client/globals.hh new file mode 100644 index 0000000..3fc2223 --- /dev/null +++ b/src/game/client/globals.hh @@ -0,0 +1,66 @@ +#ifndef CLIENTOBALS_HH +#define CLIENTOBALS_HH 1 +#pragma once + +#include "shared/globals.hh" + +class Config; +class ConfigKeyBind; +class ConfigGamepadAxis; +class ConfigGamepadButton; + +struct GLFWwindow; +struct ImFont; + +class Dimension; + +namespace globals +{ +extern Config client_config; + +extern GLFWwindow* window; + +// Some gamesystems that aren't really +// gameplay-oriented might still use client +// framerate to interpolate discrete things +// so it's still a good idea to keep these available +extern float window_frametime; +extern float window_frametime_avg; +extern std::uint64_t window_frametime_us; +extern std::uint64_t window_framecount; + +// https://gafferongames.com/post/fix_your_timestep/ +extern std::uint64_t fixed_accumulator; + +extern int width; +extern int height; +extern float aspect; + +extern GLuint world_fbo; +extern GLuint world_fbo_color; +extern GLuint world_fbo_depth; + +extern std::size_t num_drawcalls; +extern std::size_t num_triangles; + +extern ENetHost* client_host; + +extern Dimension* dimension; +extern entt::entity player; + +extern ImFont* font_debug; +extern ImFont* font_default; +extern ImFont* font_chat; + +extern ConfigKeyBind* gui_keybind_ptr; +extern ConfigGamepadAxis* gui_gamepad_axis_ptr; +extern ConfigGamepadButton* gui_gamepad_button_ptr; + +extern unsigned int gui_scale; +extern unsigned int gui_screen; + +extern ALCdevice* sound_dev; +extern ALCcontext* sound_ctx; +} // namespace globals + +#endif /* CLIENTOBALS_HH */ diff --git a/src/game/client/gui_screen.hh b/src/game/client/gui_screen.hh new file mode 100644 index 0000000..edad116 --- /dev/null +++ b/src/game/client/gui_screen.hh @@ -0,0 +1,14 @@ +#ifndef CLIENT_GUI_SCREEN_HH +#define CLIENT_GUI_SCREEN_HH 1 +#pragma once + +constexpr static unsigned int GUI_SCREEN_NONE = 0x0000U; +constexpr static unsigned int GUI_MAIN_MENU = 0x0001U; +constexpr static unsigned int GUI_PLAY_MENU = 0x0002U; +constexpr static unsigned int GUI_SETTINGS = 0x0003U; +constexpr static unsigned int GUI_PROGRESS_BAR = 0x0004U; +constexpr static unsigned int GUI_MESSAGE_BOX = 0x0005U; +constexpr static unsigned int GUI_CHAT = 0x0006U; +constexpr static unsigned int GUI_DIRECT_CONNECTION = 0x0007U; + +#endif /* CLIENT_GUI_SCREEN_HH */ diff --git a/src/game/client/hotbar.cc b/src/game/client/hotbar.cc new file mode 100644 index 0000000..a2ed859 --- /dev/null +++ b/src/game/client/hotbar.cc @@ -0,0 +1,178 @@ +#include "client/pch.hh" + +#include "client/hotbar.hh" + +#include "core/config.hh" +#include "core/resource.hh" + +#include "shared/item_registry.hh" + +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/keybind.hh" +#include "client/settings.hh" +#include "client/status_lines.hh" +#include "client/texture_gui.hh" + +constexpr static float ITEM_SIZE = 20.0f; +constexpr static float ITEM_PADDING = 2.0f; +constexpr static float SELECTOR_PADDING = 1.0f; +constexpr static float HOTBAR_PADDING = 2.0f; + +unsigned int hotbar::active_slot = 0U; +item_id hotbar::slots[HOTBAR_SIZE]; + +static ConfigKeyBind hotbar_keys[HOTBAR_SIZE]; + +static resource_ptr<TextureGUI> hotbar_background; +static resource_ptr<TextureGUI> hotbar_selector; + +static ImU32 get_color_alpha(ImGuiCol style_color, float alpha) +{ + const auto& color = ImGui::GetStyleColorVec4(style_color); + return ImGui::GetColorU32(ImVec4(color.x, color.y, color.z, alpha)); +} + +static void update_hotbar_item(void) +{ + if(hotbar::slots[hotbar::active_slot] == NULL_ITEM_ID) { + status_lines::unset(STATUS_HOTBAR); + return; + } + + if(auto info = item_registry::find(hotbar::slots[hotbar::active_slot])) { + status_lines::set(STATUS_HOTBAR, info->name, ImVec4(1.0f, 1.0f, 1.0f, 1.0f), 5.0f); + return; + } +} + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if((event.action == GLFW_PRESS) && !globals::gui_screen) { + for(unsigned int i = 0U; i < HOTBAR_SIZE; ++i) { + if(hotbar_keys[i].equals(event.key)) { + hotbar::active_slot = i; + update_hotbar_item(); + break; + } + } + } +} + +static void on_glfw_scroll(const GlfwScrollEvent& event) +{ + if(!globals::gui_screen) { + if(event.dy < 0.0) { + hotbar::next_slot(); + return; + } + + if(event.dy > 0.0) { + hotbar::prev_slot(); + return; + } + } +} + +void hotbar::init(void) +{ + hotbar_keys[0].set_key(GLFW_KEY_1); + hotbar_keys[1].set_key(GLFW_KEY_2); + hotbar_keys[2].set_key(GLFW_KEY_3); + hotbar_keys[3].set_key(GLFW_KEY_4); + hotbar_keys[4].set_key(GLFW_KEY_5); + hotbar_keys[5].set_key(GLFW_KEY_6); + hotbar_keys[6].set_key(GLFW_KEY_7); + hotbar_keys[7].set_key(GLFW_KEY_8); + hotbar_keys[8].set_key(GLFW_KEY_9); + + globals::client_config.add_value("hotbar.key.0", hotbar_keys[0]); + globals::client_config.add_value("hotbar.key.1", hotbar_keys[1]); + globals::client_config.add_value("hotbar.key.3", hotbar_keys[2]); + globals::client_config.add_value("hotbar.key.4", hotbar_keys[3]); + globals::client_config.add_value("hotbar.key.5", hotbar_keys[4]); + globals::client_config.add_value("hotbar.key.6", hotbar_keys[5]); + globals::client_config.add_value("hotbar.key.7", hotbar_keys[6]); + globals::client_config.add_value("hotbar.key.8", hotbar_keys[7]); + globals::client_config.add_value("hotbar.key.9", hotbar_keys[8]); + + settings::add_keybind(10, hotbar_keys[0], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.0"); + settings::add_keybind(11, hotbar_keys[1], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.1"); + settings::add_keybind(12, hotbar_keys[2], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.2"); + settings::add_keybind(13, hotbar_keys[3], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.3"); + settings::add_keybind(14, hotbar_keys[4], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.4"); + settings::add_keybind(15, hotbar_keys[5], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.5"); + settings::add_keybind(16, hotbar_keys[6], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.6"); + settings::add_keybind(17, hotbar_keys[7], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.7"); + settings::add_keybind(18, hotbar_keys[8], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.8"); + + hotbar_background = resource::load<TextureGUI>("textures/gui/hud_hotbar.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + hotbar_selector = resource::load<TextureGUI>("textures/gui/hud_selector.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + + globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<GlfwScrollEvent>().connect<&on_glfw_scroll>(); +} + +void hotbar::deinit(void) +{ + hotbar_background = nullptr; + hotbar_selector = nullptr; +} + +void hotbar::layout(void) +{ + auto& style = ImGui::GetStyle(); + + auto item_size = ITEM_SIZE * globals::gui_scale; + auto hotbar_width = HOTBAR_SIZE * item_size; + auto hotbar_padding = HOTBAR_PADDING * globals::gui_scale; + + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetForegroundDrawList(); + + // Draw the hotbar background image + auto background_start = ImVec2(0.5f * viewport->Size.x - 0.5f * hotbar_width, viewport->Size.y - item_size - hotbar_padding); + auto background_end = ImVec2(background_start.x + hotbar_width, background_start.y + item_size); + draw_list->AddImage(hotbar_background->handle, background_start, background_end); + + // Draw the hotbar selector image + auto selector_padding_a = SELECTOR_PADDING * globals::gui_scale; + auto selector_padding_b = SELECTOR_PADDING * globals::gui_scale * 2.0f; + auto selector_start = ImVec2( + background_start.x + hotbar::active_slot * item_size - selector_padding_a, background_start.y - selector_padding_a); + auto selector_end = ImVec2(selector_start.x + item_size + selector_padding_b, selector_start.y + item_size + selector_padding_b); + draw_list->AddImage(hotbar_selector->handle, selector_start, selector_end); + + // Figure out item texture padding values + auto item_padding_a = ITEM_PADDING * globals::gui_scale; + auto item_padding_b = ITEM_PADDING * globals::gui_scale * 2.0f; + + // Draw individual item textures in the hotbar + for(std::size_t i = 0; i < HOTBAR_SIZE; ++i) { + const auto info = item_registry::find(hotbar::slots[i]); + + if((info == nullptr) || (info->cached_texture == nullptr)) { + // There's either no item in the slot + // or the item doesn't have a texture + continue; + } + + const auto item_start = ImVec2(background_start.x + i * item_size + item_padding_a, background_start.y + item_padding_a); + const auto item_end = ImVec2(item_start.x + item_size - item_padding_b, item_start.y + item_size - item_padding_b); + draw_list->AddImage(info->cached_texture->handle, item_start, item_end); + } +} + +void hotbar::next_slot(void) +{ + hotbar::active_slot += 1U; + hotbar::active_slot %= HOTBAR_SIZE; + update_hotbar_item(); +} + +void hotbar::prev_slot(void) +{ + hotbar::active_slot += HOTBAR_SIZE - 1U; + hotbar::active_slot %= HOTBAR_SIZE; + update_hotbar_item(); +} diff --git a/src/game/client/hotbar.hh b/src/game/client/hotbar.hh new file mode 100644 index 0000000..318c631 --- /dev/null +++ b/src/game/client/hotbar.hh @@ -0,0 +1,31 @@ +#ifndef CLIENT_HOTBAR_HH +#define CLIENT_HOTBAR_HH 1 +#pragma once + +#include "shared/types.hh" + +// TODO: design an inventory system and an item +// registry and integrate the hotbar into that system + +constexpr static unsigned int HOTBAR_SIZE = 9U; + +namespace hotbar +{ +extern unsigned int active_slot; +extern item_id slots[HOTBAR_SIZE]; +} // namespace hotbar + +namespace hotbar +{ +void init(void); +void deinit(void); +void layout(void); +} // namespace hotbar + +namespace hotbar +{ +void next_slot(void); +void prev_slot(void); +} // namespace hotbar + +#endif /* CLIENT_HOTBAR_HH */ diff --git a/src/game/client/imdraw_ext.cc b/src/game/client/imdraw_ext.cc new file mode 100644 index 0000000..67df3c8 --- /dev/null +++ b/src/game/client/imdraw_ext.cc @@ -0,0 +1,13 @@ +#include "client/pch.hh" + +#include "client/imdraw_ext.hh" + +#include "client/globals.hh" + +void imdraw_ext::text_shadow( + const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, ImDrawList* draw_list) +{ + const auto shadow_position = ImVec2(position.x + 0.5f * globals::gui_scale, position.y + 0.5f * globals::gui_scale); + draw_list->AddText(font, font->FontSize, shadow_position, shadow_color, text.c_str(), text.c_str() + text.size()); + draw_list->AddText(font, font->FontSize, position, text_color, text.c_str(), text.c_str() + text.size()); +} diff --git a/src/game/client/imdraw_ext.hh b/src/game/client/imdraw_ext.hh new file mode 100644 index 0000000..0a84e69 --- /dev/null +++ b/src/game/client/imdraw_ext.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_IMDRAW_EXT_HH +#define CLIENT_IMDRAW_EXT_HH 1 +#pragma once + +namespace imdraw_ext +{ +void text_shadow( + const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, ImDrawList* draw_list); +} // namespace imdraw_ext + +#endif /* CLIENT_IMDRAW_EXT_HH */ diff --git a/src/game/client/interpolation.cc b/src/game/client/interpolation.cc new file mode 100644 index 0000000..27b6dfd --- /dev/null +++ b/src/game/client/interpolation.cc @@ -0,0 +1,61 @@ +#include "client/pch.hh" + +#include "client/interpolation.hh" + +#include "core/constexpr.hh" + +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/head.hh" +#include "shared/transform.hh" + +#include "client/globals.hh" + +static void transform_interpolate(float alpha) +{ + auto group = globals::dimension->entities.group<TransformComponentIntr>(entt::get<TransformComponent, TransformComponentPrev>); + + for(auto [entity, interp, current, previous] : group.each()) { + interp.angles[0] = vx::lerp(previous.angles[0], current.angles[0], alpha); + interp.angles[1] = vx::lerp(previous.angles[1], current.angles[1], alpha); + interp.angles[2] = vx::lerp(previous.angles[2], current.angles[2], alpha); + + // Figure out previous chunk-local floating-point coordinates transformed + // to the current WorldCoord's chunk domain coordinates; we're interpolating + // against these instead of using previous.position.local to prevent jittering + auto previous_shift = coord::to_relative(current.chunk, current.local, previous.chunk, previous.local); + auto previous_local = current.local + previous_shift; + + interp.chunk.x = current.chunk.x; + interp.chunk.y = current.chunk.y; + interp.chunk.z = current.chunk.z; + + interp.local.x = vx::lerp(previous_local.x, current.local.x, alpha); + interp.local.y = vx::lerp(previous_local.y, current.local.y, alpha); + interp.local.z = vx::lerp(previous_local.z, current.local.z, alpha); + } +} + +static void head_interpolate(float alpha) +{ + auto group = globals::dimension->entities.group<HeadComponentIntr>(entt::get<HeadComponent, HeadComponentPrev>); + + for(auto [entity, interp, current, previous] : group.each()) { + interp.angles[0] = vx::lerp(previous.angles[0], current.angles[0], alpha); + interp.angles[1] = vx::lerp(previous.angles[1], current.angles[1], alpha); + interp.angles[2] = vx::lerp(previous.angles[2], current.angles[2], alpha); + + interp.offset.x = vx::lerp(previous.offset.x, current.offset.x, alpha); + interp.offset.y = vx::lerp(previous.offset.y, current.offset.y, alpha); + interp.offset.z = vx::lerp(previous.offset.z, current.offset.z, alpha); + } +} + +void interpolation::update(void) +{ + if(globals::dimension) { + auto alpha = static_cast<float>(globals::fixed_accumulator) / static_cast<float>(globals::fixed_frametime_us); + transform_interpolate(alpha); + head_interpolate(alpha); + } +}
\ No newline at end of file diff --git a/src/game/client/interpolation.hh b/src/game/client/interpolation.hh new file mode 100644 index 0000000..3565a26 --- /dev/null +++ b/src/game/client/interpolation.hh @@ -0,0 +1,10 @@ +#ifndef CLIENT_INTERPOLATION_HH +#define CLIENT_INTERPOLATION_HH 1 +#pragma once + +namespace interpolation +{ +void update(void); +} // namespace interpolation + +#endif /* CLIENT_INTERPOLATION_HH */ diff --git a/src/game/client/keybind.cc b/src/game/client/keybind.cc new file mode 100644 index 0000000..d47397d --- /dev/null +++ b/src/game/client/keybind.cc @@ -0,0 +1,200 @@ +#include "client/pch.hh" + +#include "client/keybind.hh" + +#include "core/constexpr.hh" + +#include "client/const.hh" + +constexpr static const char* UNKNOWN_KEY_NAME = "UNKNOWN"; + +static const std::pair<int, const char*> key_names[] = { + { GLFW_KEY_SPACE, "SPACE" }, + { GLFW_KEY_APOSTROPHE, "'" }, + { GLFW_KEY_COMMA, "," }, + { GLFW_KEY_MINUS, "-" }, + { GLFW_KEY_PERIOD, "." }, + { GLFW_KEY_SLASH, "/" }, + { GLFW_KEY_0, "0" }, + { GLFW_KEY_1, "1" }, + { GLFW_KEY_2, "2" }, + { GLFW_KEY_3, "3" }, + { GLFW_KEY_4, "4" }, + { GLFW_KEY_5, "5" }, + { GLFW_KEY_6, "6" }, + { GLFW_KEY_7, "7" }, + { GLFW_KEY_8, "8" }, + { GLFW_KEY_9, "9" }, + { GLFW_KEY_SEMICOLON, ";" }, + { GLFW_KEY_EQUAL, "=" }, + { GLFW_KEY_A, "A" }, + { GLFW_KEY_B, "B" }, + { GLFW_KEY_C, "C" }, + { GLFW_KEY_D, "D" }, + { GLFW_KEY_E, "E" }, + { GLFW_KEY_F, "F" }, + { GLFW_KEY_G, "G" }, + { GLFW_KEY_H, "H" }, + { GLFW_KEY_I, "I" }, + { GLFW_KEY_J, "J" }, + { GLFW_KEY_K, "K" }, + { GLFW_KEY_L, "L" }, + { GLFW_KEY_M, "M" }, + { GLFW_KEY_N, "N" }, + { GLFW_KEY_O, "O" }, + { GLFW_KEY_P, "P" }, + { GLFW_KEY_Q, "Q" }, + { GLFW_KEY_R, "R" }, + { GLFW_KEY_S, "S" }, + { GLFW_KEY_T, "T" }, + { GLFW_KEY_U, "U" }, + { GLFW_KEY_V, "V" }, + { GLFW_KEY_W, "W" }, + { GLFW_KEY_X, "X" }, + { GLFW_KEY_Y, "Y" }, + { GLFW_KEY_Z, "Z" }, + { GLFW_KEY_LEFT_BRACKET, "[" }, + { GLFW_KEY_BACKSLASH, "\\" }, + { GLFW_KEY_RIGHT_BRACKET, "]" }, + { GLFW_KEY_GRAVE_ACCENT, "`" }, + { GLFW_KEY_WORLD_1, "WORLD_1" }, + { GLFW_KEY_WORLD_2, "WORLD_2" }, + { GLFW_KEY_ESCAPE, "ESCAPE" }, + { GLFW_KEY_ENTER, "ENTER" }, + { GLFW_KEY_TAB, "TAB" }, + { GLFW_KEY_BACKSPACE, "BACKSPACE" }, + { GLFW_KEY_INSERT, "INSERT" }, + { GLFW_KEY_DELETE, "DELETE" }, + { GLFW_KEY_RIGHT, "RIGHT" }, + { GLFW_KEY_LEFT, "LEFT" }, + { GLFW_KEY_DOWN, "DOWN" }, + { GLFW_KEY_UP, "UP" }, + { GLFW_KEY_PAGE_UP, "PAGE_UP" }, + { GLFW_KEY_PAGE_DOWN, "PAGE_DOWN" }, + { GLFW_KEY_HOME, "HOME" }, + { GLFW_KEY_END, "END" }, + { GLFW_KEY_CAPS_LOCK, "CAPS_LOCK" }, + { GLFW_KEY_SCROLL_LOCK, "SCROLL_LOCK" }, + { GLFW_KEY_NUM_LOCK, "NUM_LOCK" }, + { GLFW_KEY_PRINT_SCREEN, "PRINT_SCREEN" }, + { GLFW_KEY_PAUSE, "PAUSE" }, + { GLFW_KEY_F1, "F1" }, + { GLFW_KEY_F2, "F2" }, + { GLFW_KEY_F3, "F3" }, + { GLFW_KEY_F4, "F4" }, + { GLFW_KEY_F5, "F5" }, + { GLFW_KEY_F6, "F6" }, + { GLFW_KEY_F7, "F7" }, + { GLFW_KEY_F8, "F8" }, + { GLFW_KEY_F9, "F9" }, + { GLFW_KEY_F10, "F10" }, + { GLFW_KEY_F11, "F11" }, + { GLFW_KEY_F12, "F12" }, + { GLFW_KEY_F13, "F13" }, + { GLFW_KEY_F14, "F14" }, + { GLFW_KEY_F15, "F15" }, + { GLFW_KEY_F16, "F16" }, + { GLFW_KEY_F17, "F17" }, + { GLFW_KEY_F18, "F18" }, + { GLFW_KEY_F19, "F19" }, + { GLFW_KEY_F20, "F20" }, + { GLFW_KEY_F21, "F21" }, + { GLFW_KEY_F22, "F22" }, + { GLFW_KEY_F23, "F23" }, + { GLFW_KEY_F24, "F24" }, + { GLFW_KEY_F25, "F25" }, + { GLFW_KEY_KP_0, "KEYPAD_0" }, + { GLFW_KEY_KP_1, "KEYPAD_1" }, + { GLFW_KEY_KP_2, "KEYPAD_2" }, + { GLFW_KEY_KP_3, "KEYPAD_3" }, + { GLFW_KEY_KP_4, "KEYPAD_4" }, + { GLFW_KEY_KP_5, "KEYPAD_5" }, + { GLFW_KEY_KP_6, "KEYPAD_6" }, + { GLFW_KEY_KP_7, "KEYPAD_7" }, + { GLFW_KEY_KP_8, "KEYPAD_8" }, + { GLFW_KEY_KP_9, "KEYPAD_9" }, + { GLFW_KEY_KP_DECIMAL, "KEYPAD_POINT" }, + { GLFW_KEY_KP_DIVIDE, "KEYPAD_DIV" }, + { GLFW_KEY_KP_MULTIPLY, "KEYPAD_MUL" }, + { GLFW_KEY_KP_SUBTRACT, "KEYPAD_MINUS" }, + { GLFW_KEY_KP_ADD, "KEYPAD_PLUS" }, + { GLFW_KEY_KP_ENTER, "KEYPAD_ENTER" }, + { GLFW_KEY_KP_EQUAL, "KEYPAD_EQUAL" }, + { GLFW_KEY_LEFT_SHIFT, "LEFT_SHIFT" }, + { GLFW_KEY_LEFT_CONTROL, "LEFT_CTRL" }, + { GLFW_KEY_LEFT_ALT, "LEFT_ALT" }, + { GLFW_KEY_LEFT_SUPER, "LEFT_SUPER" }, + { GLFW_KEY_RIGHT_SHIFT, "RIGHT_SHIFT" }, + { GLFW_KEY_RIGHT_CONTROL, "RIGHT_CTRL" }, + { GLFW_KEY_RIGHT_ALT, "RIGHT_ALT" }, + { GLFW_KEY_RIGHT_SUPER, "RIGHT_SUPER" }, + { GLFW_KEY_MENU, "MENU" }, +}; + +static const char* get_key_name(int keycode) +{ + for(const auto& it : key_names) { + if(it.first == keycode) { + return it.second; + } + } + + return UNKNOWN_KEY_NAME; +} + +ConfigKeyBind::ConfigKeyBind(void) +{ + m_glfw_keycode = GLFW_KEY_UNKNOWN; + m_name = UNKNOWN_KEY_NAME; +} + +ConfigKeyBind::ConfigKeyBind(int default_value) +{ + if(default_value == DEBUG_KEY) { + m_glfw_keycode = GLFW_KEY_UNKNOWN; + m_name = UNKNOWN_KEY_NAME; + } else { + m_glfw_keycode = default_value; + m_name = get_key_name(default_value); + } +} + +void ConfigKeyBind::set(const char* value) +{ + for(const auto& it : key_names) { + if((it.first != DEBUG_KEY) && !std::strcmp(it.second, value)) { + m_glfw_keycode = it.first; + m_name = it.second; + return; + } + } + + m_glfw_keycode = GLFW_KEY_UNKNOWN; + m_name = UNKNOWN_KEY_NAME; +} + +const char* ConfigKeyBind::get(void) const +{ + return m_name; +} + +void ConfigKeyBind::set_key(int keycode) +{ + if(keycode == DEBUG_KEY) { + m_glfw_keycode = GLFW_KEY_UNKNOWN; + m_name = UNKNOWN_KEY_NAME; + } else { + m_glfw_keycode = keycode; + m_name = get_key_name(keycode); + } +} + +int ConfigKeyBind::get_key(void) const +{ + return m_glfw_keycode; +} + +bool ConfigKeyBind::equals(int keycode) const +{ + return m_glfw_keycode == keycode; +} diff --git a/src/game/client/keybind.hh b/src/game/client/keybind.hh new file mode 100644 index 0000000..8cf3c3c --- /dev/null +++ b/src/game/client/keybind.hh @@ -0,0 +1,26 @@ +#ifndef CLIENT_KEYBIND_HH +#define CLIENT_KEYBIND_HH 1 +#pragma once + +#include "core/config.hh" + +class ConfigKeyBind final : public IConfigValue { +public: + explicit ConfigKeyBind(void); + explicit ConfigKeyBind(int default_value); + virtual ~ConfigKeyBind(void) = default; + + virtual void set(const char* value) override; + virtual const char* get(void) const override; + + void set_key(int keycode); + int get_key(void) const; + + bool equals(int keycode) const; + +private: + const char* m_name; + int m_glfw_keycode; +}; + +#endif /* CLIENT_KEYBIND_HH */ diff --git a/src/game/client/language.cc b/src/game/client/language.cc new file mode 100644 index 0000000..2ae0bc6 --- /dev/null +++ b/src/game/client/language.cc @@ -0,0 +1,196 @@ +#include "client/pch.hh" + +#include "client/language.hh" + +#include "core/config.hh" + +#include "client/globals.hh" +#include "client/settings.hh" + +constexpr static const char* DEFAULT_LANGUAGE = "en_US"; + +// Available languages are kept in a special manifest file which +// is essentially a key-value map of semi-IETF-compliant language tags +// and the language's endonym; after reading the manifest, the translation +// system knows what language it can load and will act accordingly +constexpr static const char* MANIFEST_PATH = "lang/manifest.json"; + +static LanguageManifest manifest; +static LanguageIterator current_language; +static std::unordered_map<std::string, std::string> language_map; +static std::unordered_map<std::string, LanguageIterator> ietf_map; +static ConfigString config_language(DEFAULT_LANGUAGE); + +static void send_language_event(LanguageIterator new_language) +{ + LanguageSetEvent event; + event.new_language = new_language; + globals::dispatcher.trigger(event); +} + +void language::init(void) +{ + globals::client_config.add_value("language", config_language); + + settings::add_language_select(0, settings_location::GENERAL, "language"); + + auto file = PHYSFS_openRead(MANIFEST_PATH); + + if(file == nullptr) { + spdlog::critical("language: {}: {}", MANIFEST_PATH, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + std::terminate(); + } + + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + auto jsonv = json_parse_string(source.c_str()); + const auto json = json_value_get_object(jsonv); + const auto count = json_object_get_count(json); + + if((jsonv == nullptr) || (json == nullptr) || (count == 0)) { + spdlog::critical("language: {}: parse error", MANIFEST_PATH); + json_value_free(jsonv); + std::terminate(); + } + + for(std::size_t i = 0; i < count; ++i) { + const auto ietf = json_object_get_name(json, i); + const auto value = json_object_get_value_at(json, i); + const auto endonym = json_value_get_string(value); + + if(ietf && endonym) { + LanguageInfo info; + info.ietf = std::string(ietf); + info.endonym = std::string(endonym); + info.display = std::format("{} ({})", endonym, ietf); + manifest.push_back(info); + } + } + + for(auto it = manifest.cbegin(); it != manifest.cend(); ++it) { + ietf_map.emplace(it->ietf, it); + } + + json_value_free(jsonv); + + // This is temporary! This value will + // be overriden in init_late after the + // config system updates config_language + current_language = manifest.cend(); +} + +void language::init_late(void) +{ + auto user_language = ietf_map.find(config_language.get()); + + if(user_language != ietf_map.cend()) { + language::set(user_language->second); + return; + } + + auto fallback = ietf_map.find(DEFAULT_LANGUAGE); + + if(fallback != ietf_map.cend()) { + language::set(fallback->second); + return; + } + + spdlog::critical("language: we're doomed!"); + spdlog::critical("language: {} doesn't exist!", DEFAULT_LANGUAGE); + std::terminate(); +} + +void language::set(LanguageIterator new_language) +{ + if(new_language != manifest.cend()) { + auto path = std::format("lang/lang.{}.json", new_language->ietf); + + auto file = PHYSFS_openRead(path.c_str()); + + if(file == nullptr) { + spdlog::warn("language: {}: {}", path, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + send_language_event(new_language); + return; + } + + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + auto jsonv = json_parse_string(source.c_str()); + const auto json = json_value_get_object(jsonv); + const auto count = json_object_get_count(json); + + if((jsonv == nullptr) || (json == nullptr) || (count == 0)) { + spdlog::warn("language: {}: parse error", path); + send_language_event(new_language); + json_value_free(jsonv); + return; + } + + language_map.clear(); + + for(size_t i = 0; i < count; ++i) { + const auto key = json_object_get_name(json, i); + const auto value = json_object_get_value_at(json, i); + const auto value_str = json_value_get_string(value); + + if(key && value_str) { + language_map.emplace(key, value_str); + continue; + } + } + + json_value_free(jsonv); + + current_language = new_language; + config_language.set(new_language->ietf.c_str()); + } + + send_language_event(new_language); +} + +LanguageIterator language::get_current(void) +{ + return current_language; +} + +LanguageIterator language::find(const char* ietf) +{ + const auto it = ietf_map.find(ietf); + if(it != ietf_map.cend()) { + return it->second; + } else { + return manifest.cend(); + } +} + +LanguageIterator language::cbegin(void) +{ + return manifest.cbegin(); +} + +LanguageIterator language::cend(void) +{ + return manifest.cend(); +} + +const char* language::resolve(const char* key) +{ + const auto it = language_map.find(key); + if(it != language_map.cend()) { + return it->second.c_str(); + } else { + return key; + } +} + +std::string language::resolve_gui(const char* key) +{ + // We need window tags to retain their hierarchy when a language + // dynamically changes; ImGui allows to provide hidden unique identifiers + // to GUI primitives that have their name change dynamically, so we're using this + return std::format("{}###{}", language::resolve(key), key); +} diff --git a/src/game/client/language.hh b/src/game/client/language.hh new file mode 100644 index 0000000..680cd92 --- /dev/null +++ b/src/game/client/language.hh @@ -0,0 +1,43 @@ +#ifndef CLIENT_LANGUAGE_HH +#define CLIENT_LANGUAGE_HH 1 +#pragma once + +struct LanguageInfo final { + std::string endonym; // Language's self-name + std::string display; // Display for the settings GUI + std::string ietf; // Semi-compliant language abbreviation +}; + +using LanguageManifest = std::vector<LanguageInfo>; +using LanguageIterator = LanguageManifest::const_iterator; + +struct LanguageSetEvent final { + LanguageIterator new_language; +}; + +namespace language +{ +void init(void); +void init_late(void); +} // namespace language + +namespace language +{ +void set(LanguageIterator new_language); +} // namespace language + +namespace language +{ +LanguageIterator get_current(void); +LanguageIterator find(const char* ietf); +LanguageIterator cbegin(void); +LanguageIterator cend(void); +} // namespace language + +namespace language +{ +const char* resolve(const char* key); +std::string resolve_gui(const char* key); +} // namespace language + +#endif /* CLIENT_LANGUAGE_HH */ diff --git a/src/game/client/listener.cc b/src/game/client/listener.cc new file mode 100644 index 0000000..6b691eb --- /dev/null +++ b/src/game/client/listener.cc @@ -0,0 +1,38 @@ +#include "client/pch.hh" + +#include "client/listener.hh" + +#include "core/config.hh" +#include "core/constexpr.hh" + +#include "shared/dimension.hh" +#include "shared/velocity.hh" + +#include "client/camera.hh" +#include "client/const.hh" +#include "client/globals.hh" +#include "client/session.hh" +#include "client/sound.hh" + +void listener::update(void) +{ + if(session::is_ingame()) { + const auto& velocity = globals::dimension->entities.get<VelocityComponent>(globals::player).value; + const auto& position = camera::position_local; + + alListener3f(AL_POSITION, position.x, position.y, position.z); + alListener3f(AL_VELOCITY, velocity.x, velocity.y, velocity.z); + + float orientation[6]; + orientation[0] = camera::direction.x; + orientation[1] = camera::direction.y; + orientation[2] = camera::direction.z; + orientation[3] = DIR_UP<float>.x; + orientation[4] = DIR_UP<float>.y; + orientation[5] = DIR_UP<float>.z; + + alListenerfv(AL_ORIENTATION, orientation); + } + + alListenerf(AL_GAIN, vx::clamp(sound::volume_master.get_value() * 0.01f, 0.0f, 1.0f)); +} diff --git a/src/game/client/listener.hh b/src/game/client/listener.hh new file mode 100644 index 0000000..731babc --- /dev/null +++ b/src/game/client/listener.hh @@ -0,0 +1,10 @@ +#ifndef CLIENT_LISTENER_HH +#define CLIENT_LISTENER_HH 1 +#pragma once + +namespace listener +{ +void update(void); +} // namespace listener + +#endif /* CLIENT_LISTENER_HH */ diff --git a/src/game/client/main.cc b/src/game/client/main.cc new file mode 100644 index 0000000..d9e915c --- /dev/null +++ b/src/game/client/main.cc @@ -0,0 +1,441 @@ +#include "client/pch.hh" + +#include "core/binfile.hh" +#include "core/cmdline.hh" +#include "core/config.hh" +#include "core/epoch.hh" +#include "core/image.hh" +#include "core/resource.hh" +#include "core/version.hh" + +#include "shared/game.hh" +#include "shared/splash.hh" +#include "shared/threading.hh" + +#include "client/const.hh" +#include "client/game.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/sound_effect.hh" +#include "client/texture_gui.hh" +#include "client/window_title.hh" + +#if defined(_WIN32) +extern "C" __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; +#endif + +static void on_glfw_error(int code, const char* message) +{ + spdlog::error("glfw: {}", message); +} + +static void on_glfw_char(GLFWwindow* window, unsigned int codepoint) +{ + ImGui_ImplGlfw_CharCallback(window, codepoint); +} + +static void on_glfw_cursor_enter(GLFWwindow* window, int entered) +{ + ImGui_ImplGlfw_CursorEnterCallback(window, entered); +} + +static void on_glfw_cursor_pos(GLFWwindow* window, double xpos, double ypos) +{ + GlfwCursorPosEvent event; + event.pos.x = static_cast<float>(xpos); + event.pos.y = static_cast<float>(ypos); + globals::dispatcher.trigger(event); + + ImGui_ImplGlfw_CursorPosCallback(window, xpos, ypos); +} + +static void on_glfw_framebuffer_size(GLFWwindow* window, int width, int height) +{ + if(glfwGetWindowAttrib(window, GLFW_ICONIFIED)) { + // Don't do anything if the window was just + // iconified (minimized); as it turns out minimized + // windows on WIN32 seem to be forced into 0x0 + return; + } + + globals::width = width; + globals::height = height; + globals::aspect = static_cast<float>(width) / static_cast<float>(height); + + GlfwFramebufferSizeEvent fb_event; + fb_event.size.x = globals::width; + fb_event.size.y = globals::height; + fb_event.aspect = globals::aspect; + globals::dispatcher.trigger(fb_event); +} + +static void on_glfw_key(GLFWwindow* window, int key, int scancode, int action, int mods) +{ + GlfwKeyEvent event; + event.key = key; + event.scancode = scancode; + event.action = action; + event.mods = mods; + globals::dispatcher.trigger(event); + + ImGui_ImplGlfw_KeyCallback(window, key, scancode, action, mods); +} + +static void on_glfw_joystick(int joystick_id, int event_type) +{ + GlfwJoystickEvent event; + event.joystick_id = joystick_id; + event.event_type = event_type; + globals::dispatcher.trigger(event); +} + +static void on_glfw_monitor_event(GLFWmonitor* monitor, int event) +{ + ImGui_ImplGlfw_MonitorCallback(monitor, event); +} + +static void on_glfw_mouse_button(GLFWwindow* window, int button, int action, int mods) +{ + GlfwMouseButtonEvent event; + event.button = button; + event.action = action; + event.mods = mods; + globals::dispatcher.trigger(event); + + ImGui_ImplGlfw_MouseButtonCallback(window, button, action, mods); +} + +static void on_glfw_scroll(GLFWwindow* window, double dx, double dy) +{ + GlfwScrollEvent event; + event.dx = static_cast<float>(dx); + event.dy = static_cast<float>(dy); + globals::dispatcher.trigger(event); + + ImGui_ImplGlfw_ScrollCallback(window, dx, dy); +} + +static void on_glfw_window_focus(GLFWwindow* window, int focused) +{ + ImGui_ImplGlfw_WindowFocusCallback(window, focused); +} + +static void GLAD_API_PTR on_opengl_message( + GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* param) +{ + spdlog::info("opengl: {}", reinterpret_cast<const char*>(message)); +} + +static void on_termination_signal(int) +{ + spdlog::warn("client: received termination signal"); + glfwSetWindowShouldClose(globals::window, true); +} + +int main(int argc, char** argv) +{ + cmdline::create(argc, argv); + +#if defined(_WIN32) +#if defined(NDEBUG) + if(GetConsoleWindow() && !cmdline::contains("debug")) { + // Hide the console window on release builds + // unless explicitly specified to preserve it instead + FreeConsole(); + } +#else + if(GetConsoleWindow() && cmdline::contains("nodebug")) { + // Hide the console window on debug builds when + // explicitly specified by the user to hide it + FreeConsole(); + } +#endif +#endif + + shared_game::init(argc, argv); + + spdlog::info("Voxelius Client {}", project_version_string); + + glfwSetErrorCallback(&on_glfw_error); + +#if defined(__unix__) + // Wayland constantly throws random bullshit at me + // when I'm dealing with pretty much anything cross-platform + // on pretty much any kind of UNIX and Linux distribution + glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); +#endif + + if(!glfwInit()) { + spdlog::critical("glfw: init failed"); + std::terminate(); + } + + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_SAMPLES, 0); + + globals::window = glfwCreateWindow(DEFAULT_WIDTH, DEFAULT_HEIGHT, "Client", nullptr, nullptr); + + if(!globals::window) { + spdlog::critical("glfw: failed to open a window"); + std::terminate(); + } + + std::signal(SIGINT, &on_termination_signal); + std::signal(SIGTERM, &on_termination_signal); + + glfwMakeContextCurrent(globals::window); + glfwSwapInterval(1); + + if(!gladLoadGL(&glfwGetProcAddress)) { + spdlog::critical("glad: failed to load function pointers"); + std::terminate(); + } + + if(GLAD_GL_KHR_debug) { + if(!cmdline::contains("nodebug")) { + glEnable(GL_DEBUG_OUTPUT); + glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); + glDebugMessageCallback(&on_opengl_message, nullptr); + + // NVIDIA drivers tend to spam quote-unquote "useful" + // information about buffer usage into the debug callback + static const std::uint32_t ignore_nvidia_131185 = 131185; + glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER, GL_DONT_CARE, 1, &ignore_nvidia_131185, GL_FALSE); + } else { + spdlog::warn("glad: nodebug command line parameter found"); + spdlog::warn("glad: OpenGL errors will not be logged"); + } + } else { + spdlog::warn("glad: KHR_debug extension not supported"); + spdlog::warn("glad: OpenGL errors will not be logged"); + } + + spdlog::info("opengl: version: {}", reinterpret_cast<const char*>(glGetString(GL_VERSION))); + spdlog::info("opengl: renderer: {}", reinterpret_cast<const char*>(glGetString(GL_RENDERER))); + + glDisable(GL_MULTISAMPLE); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + ImGui_ImplGlfw_InitForOpenGL(globals::window, false); + ImGui_ImplOpenGL3_Init(nullptr); + + // The UI is scaled against a resolution defined by BASE_WIDTH and BASE_HEIGHT + // constants. However, UI scale of 1 doesn't look that good, so the window size is + // limited to a resolution that allows at least UI scale of 2 and is defined by MIN_WIDTH and MIN_HEIGHT. + glfwSetWindowSizeLimits(globals::window, MIN_WIDTH, MIN_HEIGHT, GLFW_DONT_CARE, GLFW_DONT_CARE); + + glfwSetCharCallback(globals::window, &on_glfw_char); + glfwSetCursorEnterCallback(globals::window, &on_glfw_cursor_enter); + glfwSetCursorPosCallback(globals::window, &on_glfw_cursor_pos); + glfwSetFramebufferSizeCallback(globals::window, &on_glfw_framebuffer_size); + glfwSetKeyCallback(globals::window, &on_glfw_key); + glfwSetMouseButtonCallback(globals::window, &on_glfw_mouse_button); + glfwSetScrollCallback(globals::window, &on_glfw_scroll); + glfwSetWindowFocusCallback(globals::window, &on_glfw_window_focus); + + glfwSetJoystickCallback(&on_glfw_joystick); + glfwSetMonitorCallback(&on_glfw_monitor_event); + + if(auto image = resource::load<Image>("textures/gui/window_icon.png")) { + GLFWimage icon_image; + icon_image.width = image->size.x; + icon_image.height = image->size.y; + icon_image.pixels = reinterpret_cast<unsigned char*>(image->pixels); + glfwSetWindowIcon(globals::window, 1, &icon_image); + } + + if(cmdline::contains("nosound")) { + spdlog::warn("client: sound disabled [per command line]"); + globals::sound_dev = nullptr; + globals::sound_ctx = nullptr; + } else { + if(!saladLoadALdefault()) { + spdlog::warn("client: sound disabled [openal loading failed]"); + globals::sound_dev = nullptr; + globals::sound_ctx = nullptr; + } else { + globals::sound_dev = alcOpenDevice(nullptr); + + if(globals::sound_dev == nullptr) { + spdlog::warn("client: sound disabled [no device]"); + globals::sound_ctx = nullptr; + } else { + spdlog::info("sound: {}", reinterpret_cast<const char*>(alcGetString(globals::sound_dev, ALC_DEVICE_SPECIFIER))); + + globals::sound_ctx = alcCreateContext(globals::sound_dev, nullptr); + + if(globals::sound_ctx == nullptr) { + spdlog::warn("client: sound disabled [context creation failed]"); + alcCloseDevice(globals::sound_dev); + globals::sound_dev = nullptr; + } else { + alcMakeContextCurrent(globals::sound_ctx); + } + } + } + } + + splash::init_client(); + + window_title::update(); + + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + globals::fixed_frametime = 0.0f; + globals::fixed_frametime_avg = 0.0f; + globals::fixed_frametime_us = UINT64_MAX; + globals::fixed_framecount = 0; + + globals::curtime = epoch::microseconds(); + + globals::window_frametime = 0.0f; + globals::window_frametime_avg = 0.0f; + globals::window_frametime_us = 0; + globals::window_framecount = 0; + + int vmode_width = DEFAULT_WIDTH; + int vmode_height = DEFAULT_HEIGHT; + + if(auto vmode = cmdline::get("mode")) { + std::sscanf(vmode, "%dx%d", &vmode_width, &vmode_height); + vmode_height = vx::max(vmode_height, MIN_HEIGHT); + vmode_width = vx::max(vmode_width, MIN_WIDTH); + } + + glfwSetWindowSize(globals::window, vmode_width, vmode_height); + + client_game::init(); + + int wwidth, wheight; + glfwGetFramebufferSize(globals::window, &wwidth, &wheight); + on_glfw_framebuffer_size(globals::window, wwidth, wheight); + + threading::init(); + + globals::client_config.load_file("client.conf"); + globals::client_config.load_cmdline(); + + client_game::init_late(); + + auto last_curtime = globals::curtime; + + while(!glfwWindowShouldClose(globals::window)) { + globals::curtime = epoch::microseconds(); + + globals::window_frametime_us = globals::curtime - last_curtime; + globals::window_frametime = static_cast<float>(globals::window_frametime_us) / 1000000.0f; + globals::window_frametime_avg += globals::window_frametime; + globals::window_frametime_avg *= 0.5f; + + if(globals::fixed_frametime_us == UINT64_MAX) { + globals::fixed_framecount = 0; + globals::fixed_accumulator = 0; + } else { + globals::fixed_accumulator += globals::window_frametime_us; + globals::fixed_framecount = globals::fixed_accumulator / globals::fixed_frametime_us; + globals::fixed_accumulator %= globals::fixed_frametime_us; + } + + globals::num_drawcalls = 0; + globals::num_triangles = 0; + + last_curtime = globals::curtime; + + for(std::uint64_t i = 0; i < globals::fixed_framecount; ++i) + client_game::fixed_update(); + client_game::update(); + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + glDisable(GL_BLEND); + + glDisable(GL_DEPTH_TEST); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, globals::width, globals::height); + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + // Make sure there is no stray program object + // being bound to the context. Usually third-party + // overlay software (such as RivaTuner) injects itself + // into the rendering loop and binds internal objects, + // which creates an incomprehensible visual mess + glUseProgram(0); + + client_game::render(); + + client_game::layout(); + + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + glfwSwapBuffers(globals::window); + + for(std::uint64_t i = 0; i < globals::fixed_framecount; ++i) + client_game::fixed_update_late(); + client_game::update_late(); + + glfwPollEvents(); + + // EnTT provides two ways of dispatching events: + // queued and immediate. When glfwPollEvents() is + // called, immediate events are triggered across + // the application, whilst queued ones are triggered + // later by calling entt::dispatcher::update() + globals::dispatcher.update(); + + globals::window_framecount += 1; + + resource::soft_cleanup<BinFile>(); + resource::soft_cleanup<Image>(); + + resource::soft_cleanup<SoundEffect>(); + resource::soft_cleanup<TextureGUI>(); + + threading::update(); + } + + client_game::deinit(); + + resource::hard_cleanup<BinFile>(); + resource::hard_cleanup<Image>(); + + resource::hard_cleanup<SoundEffect>(); + resource::hard_cleanup<TextureGUI>(); + + spdlog::info("client: shutdown after {} frames", globals::window_framecount); + spdlog::info("client: average framerate: {:.03f} FPS", 1.0f / globals::window_frametime_avg); + spdlog::info("client: average frametime: {:.03f} ms", 1000.0f * globals::window_frametime_avg); + + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + if(globals::sound_ctx) { + alcMakeContextCurrent(nullptr); + alcDestroyContext(globals::sound_ctx); + alcCloseDevice(globals::sound_dev); + } + + glfwDestroyWindow(globals::window); + glfwTerminate(); + + globals::client_config.save_file("client.conf"); + + threading::deinit(); + + shared_game::deinit(); + + return EXIT_SUCCESS; +} diff --git a/src/game/client/main_menu.cc b/src/game/client/main_menu.cc new file mode 100644 index 0000000..39763ec --- /dev/null +++ b/src/game/client/main_menu.cc @@ -0,0 +1,163 @@ +#include "client/pch.hh" + +#include "client/main_menu.hh" + +#include "core/constexpr.hh" +#include "core/resource.hh" +#include "core/version.hh" + +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/language.hh" +#include "client/session.hh" +#include "client/texture_gui.hh" +#include "client/window_title.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +static std::string str_play; +static std::string str_resume; +static std::string str_settings; +static std::string str_leave; +static std::string str_quit; + +static resource_ptr<TextureGUI> title; +static float title_aspect; + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if(session::is_ingame() && (event.key == GLFW_KEY_ESCAPE) && (event.action == GLFW_PRESS)) { + if(globals::gui_screen == GUI_SCREEN_NONE) { + globals::gui_screen = GUI_MAIN_MENU; + return; + } + + if(globals::gui_screen == GUI_MAIN_MENU) { + globals::gui_screen = GUI_SCREEN_NONE; + return; + } + } +} + +static void on_language_set(const LanguageSetEvent& event) +{ + str_play = language::resolve_gui("main_menu.play"); + str_resume = language::resolve_gui("main_menu.resume"); + str_settings = language::resolve("main_menu.settings"); + str_leave = language::resolve("main_menu.leave"); + str_quit = language::resolve("main_menu.quit"); +} + +void main_menu::init(void) +{ + title = resource::load<TextureGUI>("textures/gui/menu_title.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + + if(title == nullptr) { + spdlog::critical("main_menu: texture load failed"); + std::terminate(); + } + + if(title->size.x > title->size.y) { + title_aspect = static_cast<float>(title->size.x) / static_cast<float>(title->size.y); + } else { + title_aspect = static_cast<float>(title->size.y) / static_cast<float>(title->size.x); + } + + globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<LanguageSetEvent>().connect<&on_language_set>(); +} + +void main_menu::deinit(void) +{ + title = nullptr; +} + +void main_menu::layout(void) +{ + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(0.0f, viewport->Size.y * 0.15f); + const auto window_size = ImVec2(viewport->Size.x, viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###main_menu", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 2.0f * globals::gui_scale)); + + if(session::is_ingame()) { + ImGui::Dummy(ImVec2(0.0f, 32.0f * globals::gui_scale)); + } else { + auto reference_height = 0.225f * window_size.y; + auto image_width = vx::min(window_size.x, reference_height * title_aspect); + auto image_height = image_width / title_aspect; + ImGui::SetCursorPosX(0.5f * (window_size.x - image_width)); + ImGui::Image(title->handle, ImVec2(image_width, image_height)); + } + + ImGui::Dummy(ImVec2(0.0f, 24.0f * globals::gui_scale)); + + const float button_width = 240.0f * globals::gui_scale; + const float button_xpos = 0.5f * (window_size.x - button_width); + + if(session::is_ingame()) { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_resume.c_str(), ImVec2(button_width, 0.0f))) { + globals::gui_screen = GUI_SCREEN_NONE; + } + + ImGui::Spacing(); + } else { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_play.c_str(), ImVec2(button_width, 0.0f))) { + globals::gui_screen = GUI_PLAY_MENU; + } + + ImGui::Spacing(); + } + + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_settings.c_str(), ImVec2(button_width, 0.0f))) { + globals::gui_screen = GUI_SETTINGS; + } + + ImGui::Spacing(); + + if(session::is_ingame()) { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_leave.c_str(), ImVec2(button_width, 0.0f))) { + session::disconnect("protocol.client_disconnect"); + globals::gui_screen = GUI_PLAY_MENU; + window_title::update(); + } + + ImGui::Spacing(); + } else { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_quit.c_str(), ImVec2(button_width, 0.0f))) { + glfwSetWindowShouldClose(globals::window, true); + } + + ImGui::Spacing(); + } + + if(!session::is_ingame()) { + const auto& padding = ImGui::GetStyle().FramePadding; + const auto& spacing = ImGui::GetStyle().ItemSpacing; + + ImGui::PushFont(globals::font_debug); + ImGui::SetCursorScreenPos(ImVec2(padding.x + spacing.x, window_size.y - globals::font_debug->FontSize - padding.y - spacing.y)); + ImGui::Text("Voxelius %s", project_version_string.c_str()); + ImGui::PopFont(); + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} diff --git a/src/game/client/main_menu.hh b/src/game/client/main_menu.hh new file mode 100644 index 0000000..9166722 --- /dev/null +++ b/src/game/client/main_menu.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_MAIN_MENU_HH +#define CLIENT_MAIN_MENU_HH 1 +#pragma once + +namespace main_menu +{ +void init(void); +void deinit(void); +void layout(void); +} // namespace main_menu + +#endif /* CLIENT_MAIN_MENU_HH */ diff --git a/src/game/client/message_box.cc b/src/game/client/message_box.cc new file mode 100644 index 0000000..da1b715 --- /dev/null +++ b/src/game/client/message_box.cc @@ -0,0 +1,94 @@ +#include "client/pch.hh" + +#include "client/message_box.hh" + +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/language.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +struct Button final { + message_box_action action; + std::string str_title; +}; + +static std::string str_title; +static std::string str_subtitle; +static std::vector<Button> buttons; + +void message_box::init(void) +{ + str_title = std::string(); + str_subtitle = std::string(); + buttons.clear(); +} + +void message_box::layout(void) +{ + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(0.0f, viewport->Size.y * 0.30f); + const auto window_size = ImVec2(viewport->Size.x, viewport->Size.y * 0.70f); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###UIProgress", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 1.0f * globals::gui_scale)); + + const float title_width = ImGui::CalcTextSize(str_title.c_str()).x; + ImGui::SetCursorPosX(0.5f * (window_size.x - title_width)); + ImGui::TextUnformatted(str_title.c_str()); + + ImGui::Dummy(ImVec2(0.0f, 8.0f * globals::gui_scale)); + + if(!str_subtitle.empty()) { + const float subtitle_width = ImGui::CalcTextSize(str_subtitle.c_str()).x; + ImGui::SetCursorPosX(0.5f * (window_size.x - subtitle_width)); + ImGui::TextUnformatted(str_subtitle.c_str()); + } + + ImGui::Dummy(ImVec2(0.0f, 32.0f * globals::gui_scale)); + + for(const auto& button : buttons) { + const float button_width = 0.8f * ImGui::CalcItemWidth(); + ImGui::SetCursorPosX(0.5f * (window_size.x - button_width)); + + if(ImGui::Button(button.str_title.c_str(), ImVec2(button_width, 0.0f))) { + if(button.action) { + button.action(); + } + } + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} + +void message_box::reset(void) +{ + str_title.clear(); + str_subtitle.clear(); + buttons.clear(); +} + +void message_box::set_title(const char* title) +{ + str_title = language::resolve(title); +} + +void message_box::set_subtitle(const char* subtitle) +{ + str_subtitle = language::resolve(subtitle); +} + +void message_box::add_button(const char* text, const message_box_action& action) +{ + Button button = {}; + button.str_title = std::format("{}###MessageBox_Button{}", language::resolve(text), buttons.size()); + button.action = action; + + buttons.push_back(button); +} diff --git a/src/game/client/message_box.hh b/src/game/client/message_box.hh new file mode 100644 index 0000000..7ea2466 --- /dev/null +++ b/src/game/client/message_box.hh @@ -0,0 +1,21 @@ +#ifndef CLIENT_MESSAGE_BOX_HH +#define CLIENT_MESSAGE_BOX_HH 1 +#pragma once + +using message_box_action = void (*)(void); + +namespace message_box +{ +void init(void); +void layout(void); +void reset(void); +} // namespace message_box + +namespace message_box +{ +void set_title(const char* title); +void set_subtitle(const char* subtitle); +void add_button(const char* text, const message_box_action& action); +} // namespace message_box + +#endif /* CLIENT_MESSAGE_BOX_HH */ diff --git a/src/game/client/metrics.cc b/src/game/client/metrics.cc new file mode 100644 index 0000000..8b5b7dd --- /dev/null +++ b/src/game/client/metrics.cc @@ -0,0 +1,98 @@ +#include "client/pch.hh" + +#include "client/metrics.hh" + +#include "core/version.hh" + +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/grounded.hh" +#include "shared/head.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" + +#include "client/camera.hh" +#include "client/game.hh" +#include "client/globals.hh" +#include "client/imdraw_ext.hh" +#include "client/session.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = + ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav; + +static std::basic_string<GLubyte> r_version; +static std::basic_string<GLubyte> r_renderer; + +void metrics::init(void) +{ + r_version = std::basic_string<GLubyte>(glGetString(GL_VERSION)); + r_renderer = std::basic_string<GLubyte>(glGetString(GL_RENDERER)); +} + +void metrics::layout(void) +{ + if(!session::is_ingame()) { + // Sanity check; we are checking this + // in client_game before calling layout + // on HUD-ish GUI systems but still + return; + } + + auto draw_list = ImGui::GetForegroundDrawList(); + + // FIXME: maybe use style colors instead of hardcoding? + auto text_color = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + auto shadow_color = ImGui::GetColorU32(ImVec4(0.1f, 0.1f, 0.1f, 1.0f)); + + auto position = ImVec2(8.0f, 8.0f); + auto y_step = 1.5f * globals::font_debug->FontSize; + + // Draw version + auto version_line = std::format("Voxelius {}", project_version_string); + imdraw_ext::text_shadow(version_line, position, text_color, shadow_color, globals::font_debug, draw_list); + position.y += 1.5f * y_step; + + // Draw client-side window framerate metrics + auto window_framerate = 1.0f / globals::window_frametime_avg; + auto window_frametime = 1000.0f * globals::window_frametime_avg; + auto window_fps_line = std::format("{:.02f} FPS [{:.02f} ms]", window_framerate, window_frametime); + imdraw_ext::text_shadow(window_fps_line, position, text_color, shadow_color, globals::font_debug, draw_list); + position.y += y_step; + + // Draw world rendering metrics + auto drawcall_line = std::format("World: {} DC / {} TRI", globals::num_drawcalls, globals::num_triangles); + imdraw_ext::text_shadow(drawcall_line, position, text_color, shadow_color, globals::font_debug, draw_list); + position.y += y_step; + + // Draw OpenGL version string + auto r_version_line = std::format("GL_VERSION: {}", reinterpret_cast<const char*>(r_version.c_str())); + imdraw_ext::text_shadow(r_version_line, position, text_color, shadow_color, globals::font_debug, draw_list); + position.y += y_step; + + // Draw OpenGL renderer string + auto r_renderer_line = std::format("GL_RENDERER: {}", reinterpret_cast<const char*>(r_renderer.c_str())); + imdraw_ext::text_shadow(r_renderer_line, position, text_color, shadow_color, globals::font_debug, draw_list); + position.y += 1.5f * y_step; + + const auto& head = globals::dimension->entities.get<HeadComponent>(globals::player); + const auto& transform = globals::dimension->entities.get<TransformComponent>(globals::player); + const auto& velocity = globals::dimension->entities.get<VelocityComponent>(globals::player); + + // Draw player voxel position + auto voxel_position = coord::to_voxel(transform.chunk, transform.local); + auto voxel_line = std::format("voxel: [{} {} {}]", voxel_position.x, voxel_position.y, voxel_position.z); + imdraw_ext::text_shadow(voxel_line, position, text_color, shadow_color, globals::font_debug, draw_list); + position.y += y_step; + + // Draw player world position + auto world_line = std::format("world: [{} {} {}] [{:.03f} {:.03f} {:.03f}]", transform.chunk.x, transform.chunk.y, transform.chunk.z, + transform.local.x, transform.local.y, transform.local.z); + imdraw_ext::text_shadow(world_line, position, text_color, shadow_color, globals::font_debug, draw_list); + position.y += y_step; + + // Draw player look angles + auto angles = glm::degrees(transform.angles + head.angles); + auto angle_line = std::format("angle: [{: .03f} {: .03f} {: .03f}]", angles[0], angles[1], angles[2]); + imdraw_ext::text_shadow(angle_line, position, text_color, shadow_color, globals::font_debug, draw_list); + position.y += y_step; +} diff --git a/src/game/client/metrics.hh b/src/game/client/metrics.hh new file mode 100644 index 0000000..ba68a86 --- /dev/null +++ b/src/game/client/metrics.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_METRICS_HH +#define CLIENT_METRICS_HH 1 +#pragma once + +namespace metrics +{ +void init(void); +void layout(void); +} // namespace metrics + +#endif /* CLIENT_METRICS_HH */ diff --git a/src/game/client/outline.cc b/src/game/client/outline.cc new file mode 100644 index 0000000..31a6bd9 --- /dev/null +++ b/src/game/client/outline.cc @@ -0,0 +1,148 @@ +#include "client/pch.hh" + +#include "client/outline.hh" + +#include "core/config.hh" + +#include "shared/coord.hh" + +#include "client/camera.hh" +#include "client/const.hh" +#include "client/game.hh" +#include "client/program.hh" + +// ONLY TOUCH THESE IF THE RESPECTIVE SHADER +// VARIANT MACRO DECLARATIONS LAYOUT CHANGED AS WELL +constexpr static unsigned int WORLD_CURVATURE = 0U; + +static GL_Program program; +static std::size_t u_vpmatrix; +static std::size_t u_worldpos; +static std::size_t u_viewdist; +static std::size_t u_modulate; +static std::size_t u_scale; + +static GLuint vaobj; +static GLuint cube_vbo; +static GLuint line_vbo; + +void outline::init(void) +{ + if(!program.setup("shaders/outline.vert", "shaders/outline.frag")) { + spdlog::critical("outline: program setup failed"); + std::terminate(); + } + + u_vpmatrix = program.add_uniform("u_ViewProjMatrix"); + u_worldpos = program.add_uniform("u_WorldPosition"); + u_viewdist = program.add_uniform("u_ViewDistance"); + u_modulate = program.add_uniform("u_Modulate"); + u_scale = program.add_uniform("u_Scale"); + + const glm::fvec3 cube_vertices[24] = { + glm::fvec3(0.0f, 0.0f, 0.0f), + glm::fvec3(0.0f, 1.0f, 0.0f), + glm::fvec3(0.0f, 1.0f, 0.0f), + glm::fvec3(1.0f, 1.0f, 0.0f), + glm::fvec3(1.0f, 1.0f, 0.0f), + glm::fvec3(1.0f, 0.0f, 0.0f), + glm::fvec3(1.0f, 0.0f, 0.0f), + glm::fvec3(0.0f, 0.0f, 0.0f), + + glm::fvec3(0.0f, 0.0f, 1.0f), + glm::fvec3(0.0f, 1.0f, 1.0f), + glm::fvec3(0.0f, 1.0f, 1.0f), + glm::fvec3(1.0f, 1.0f, 1.0f), + glm::fvec3(1.0f, 1.0f, 1.0f), + glm::fvec3(1.0f, 0.0f, 1.0f), + glm::fvec3(1.0f, 0.0f, 1.0f), + glm::fvec3(0.0f, 0.0f, 1.0f), + + glm::fvec3(0.0f, 0.0f, 0.0f), + glm::fvec3(0.0f, 0.0f, 1.0f), + glm::fvec3(0.0f, 1.0f, 0.0f), + glm::fvec3(0.0f, 1.0f, 1.0f), + glm::fvec3(1.0f, 0.0f, 0.0f), + glm::fvec3(1.0f, 0.0f, 1.0f), + glm::fvec3(1.0f, 1.0f, 0.0f), + glm::fvec3(1.0f, 1.0f, 1.0f), + }; + + glGenBuffers(1, &cube_vbo); + glBindBuffer(GL_ARRAY_BUFFER, cube_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(cube_vertices), cube_vertices, GL_STATIC_DRAW); + + const glm::fvec3 line_vertices[2] = { + glm::fvec3(0.0f, 0.0f, 0.0f), + glm::fvec3(1.0f, 1.0f, 1.0f), + }; + + glGenBuffers(1, &line_vbo); + glBindBuffer(GL_ARRAY_BUFFER, line_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(line_vertices), line_vertices, GL_STATIC_DRAW); + + glGenVertexArrays(1, &vaobj); + + glBindVertexArray(vaobj); + glEnableVertexAttribArray(0); + glVertexAttribDivisor(0, 0); +} + +void outline::deinit(void) +{ + glDeleteVertexArrays(1, &vaobj); + glDeleteBuffers(1, &line_vbo); + glDeleteBuffers(1, &cube_vbo); + program.destroy(); +} + +void outline::prepare(void) +{ + program.set_variant_vert(WORLD_CURVATURE, client_game::world_curvature.get_value()); + + if(!program.update()) { + spdlog::critical("outline_renderer: program update failed"); + std::terminate(); + } + + glDisable(GL_CULL_FACE); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + glUseProgram(program.handle); + glUniformMatrix4fv(program.uniforms[u_vpmatrix].location, 1, false, glm::value_ptr(camera::matrix)); + glUniform1f(program.uniforms[u_viewdist].location, CHUNK_SIZE * camera::view_distance.get_value()); + + glBindVertexArray(vaobj); + glEnableVertexAttribArray(0); + glVertexAttribDivisor(0, 0); +} + +void outline::cube(const chunk_pos& cpos, const glm::fvec3& fpos, const glm::fvec3& size, float thickness, const glm::fvec4& color) +{ + auto patch_cpos = cpos - camera::position_chunk; + + glLineWidth(thickness); + + glUniform3fv(program.uniforms[u_worldpos].location, 1, glm::value_ptr(coord::to_fvec3(patch_cpos, fpos))); + glUniform4fv(program.uniforms[u_modulate].location, 1, glm::value_ptr(color)); + glUniform3fv(program.uniforms[u_scale].location, 1, glm::value_ptr(size)); + + glBindBuffer(GL_ARRAY_BUFFER, cube_vbo); + glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(glm::fvec3), nullptr); + glDrawArrays(GL_LINES, 0, 24); +} + +void outline::line(const chunk_pos& cpos, const glm::fvec3& fpos, const glm::fvec3& size, float thickness, const glm::fvec4& color) +{ + auto patch_cpos = cpos - camera::position_chunk; + + glLineWidth(thickness); + + glUniform3fv(program.uniforms[u_worldpos].location, 1, glm::value_ptr(coord::to_fvec3(patch_cpos, fpos))); + glUniform4fv(program.uniforms[u_modulate].location, 1, glm::value_ptr(color)); + glUniform3fv(program.uniforms[u_scale].location, 1, glm::value_ptr(size)); + + glBindBuffer(GL_ARRAY_BUFFER, line_vbo); + glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(glm::fvec3), nullptr); + glDrawArrays(GL_LINES, 0, 2); +} diff --git a/src/game/client/outline.hh b/src/game/client/outline.hh new file mode 100644 index 0000000..a7789b2 --- /dev/null +++ b/src/game/client/outline.hh @@ -0,0 +1,20 @@ +#ifndef CLIENT_OUTLINE_HH +#define CLIENT_OUTLINE_HH 1 +#pragma once + +#include "shared/types.hh" + +namespace outline +{ +void init(void); +void deinit(void); +void prepare(void); +} // namespace outline + +namespace outline +{ +void cube(const chunk_pos& cpos, const glm::fvec3& fpos, const glm::fvec3& size, float thickness, const glm::fvec4& color); +void line(const chunk_pos& cpos, const glm::fvec3& fpos, const glm::fvec3& size, float thickness, const glm::fvec4& color); +} // namespace outline + +#endif /* CLIENT_OUTLINE_HH */ diff --git a/src/game/client/pch.hh b/src/game/client/pch.hh new file mode 100644 index 0000000..df9d2b5 --- /dev/null +++ b/src/game/client/pch.hh @@ -0,0 +1,32 @@ +#ifndef CLIENT_PCH_HH +#define CLIENT_PCH_HH 1 +#pragma once + +#include <shared/pch.hh> + +#include <AL/al.h> +#include <AL/alc.h> +#include <AL/salad.h> + +#include <dr_mp3.h> +#include <dr_wav.h> + +#include <GLFW/glfw3.h> + +#include <glad/gl.h> + +#include <imgui.h> +#include <imgui_impl_glfw.h> +#include <imgui_impl_opengl3.h> +#include <imgui_stdlib.h> + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#endif + +#if defined(__unix__) +#include <dlfcn.h> +#endif + +#endif /* CLIENT_PCH_HH */ diff --git a/src/game/client/play_menu.cc b/src/game/client/play_menu.cc new file mode 100644 index 0000000..b9a6693 --- /dev/null +++ b/src/game/client/play_menu.cc @@ -0,0 +1,548 @@ +#include "client/pch.hh" + +#include "client/play_menu.hh" + +#include "core/config.hh" +#include "core/constexpr.hh" +#include "core/strtools.hh" + +#include "shared/protocol.hh" + +#include "client/bother.hh" +#include "client/game.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/language.hh" +#include "client/session.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; +constexpr static const char* DEFAULT_SERVER_NAME = "Voxelius Server"; +constexpr static const char* SERVERS_TXT = "servers.txt"; +constexpr static const char* WARNING_TOAST = "[!]"; + +constexpr static std::size_t MAX_SERVER_ITEM_NAME = 24; + +enum class item_status : unsigned int { + UNKNOWN = 0x0000U, + PINGING = 0x0001U, + REACHED = 0x0002U, + FAILURE = 0x0003U, +}; + +struct ServerStatusItem final { + std::string name; + std::string password; + std::string hostname; + std::uint16_t port; + + // Things pulled from bother events + std::uint32_t protocol_version; + std::uint16_t num_players; + std::uint16_t max_players; + std::string motd; + + // Unique identifier that monotonically + // grows with each new server added and + // doesn't reset with each server removed + unsigned int identity; + + item_status status; +}; + +static std::string str_tab_servers; + +static std::string str_join; +static std::string str_connect; +static std::string str_add; +static std::string str_edit; +static std::string str_remove; +static std::string str_refresh; + +static std::string str_status_init; +static std::string str_status_ping; +static std::string str_status_fail; + +static std::string str_outdated_client; +static std::string str_outdated_server; + +static std::string input_itemname; +static std::string input_hostname; +static std::string input_password; + +static unsigned int next_identity; +static std::deque<ServerStatusItem*> servers_deque; +static ServerStatusItem* selected_server; +static bool editing_server; +static bool adding_server; +static bool needs_focus; + +static void parse_hostname(ServerStatusItem* item, const std::string& hostname) +{ + auto parts = strtools::split(hostname, ":"); + + if(!parts[0].empty()) { + item->hostname = parts[0]; + } else { + item->hostname = std::string("localhost"); + } + + if(parts.size() >= 2) { + item->port = vx::clamp<std::uint16_t>(strtoul(parts[1].c_str(), nullptr, 10), 1024, UINT16_MAX); + } else { + item->port = protocol::PORT; + } +} + +static void add_new_server(void) +{ + auto item = new ServerStatusItem(); + item->port = protocol::PORT; + item->protocol_version = protocol::VERSION; + item->max_players = UINT16_MAX; + item->num_players = UINT16_MAX; + item->identity = next_identity; + item->status = item_status::UNKNOWN; + + next_identity += 1U; + + input_itemname = DEFAULT_SERVER_NAME; + input_hostname = std::string(); + input_password = std::string(); + + servers_deque.push_back(item); + selected_server = item; + editing_server = true; + adding_server = true; + needs_focus = true; +} + +static void edit_selected_server(void) +{ + input_itemname = selected_server->name; + + if(selected_server->port != protocol::PORT) { + input_hostname = std::format("{}:{}", selected_server->hostname, selected_server->port); + } else { + input_hostname = selected_server->hostname; + } + + input_password = selected_server->password; + + editing_server = true; + needs_focus = true; +} + +static void remove_selected_server(void) +{ + bother::cancel(selected_server->identity); + + for(auto it = servers_deque.cbegin(); it != servers_deque.cend(); ++it) { + if(selected_server == (*it)) { + delete selected_server; + selected_server = nullptr; + servers_deque.erase(it); + return; + } + } +} + +static void join_selected_server(void) +{ + if(!session::peer) { + session::connect(selected_server->hostname.c_str(), selected_server->port, selected_server->password.c_str()); + } +} + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if((event.key == GLFW_KEY_ESCAPE) && (event.action == GLFW_PRESS)) { + if(globals::gui_screen == GUI_PLAY_MENU) { + if(editing_server) { + if(adding_server) { + remove_selected_server(); + } else { + input_itemname.clear(); + input_hostname.clear(); + input_password.clear(); + editing_server = false; + adding_server = false; + return; + } + } + + globals::gui_screen = GUI_MAIN_MENU; + selected_server = nullptr; + return; + } + } +} + +static void on_language_set(const LanguageSetEvent& event) +{ + str_tab_servers = language::resolve_gui("play_menu.tab.servers"); + + str_join = language::resolve_gui("play_menu.join"); + str_connect = language::resolve_gui("play_menu.connect"); + str_add = language::resolve_gui("play_menu.add"); + str_edit = language::resolve_gui("play_menu.edit"); + str_remove = language::resolve_gui("play_menu.remove"); + str_refresh = language::resolve_gui("play_menu.refresh"); + + str_status_init = language::resolve("play_menu.status.init"); + str_status_ping = language::resolve("play_menu.status.ping"); + str_status_fail = language::resolve("play_menu.status.fail"); + + str_outdated_client = language::resolve("play_menu.outdated_client"); + str_outdated_server = language::resolve("play_menu.outdated_server"); +} + +static void on_bother_response(const BotherResponseEvent& event) +{ + for(auto item : servers_deque) { + if(item->identity == event.identity) { + if(event.is_server_unreachable) { + item->protocol_version = 0U; + item->num_players = UINT16_MAX; + item->max_players = UINT16_MAX; + item->motd = str_status_fail; + item->status = item_status::FAILURE; + } else { + item->protocol_version = event.protocol_version; + item->num_players = event.num_players; + item->max_players = event.max_players; + item->motd = event.motd; + item->status = item_status::REACHED; + } + + break; + } + } +} + +static void layout_server_item(ServerStatusItem* item) +{ + // Preserve the cursor at which we draw stuff + const ImVec2& cursor = ImGui::GetCursorScreenPos(); + const ImVec2& padding = ImGui::GetStyle().FramePadding; + const ImVec2& spacing = ImGui::GetStyle().ItemSpacing; + + const float item_width = ImGui::GetContentRegionAvail().x; + const float line_height = ImGui::GetTextLineHeightWithSpacing(); + const std::string sid = std::format("###play_menu.servers.{}", static_cast<void*>(item)); + if(ImGui::Selectable(sid.c_str(), (item == selected_server), 0, ImVec2(0.0, 2.0f * (line_height + padding.y + spacing.y)))) { + selected_server = item; + editing_server = false; + } + + if(ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + // Double clicked - join the selected server + join_selected_server(); + } + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + if(item == selected_server) { + const ImVec2 start = ImVec2(cursor.x, cursor.y); + const ImVec2 end = ImVec2(start.x + item_width, start.y + 2.0f * (line_height + padding.y + spacing.y)); + draw_list->AddRect(start, end, ImGui::GetColorU32(ImGuiCol_Text), 0.0f, 0, globals::gui_scale); + } + + const ImVec2 name_pos = ImVec2(cursor.x + padding.x + 0.5f * spacing.x, cursor.y + padding.y); + draw_list->AddText(name_pos, ImGui::GetColorU32(ImGuiCol_Text), item->name.c_str(), item->name.c_str() + item->name.size()); + + if(item->status == item_status::REACHED) { + auto stats = std::format("{}/{}", item->num_players, item->max_players); + auto stats_width = ImGui::CalcTextSize(stats.c_str(), stats.c_str() + stats.size()).x; + auto stats_pos = ImVec2(cursor.x + item_width - stats_width - padding.x, cursor.y + padding.y); + draw_list->AddText(stats_pos, ImGui::GetColorU32(ImGuiCol_TextDisabled), stats.c_str(), stats.c_str() + stats.size()); + + if(item->protocol_version != protocol::VERSION) { + auto warning_size = ImGui::CalcTextSize(WARNING_TOAST); + auto warning_pos = ImVec2(stats_pos.x - warning_size.x - padding.x - 4.0f * globals::gui_scale, cursor.y + padding.y); + auto warning_end = ImVec2(warning_pos.x + warning_size.x, warning_pos.y + warning_size.y); + draw_list->AddText(warning_pos, ImGui::GetColorU32(ImGuiCol_DragDropTarget), WARNING_TOAST); + + if(ImGui::IsMouseHoveringRect(warning_pos, warning_end)) { + ImGui::BeginTooltip(); + + if(item->protocol_version < protocol::VERSION) { + ImGui::TextUnformatted(str_outdated_server.c_str(), str_outdated_server.c_str() + str_outdated_server.size()); + } else { + ImGui::TextUnformatted(str_outdated_client.c_str(), str_outdated_client.c_str() + str_outdated_client.size()); + } + + ImGui::EndTooltip(); + } + } + } + + ImU32 motd_color = {}; + const std::string* motd_text; + + switch(item->status) { + case item_status::UNKNOWN: + motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); + motd_text = &str_status_init; + break; + case item_status::PINGING: + motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); + motd_text = &str_status_ping; + break; + case item_status::REACHED: + motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); + motd_text = &item->motd; + break; + default: + motd_color = ImGui::GetColorU32(ImGuiCol_PlotLinesHovered); + motd_text = &str_status_fail; + break; + } + + const ImVec2 motd_pos = ImVec2(cursor.x + padding.x + 0.5f * spacing.x, cursor.y + padding.y + line_height); + draw_list->AddText(motd_pos, motd_color, motd_text->c_str(), motd_text->c_str() + motd_text->size()); +} + +static void layout_server_edit(ServerStatusItem* item) +{ + if(needs_focus) { + ImGui::SetKeyboardFocusHere(); + needs_focus = false; + } + + ImGui::SetNextItemWidth(-0.25f * ImGui::GetContentRegionAvail().x); + ImGui::InputText("###play_menu.servers.edit_itemname", &input_itemname); + ImGui::SameLine(); + + const bool ignore_input = strtools::is_whitespace(input_itemname) || input_hostname.empty(); + + ImGui::BeginDisabled(ignore_input); + + if(ImGui::Button("OK###play_menu.servers.submit_input", ImVec2(-1.0f, 0.0f)) + || (!ignore_input && ImGui::IsKeyPressed(ImGuiKey_Enter))) { + parse_hostname(item, input_hostname); + item->password = input_password; + item->name = input_itemname.substr(0, MAX_SERVER_ITEM_NAME); + item->status = item_status::UNKNOWN; + editing_server = false; + adding_server = false; + + input_itemname.clear(); + input_hostname.clear(); + + bother::cancel(item->identity); + } + + ImGui::EndDisabled(); + + ImGuiInputTextFlags hostname_flags = ImGuiInputTextFlags_CharsNoBlank; + + if(client_game::streamer_mode.get_value()) { + // Hide server hostname to avoid things like + // followers flooding the server that is streamed online + hostname_flags |= ImGuiInputTextFlags_Password; + } + + ImGui::SetNextItemWidth(-0.50f * ImGui::GetContentRegionAvail().x); + ImGui::InputText("###play_menu.servers.edit_hostname", &input_hostname, hostname_flags); + ImGui::SameLine(); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputText("###play_menu.servers.edit_password", &input_password, ImGuiInputTextFlags_Password); +} + +static void layout_servers(void) +{ + if(ImGui::BeginListBox("###play_menu.servers.listbox", ImVec2(-1.0f, -1.0f))) { + for(ServerStatusItem* item : servers_deque) { + if(editing_server && item == selected_server) { + layout_server_edit(item); + } else { + layout_server_item(item); + } + } + + ImGui::EndListBox(); + } +} + +static void layout_servers_buttons(void) +{ + auto avail_width = ImGui::GetContentRegionAvail().x; + + // Can only join when selected and not editing + ImGui::BeginDisabled(!selected_server || editing_server); + + if(ImGui::Button(str_join.c_str(), ImVec2(-0.50f * avail_width, 0.0f))) { + join_selected_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + // Can only connect directly when not editing anything + ImGui::BeginDisabled(editing_server); + + if(ImGui::Button(str_connect.c_str(), ImVec2(-1.00f, 0.0f))) { + globals::gui_screen = GUI_DIRECT_CONNECTION; + } + + ImGui::EndDisabled(); + + // Can only add when not editing anything + ImGui::BeginDisabled(editing_server); + + if(ImGui::Button(str_add.c_str(), ImVec2(-0.75f * avail_width, 0.0f))) { + add_new_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + // Can only edit when selected and not editing + ImGui::BeginDisabled(!selected_server || editing_server); + + if(ImGui::Button(str_edit.c_str(), ImVec2(-0.50f * avail_width, 0.0f))) { + edit_selected_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + // Can only remove when selected and not editing + ImGui::BeginDisabled(!selected_server || editing_server); + + if(ImGui::Button(str_remove.c_str(), ImVec2(-0.25f * avail_width, 0.0f))) { + remove_selected_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + if(ImGui::Button(str_refresh.c_str(), ImVec2(-1.0f, 0.0f))) { + for(ServerStatusItem* item : servers_deque) { + if(item->status != item_status::PINGING) { + if(!editing_server || item != selected_server) { + item->status = item_status::UNKNOWN; + bother::cancel(item->identity); + } + } + } + } +} + +void play_menu::init(void) +{ + if(auto file = PHYSFS_openRead(SERVERS_TXT)) { + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + auto stream = std::istringstream(source); + auto line = std::string(); + + while(std::getline(stream, line)) { + auto parts = strtools::split(line, "%"); + + auto item = new ServerStatusItem(); + item->port = protocol::PORT; + item->protocol_version = protocol::VERSION; + item->max_players = UINT16_MAX; + item->num_players = UINT16_MAX; + item->identity = next_identity; + item->status = item_status::UNKNOWN; + + next_identity += 1U; + + parse_hostname(item, parts[0]); + + if(parts.size() >= 2) { + item->password = parts[1]; + } else { + item->password = std::string(); + } + + if(parts.size() >= 3) { + item->name = parts[2].substr(0, MAX_SERVER_ITEM_NAME); + } else { + item->name = DEFAULT_SERVER_NAME; + } + + servers_deque.push_back(item); + } + } + + globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<LanguageSetEvent>().connect<&on_language_set>(); + globals::dispatcher.sink<BotherResponseEvent>().connect<&on_bother_response>(); +} + +void play_menu::deinit(void) +{ + std::ostringstream stream; + + for(const auto item : servers_deque) { + stream << std::format("{}:{}%{}%{}", item->hostname, item->port, item->password, item->name) << std::endl; + } + + if(auto file = PHYSFS_openWrite(SERVERS_TXT)) { + auto source = stream.str(); + PHYSFS_writeBytes(file, source.data(), source.size()); + PHYSFS_close(file); + } + + for(auto item : servers_deque) + delete item; + servers_deque.clear(); +} + +void play_menu::layout(void) +{ + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(viewport->Size.x * 0.05f, viewport->Size.y * 0.05f); + const auto window_size = ImVec2(viewport->Size.x * 0.90f, viewport->Size.y * 0.90f); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###play_menu", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f * globals::gui_scale, 3.0f * globals::gui_scale)); + + if(ImGui::BeginTabBar("###play_menu.tabs", ImGuiTabBarFlags_FittingPolicyResizeDown)) { + if(ImGui::TabItemButton("<<")) { + globals::gui_screen = GUI_MAIN_MENU; + selected_server = nullptr; + editing_server = false; + } + + if(ImGui::BeginTabItem(str_tab_servers.c_str())) { + if(ImGui::BeginChild("###play_menu.servers.child", ImVec2(0.0f, -2.0f * ImGui::GetFrameHeightWithSpacing()))) { + layout_servers(); + } + + ImGui::EndChild(); + + layout_servers_buttons(); + + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} + +void play_menu::update_late(void) +{ + for(auto item : servers_deque) { + if(item->status == item_status::UNKNOWN) { + bother::ping(item->identity, item->hostname.c_str(), item->port); + item->status = item_status::PINGING; + continue; + } + } +} diff --git a/src/game/client/play_menu.hh b/src/game/client/play_menu.hh new file mode 100644 index 0000000..44193df --- /dev/null +++ b/src/game/client/play_menu.hh @@ -0,0 +1,13 @@ +#ifndef CLIENT_PLAY_MENU_HH +#define CLIENT_PLAY_MENU_HH 1 +#pragma once + +namespace play_menu +{ +void init(void); +void deinit(void); +void layout(void); +void update_late(void); +} // namespace play_menu + +#endif /* CLIENT_PLAY_MENU_HH */ diff --git a/src/game/client/player_look.cc b/src/game/client/player_look.cc new file mode 100644 index 0000000..767c171 --- /dev/null +++ b/src/game/client/player_look.cc @@ -0,0 +1,146 @@ +#include "client/pch.hh" + +#include "client/player_look.hh" + +#include "core/angles.hh" +#include "core/config.hh" + +#include "shared/dimension.hh" +#include "shared/head.hh" + +#include "client/const.hh" +#include "client/gamepad.hh" +#include "client/gamepad_axis.hh" +#include "client/gamepad_button.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/keybind.hh" +#include "client/session.hh" +#include "client/settings.hh" + +constexpr static float PITCH_MIN = -1.0f * vx::radians(90.0f); +constexpr static float PITCH_MAX = +1.0f * vx::radians(90.0f); + +// Mouse options +static ConfigBoolean mouse_raw_input(true); +static ConfigUnsigned mouse_sensitivity(25U, 1U, 100U); + +// Gamepad options +static ConfigFloat gamepad_fastlook_factor(1.5f, 1.0f, 5.0f); +static ConfigUnsigned gamepad_accel_pitch(15U, 1U, 100U); +static ConfigUnsigned gamepad_accel_yaw(25U, 1U, 100U); + +// Gamepad axes +static ConfigGamepadAxis axis_pitch(GLFW_GAMEPAD_AXIS_LEFT_Y, false); +static ConfigGamepadAxis axis_yaw(GLFW_GAMEPAD_AXIS_LEFT_X, false); + +// Gamepad buttons +static ConfigGamepadButton button_fastlook(GLFW_GAMEPAD_BUTTON_LEFT_THUMB); + +static bool fastlook_enabled; +static glm::fvec2 last_cursor; + +static void add_angles(float pitch, float yaw) +{ + if(session::is_ingame()) { + auto& head = globals::dimension->entities.get<HeadComponent>(globals::player); + + head.angles[0] += pitch; + head.angles[1] += yaw; + head.angles[0] = vx::clamp(head.angles[0], PITCH_MIN, PITCH_MAX); + head.angles = cxangles::wrap_180(head.angles); + + // Client-side head angles are not interpolated; + // Re-assigning the previous state after the current + // state has been already modified is certainly a way + // to circumvent the interpolation applied to anything with a head + globals::dimension->entities.emplace_or_replace<HeadComponentPrev>(globals::player, head); + } +} + +static void on_glfw_cursor_pos(const GlfwCursorPosEvent& event) +{ + if(gamepad::available && gamepad::active.get_value()) { + // The player is assumed to be using + // a gamepad instead of mouse and keyboard + last_cursor = event.pos; + return; + } + + if(globals::gui_screen || !session::is_ingame()) { + // UI is visible or we're not in-game + last_cursor = event.pos; + return; + } + + auto dx = -0.01f * static_cast<float>(mouse_sensitivity.get_value()) * vx::radians(event.pos.x - last_cursor.x); + auto dy = -0.01f * static_cast<float>(mouse_sensitivity.get_value()) * vx::radians(event.pos.y - last_cursor.y); + add_angles(dy, dx); + + last_cursor = event.pos; +} + +static void on_gamepad_button(const GamepadButtonEvent& event) +{ + if(button_fastlook.equals(event.button)) { + fastlook_enabled = event.action == GLFW_PRESS; + } +} + +void player_look::init(void) +{ + globals::client_config.add_value("player_look.mouse.raw_input", mouse_raw_input); + globals::client_config.add_value("player_look.mouse.sensitivity", mouse_sensitivity); + globals::client_config.add_value("player_look.gamepad.fastlook_factor", gamepad_fastlook_factor); + globals::client_config.add_value("player_look.gamepad.accel_pitch", gamepad_accel_pitch); + globals::client_config.add_value("player_look.gamepad.accel_yaw", gamepad_accel_yaw); + globals::client_config.add_value("player_look.gp_axis.pitch", axis_pitch); + globals::client_config.add_value("player_look.gp_axis.yaw", axis_yaw); + globals::client_config.add_value("player_look.gp_button.fastlook", button_fastlook); + + settings::add_slider(0, mouse_sensitivity, settings_location::MOUSE, "player_look.mouse.sensitivity", true); + settings::add_checkbox(1, mouse_raw_input, settings_location::MOUSE, "player_look.mouse.raw_input", true); + + settings::add_slider(0, gamepad_accel_pitch, settings_location::GAMEPAD_GAMEPLAY, "player_look.gamepad.accel_pitch", false); + settings::add_slider(1, gamepad_accel_yaw, settings_location::GAMEPAD_GAMEPLAY, "player_look.gamepad.accel_yaw", false); + settings::add_gamepad_axis(2, axis_pitch, settings_location::GAMEPAD_GAMEPLAY, "player_look.gp_axis.pitch"); + settings::add_gamepad_axis(3, axis_yaw, settings_location::GAMEPAD_GAMEPLAY, "player_look.gp_axis.yaw"); + settings::add_slider( + 4, gamepad_fastlook_factor, settings_location::GAMEPAD_GAMEPLAY, "player_look.gamepad.fastlook_factor", true, "%.02f"); + settings::add_gamepad_button(5, button_fastlook, settings_location::GAMEPAD_GAMEPLAY, "player_look.gp_button.fastlook"); + + fastlook_enabled = false; + last_cursor.x = 0.5f * static_cast<float>(globals::width); + last_cursor.y = 0.5f * static_cast<float>(globals::height); + + globals::dispatcher.sink<GlfwCursorPosEvent>().connect<&on_glfw_cursor_pos>(); + globals::dispatcher.sink<GamepadButtonEvent>().connect<&on_gamepad_button>(); +} + +void player_look::update_late(void) +{ + if(gamepad::available && gamepad::active.get_value() && !globals::gui_screen) { + auto pitch_value = axis_pitch.get_value(gamepad::state, gamepad::deadzone.get_value()); + auto yaw_value = axis_yaw.get_value(gamepad::state, gamepad::deadzone.get_value()); + + if(fastlook_enabled) { + // Fastlook allows the camera to + // rotate quicker when a button is held down + pitch_value *= gamepad_fastlook_factor.get_value(); + yaw_value *= gamepad_fastlook_factor.get_value(); + } + + pitch_value *= 0.001f * static_cast<float>(gamepad_accel_pitch.get_value()); + yaw_value *= 0.001f * static_cast<float>(gamepad_accel_yaw.get_value()); + + add_angles(pitch_value, yaw_value); + } + + if(!globals::gui_screen && session::is_ingame()) { + glfwSetInputMode(globals::window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + glfwSetInputMode(globals::window, GLFW_RAW_MOUSE_MOTION, mouse_raw_input.get_value()); + } else { + glfwSetInputMode(globals::window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + glfwSetInputMode(globals::window, GLFW_RAW_MOUSE_MOTION, false); + } +} diff --git a/src/game/client/player_look.hh b/src/game/client/player_look.hh new file mode 100644 index 0000000..c10df0e --- /dev/null +++ b/src/game/client/player_look.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_PLAYER_LOOK_HH +#define CLIENT_PLAYER_LOOK_HH 1 +#pragma once + +namespace player_look +{ +void init(void); +void update_late(void); +} // namespace player_look + +#endif /* CLIENT_PLAYER_LOOK_HH */ diff --git a/src/game/client/player_move.cc b/src/game/client/player_move.cc new file mode 100644 index 0000000..d949d53 --- /dev/null +++ b/src/game/client/player_move.cc @@ -0,0 +1,283 @@ +#include "client/pch.hh" + +#include "client/player_move.hh" + +#include "core/angles.hh" +#include "core/config.hh" +#include "core/constexpr.hh" + +#include "shared/dimension.hh" +#include "shared/grounded.hh" +#include "shared/head.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" + +#include "client/const.hh" +#include "client/gamepad.hh" +#include "client/gamepad_axis.hh" +#include "client/gamepad_button.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/keybind.hh" +#include "client/session.hh" +#include "client/settings.hh" +#include "client/sound.hh" +#include "client/status_lines.hh" +#include "client/toggles.hh" +#include "client/voxel_sounds.hh" + +constexpr static std::uint64_t PMOVE_JUMP_COOLDOWN = 500000; // 0.5 seconds + +constexpr static float PMOVE_FOOTSTEP_SIZE = 2.0f; + +// Movement keys +static ConfigKeyBind key_move_forward(GLFW_KEY_W); +static ConfigKeyBind key_move_back(GLFW_KEY_S); +static ConfigKeyBind key_move_left(GLFW_KEY_A); +static ConfigKeyBind key_move_right(GLFW_KEY_D); +static ConfigKeyBind key_move_down(GLFW_KEY_LEFT_SHIFT); +static ConfigKeyBind key_move_up(GLFW_KEY_SPACE); + +// Movement gamepad axes +static ConfigGamepadAxis axis_move_forward(GLFW_GAMEPAD_AXIS_RIGHT_X, false); +static ConfigGamepadAxis axis_move_sideways(GLFW_GAMEPAD_AXIS_RIGHT_Y, false); + +// Movement gamepad buttons +static ConfigGamepadButton button_move_down(GLFW_GAMEPAD_BUTTON_DPAD_DOWN); +static ConfigGamepadButton button_move_up(GLFW_GAMEPAD_BUTTON_DPAD_UP); + +// General movement options +static ConfigBoolean enable_speedometer(true); + +static glm::fvec3 movement_direction; + +static std::uint64_t next_jump_us; +static float speedometer_value; +static float footsteps_distance; + +static std::mt19937_64 pitch_random; +static std::uniform_real_distribution<float> pitch_distrib; + +// Quake III's PM_Accelerate-ish function used for +// conventional (gravity-affected non-flight) movement +static glm::fvec3 pm_accelerate(const glm::fvec3& wishdir, const glm::fvec3& velocity, float wishspeed, float accel) +{ + auto current_speed = glm::dot(velocity, wishdir); + auto add_speed = wishspeed - current_speed; + + if(add_speed <= 0.0f) { + // Not accelerating + return velocity; + } + + auto accel_speed = vx::min(add_speed, accel * globals::fixed_frametime * wishspeed); + + auto result = glm::fvec3(velocity); + result.x += accel_speed * wishdir.x; + result.z += accel_speed * wishdir.z; + return result; +} + +// Conventional movement - velocity update when not on the ground +static glm::fvec3 pm_air_move(const glm::fvec3& wishdir, const glm::fvec3& velocity) +{ + return pm_accelerate(wishdir, velocity, PMOVE_ACCELERATION_AIR, PMOVE_MAX_SPEED_AIR); +} + +// Conventional movement - velocity uodate when on the ground +static glm::fvec3 pm_ground_move(const glm::fvec3& wishdir, const glm::fvec3& velocity) +{ + if(auto speed = glm::length(velocity)) { + auto speed_drop = speed * PMOVE_FRICTION_GROUND * globals::fixed_frametime; + auto speed_factor = vx::max(speed - speed_drop, 0.0f) / speed; + return pm_accelerate(wishdir, velocity * speed_factor, PMOVE_ACCELERATION_GROUND, PMOVE_MAX_SPEED_GROUND); + } + + return pm_accelerate(wishdir, velocity, PMOVE_ACCELERATION_GROUND, PMOVE_MAX_SPEED_GROUND); +} + +// A simpler minecraft-like acceleration model +// used whenever the TOGGLE_PM_FLIGHT is enabled +static glm::fvec3 pm_flight_move(const glm::fvec3& wishdir) +{ + // FIXME: make it smoother + return wishdir * PMOVE_MAX_SPEED_AIR; +} + +void player_move::init(void) +{ + movement_direction = ZERO_VEC3<float>; + + next_jump_us = 0x0000000000000000U; + speedometer_value = 0.0f; + footsteps_distance = 0.0f; + + // UNDONE: make this a separate subsystem + pitch_random.seed(std::random_device()()); + pitch_distrib = std::uniform_real_distribution<float>(0.9f, 1.1f); + + globals::client_config.add_value("player_move.key.forward", key_move_forward); + globals::client_config.add_value("player_move.key.back", key_move_back); + globals::client_config.add_value("player_move.key.left", key_move_left); + globals::client_config.add_value("player_move.key.right", key_move_right); + globals::client_config.add_value("player_move.key.down", key_move_down); + globals::client_config.add_value("player_move.key.up", key_move_up); + globals::client_config.add_value("player_move.gp_axis.move_forward", axis_move_forward); + globals::client_config.add_value("player_move.gp_axis.move_sideways", axis_move_sideways); + globals::client_config.add_value("player_move.gp_button.move_down", button_move_down); + globals::client_config.add_value("player_move.gp_button.move_up", button_move_up); + globals::client_config.add_value("player_move.enable_speedometer", enable_speedometer); + + settings::add_keybind(1, key_move_forward, settings_location::KEYBOARD_MOVEMENT, "player_move.key.forward"); + settings::add_keybind(2, key_move_back, settings_location::KEYBOARD_MOVEMENT, "player_move.key.back"); + settings::add_keybind(3, key_move_left, settings_location::KEYBOARD_MOVEMENT, "player_move.key.left"); + settings::add_keybind(4, key_move_right, settings_location::KEYBOARD_MOVEMENT, "player_move.key.right"); + settings::add_keybind(5, key_move_down, settings_location::KEYBOARD_MOVEMENT, "player_move.key.down"); + settings::add_keybind(6, key_move_up, settings_location::KEYBOARD_MOVEMENT, "player_move.key.up"); + + settings::add_gamepad_axis(0, axis_move_forward, settings_location::GAMEPAD_MOVEMENT, "player_move.gp_axis.move_forward"); + settings::add_gamepad_axis(1, axis_move_sideways, settings_location::GAMEPAD_MOVEMENT, "player_move.gp_axis.move_sideways"); + settings::add_gamepad_button(2, button_move_down, settings_location::GAMEPAD_MOVEMENT, "player_move.gp_button.move_down"); + settings::add_gamepad_button(3, button_move_up, settings_location::GAMEPAD_MOVEMENT, "player_move.gp_button.move_up"); + + settings::add_checkbox(2, enable_speedometer, settings_location::VIDEO_GUI, "player_move.enable_speedometer", true); +} + +void player_move::fixed_update(void) +{ + const auto& head = globals::dimension->entities.get<HeadComponent>(globals::player); + auto& transform = globals::dimension->entities.get<TransformComponent>(globals::player); + auto& velocity = globals::dimension->entities.get<VelocityComponent>(globals::player); + + // Interpolation - preserve current component states + globals::dimension->entities.emplace_or_replace<TransformComponentPrev>(globals::player, transform); + + glm::fvec3 forward, right; + cxangles::vectors(glm::fvec3(0.0f, head.angles[1], 0.0f), &forward, &right, nullptr); + + glm::fvec3 wishdir = ZERO_VEC3<float>; + glm::fvec3 movevars = glm::fvec3(movement_direction.x, 0.0f, movement_direction.z); + wishdir.x = glm::dot(movevars, right); + wishdir.z = glm::dot(movevars, forward); + + if(toggles::get(TOGGLE_PM_FLIGHT)) { + velocity.value = pm_flight_move(glm::fvec3(wishdir.x, movement_direction.y, wishdir.z)); + return; + } + + auto grounded = globals::dimension->entities.try_get<GroundedComponent>(globals::player); + auto velocity_horizontal = glm::fvec3(velocity.value.x, 0.0f, velocity.value.z); + + if(grounded) { + auto new_velocity = pm_ground_move(wishdir, velocity_horizontal); + velocity.value.x = new_velocity.x; + velocity.value.z = new_velocity.z; + + auto new_speed = glm::length(new_velocity); + + if(new_speed > 0.01f) { + footsteps_distance += globals::fixed_frametime * new_speed; + } else { + footsteps_distance = 0.0f; + } + + if(footsteps_distance >= PMOVE_FOOTSTEP_SIZE) { + if(auto effect = voxel_sounds::get_footsteps(grounded->surface)) { + sound::play_player(effect, false, pitch_distrib(pitch_random)); + } + + footsteps_distance = 0.0f; + } + } else { + auto new_velocity = pm_air_move(wishdir, velocity_horizontal); + velocity.value.x = new_velocity.x; + velocity.value.z = new_velocity.z; + } + + if(movement_direction.y == 0.0f) { + // Allow players to queue bunny-jumps by quickly + // releasing and pressing the jump key again without a cooldown + next_jump_us = 0x0000000000000000U; + return; + } + + if(grounded && (movement_direction.y > 0.0f) && (globals::curtime >= next_jump_us)) { + velocity.value.y = -PMOVE_JUMP_FORCE * globals::dimension->get_gravity(); + + auto new_speed = glm::length(glm::fvec2(velocity.value.x, velocity.value.z)); + auto new_speed_text = std::format("{:.02f} M/S", new_speed); + auto speed_change = new_speed - speedometer_value; + + speedometer_value = new_speed; + + next_jump_us = globals::curtime + PMOVE_JUMP_COOLDOWN; + + if(enable_speedometer.get_value()) { + if(vx::abs(speed_change) < 0.01f) { + // No considerable speed increase within + // the precision we use to draw the speedometer + status_lines::set(STATUS_DEBUG, new_speed_text, ImVec4(0.7f, 0.7f, 0.7f, 1.0f), 1.0f); + } else if(speed_change < 0.0f) { + // Speed change is negative, we are actively + // slowing down; use the red color for the status line + status_lines::set(STATUS_DEBUG, new_speed_text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f), 1.0f); + } else { + // Speed change is positive, we are actively + // speeding up; use the green color for the status line + status_lines::set(STATUS_DEBUG, new_speed_text, ImVec4(0.0f, 1.0f, 0.0f, 1.0f), 1.0f); + } + } + + if(auto effect = voxel_sounds::get_footsteps(grounded->surface)) { + sound::play_player(effect, false, 1.0f); + } + } +} + +void player_move::update_late(void) +{ + movement_direction = ZERO_VEC3<float>; + + if(globals::gui_screen || !session::is_ingame()) { + // We're either disconnected or have the + // UI opened up; anyways we shouldn't move + return; + } + + if(gamepad::available && gamepad::active.get_value()) { + if(button_move_down.is_pressed(gamepad::state)) { + movement_direction += DIR_DOWN<float>; + } + + if(button_move_up.is_pressed(gamepad::state)) { + movement_direction += DIR_UP<float>; + } + + movement_direction.x += axis_move_sideways.get_value(gamepad::state, gamepad::deadzone.get_value()); + movement_direction.z -= axis_move_forward.get_value(gamepad::state, gamepad::deadzone.get_value()); + } else { + if(GLFW_PRESS == glfwGetKey(globals::window, key_move_forward.get_key())) { + movement_direction += DIR_FORWARD<float>; + } + + if(GLFW_PRESS == glfwGetKey(globals::window, key_move_back.get_key())) { + movement_direction += DIR_BACK<float>; + } + + if(GLFW_PRESS == glfwGetKey(globals::window, key_move_left.get_key())) { + movement_direction += DIR_LEFT<float>; + } + + if(GLFW_PRESS == glfwGetKey(globals::window, key_move_right.get_key())) { + movement_direction += DIR_RIGHT<float>; + } + + if(GLFW_PRESS == glfwGetKey(globals::window, key_move_down.get_key())) { + movement_direction += DIR_DOWN<float>; + } + + if(GLFW_PRESS == glfwGetKey(globals::window, key_move_up.get_key())) { + movement_direction += DIR_UP<float>; + } + } +} diff --git a/src/game/client/player_move.hh b/src/game/client/player_move.hh new file mode 100644 index 0000000..7730d08 --- /dev/null +++ b/src/game/client/player_move.hh @@ -0,0 +1,19 @@ +#ifndef CLIENT_PLAYER_MOVE_HH +#define CLIENT_PLAYER_MOVE_HH 1 +#pragma once + +constexpr static float PMOVE_MAX_SPEED_AIR = 16.0f; +constexpr static float PMOVE_MAX_SPEED_GROUND = 8.0f; +constexpr static float PMOVE_ACCELERATION_AIR = 3.0f; +constexpr static float PMOVE_ACCELERATION_GROUND = 6.0f; +constexpr static float PMOVE_FRICTION_GROUND = 10.0f; +constexpr static float PMOVE_JUMP_FORCE = 0.275f; + +namespace player_move +{ +void init(void); +void fixed_update(void); +void update_late(void); +} // namespace player_move + +#endif /* CLIENT_PLAYER_MOVE_HH */ diff --git a/src/game/client/player_target.cc b/src/game/client/player_target.cc new file mode 100644 index 0000000..9c60c00 --- /dev/null +++ b/src/game/client/player_target.cc @@ -0,0 +1,66 @@ +#include "client/pch.hh" + +#include "client/player_target.hh" + +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/ray_dda.hh" + +#include "client/camera.hh" +#include "client/game.hh" +#include "client/globals.hh" +#include "client/outline.hh" +#include "client/session.hh" + +constexpr static float MAX_REACH = 16.0f; + +voxel_id player_target::voxel; +voxel_pos player_target::coord; +voxel_pos player_target::normal; +const VoxelInfo* player_target::info; + +void player_target::init(void) +{ + player_target::voxel = NULL_VOXEL_ID; + player_target::coord = voxel_pos(); + player_target::normal = voxel_pos(); + player_target::info = nullptr; +} + +void player_target::update(void) +{ + if(session::is_ingame()) { + RayDDA ray(globals::dimension, camera::position_chunk, camera::position_local, camera::direction); + + do { + player_target::voxel = ray.step(); + + if(player_target::voxel != NULL_VOXEL_ID) { + player_target::coord = ray.vpos; + player_target::normal = ray.vnormal; + player_target::info = voxel_registry::find(player_target::voxel); + break; + } + + player_target::coord = voxel_pos(); + player_target::normal = voxel_pos(); + player_target::info = nullptr; + } while(ray.distance < MAX_REACH); + } else { + player_target::voxel = NULL_VOXEL_ID; + player_target::coord = voxel_pos(); + player_target::normal = voxel_pos(); + player_target::info = nullptr; + } +} + +void player_target::render(void) +{ + if((player_target::voxel != NULL_VOXEL_ID) && !client_game::hide_hud) { + auto cpos = coord::to_chunk(player_target::coord); + auto fpos = coord::to_local(player_target::coord); + + outline::prepare(); + outline::cube(cpos, glm::fvec3(fpos), glm::fvec3(1.0f), 2.0f, glm::fvec4(0.0f, 0.0f, 0.0f, 1.0f)); + } +} diff --git a/src/game/client/player_target.hh b/src/game/client/player_target.hh new file mode 100644 index 0000000..b60d1a5 --- /dev/null +++ b/src/game/client/player_target.hh @@ -0,0 +1,22 @@ +#ifndef CLIENT_PLAYER_TARGET_HH +#define CLIENT_PLAYER_TARGET_HH 1 +#pragma once + +#include "shared/voxel_registry.hh" + +namespace player_target +{ +extern voxel_id voxel; +extern voxel_pos coord; +extern voxel_pos normal; +extern const VoxelInfo* info; +} // namespace player_target + +namespace player_target +{ +void init(void); +void update(void); +void render(void); +} // namespace player_target + +#endif /* CLIENT_PLAYER_TARGET_HH */ diff --git a/src/game/client/program.cc b/src/game/client/program.cc new file mode 100644 index 0000000..8d4403a --- /dev/null +++ b/src/game/client/program.cc @@ -0,0 +1,223 @@ +#include "client/pch.hh" + +#include "client/program.hh" + +#include "core/strtools.hh" + +// This fills up the array of source lines and figures out +// which lines are to be dynamically resolved as variant macros +static void parse_source(const char* source, std::vector<std::string>& out_lines, std::vector<GL_VariedMacro>& out_variants) +{ + std::string line; + std::istringstream stream = std::istringstream(source); + unsigned long line_number = 0UL; + + out_lines.clear(); + out_variants.clear(); + + while(std::getline(stream, line)) { + unsigned int macro_index = {}; + char macro_name[128] = {}; + + if(std::sscanf(line.c_str(), " # pragma variant [ %u ] %127[^, \"\t\r\n]", ¯o_index, ¯o_name) == 2) { + if(out_variants.size() <= macro_index) { + out_variants.resize(macro_index + 1U); + } + + out_variants[macro_index].name = macro_name; + out_variants[macro_index].line = line_number; + out_variants[macro_index].value = std::numeric_limits<unsigned int>::max(); + + out_lines.push_back(std::string()); + line_number += 1UL; + } else { + out_lines.push_back(line); + line_number += 1UL; + } + } +} + +static GLuint compile_shader(const char* path, const char* source, GLenum shader_stage) +{ + GLuint shader = glCreateShader(shader_stage); + glShaderSource(shader, 1, &source, nullptr); + glCompileShader(shader); + + GLint info_log_length; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &info_log_length); + + if(info_log_length >= 2) { + std::basic_string<GLchar> info_log; + info_log.resize(info_log_length); + glGetShaderInfoLog(shader, info_log_length, nullptr, info_log.data()); + spdlog::info("gl_program: {}: shader information:", path); + spdlog::info(info_log); + } + + GLint compile_status; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compile_status); + + if(!compile_status) { + glDeleteShader(shader); + return 0; + } + + return shader; +} + +bool GL_Program::setup(const char* vpath, const char* fpath) +{ + destroy(); + + vert_path = std::string(vpath); + frag_path = std::string(fpath); + + auto vfile = PHYSFS_openRead(vpath); + + if(vfile == nullptr) { + spdlog::warn("gl_program: {}: {}", vpath, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + return false; + } + + auto vsource = std::string(PHYSFS_fileLength(vfile), char(0x00)); + PHYSFS_readBytes(vfile, vsource.data(), vsource.size()); + PHYSFS_close(vfile); + + auto ffile = PHYSFS_openRead(fpath); + + if(ffile == nullptr) { + spdlog::warn("gl_program: {}: {}", fpath, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + return false; + } + + auto fsource = std::string(PHYSFS_fileLength(ffile), char(0x00)); + PHYSFS_readBytes(ffile, fsource.data(), fsource.size()); + PHYSFS_close(ffile); + + parse_source(vsource.c_str(), vert_source, vert_variants); + parse_source(fsource.c_str(), frag_source, frag_variants); + + needs_update = true; + handle = 0; + + return true; +} + +bool GL_Program::update(void) +{ + if(!needs_update) { + // The program is already up to + // date with the internal state + return true; + } + + for(const auto& macro : vert_variants) + vert_source[macro.line] = std::format("#define {} {}", macro.name, macro.value); + for(const auto& macro : frag_variants) + frag_source[macro.line] = std::format("#define {} {}", macro.name, macro.value); + + const std::string vsource = strtools::join(vert_source, "\r\n"); + const std::string fsource = strtools::join(frag_source, "\r\n"); + + GLuint vert = compile_shader(vert_path.c_str(), vsource.c_str(), GL_VERTEX_SHADER); + GLuint frag = compile_shader(frag_path.c_str(), fsource.c_str(), GL_FRAGMENT_SHADER); + + if(!vert || !frag) { + // needs_update = false; + glDeleteShader(frag); + glDeleteShader(vert); + return false; + } + + handle = glCreateProgram(); + glAttachShader(handle, vert); + glAttachShader(handle, frag); + glLinkProgram(handle); + + GLint info_log_length; + glGetProgramiv(handle, GL_INFO_LOG_LENGTH, &info_log_length); + + if(info_log_length >= 2) { + std::basic_string<GLchar> info_log; + info_log.resize(info_log_length); + glGetProgramInfoLog(handle, info_log_length, nullptr, info_log.data()); + spdlog::info("gl_program: [{}; {}]: program information", vert, frag); + spdlog::info(info_log); + } + + glDeleteShader(frag); + glDeleteShader(vert); + + GLint link_status; + glGetProgramiv(handle, GL_LINK_STATUS, &link_status); + + if(!link_status) { + // needs_update = false; + glDeleteProgram(handle); + return false; + } + + for(auto& uniform : uniforms) { + // NOTE: GL seems to silently ignore invalid uniform + // locations (-1); should we write something into logs about this? + uniform.location = glGetUniformLocation(handle, uniform.name.c_str()); + } + + needs_update = false; + return true; +} + +void GL_Program::destroy(void) +{ + if(handle) { + glDeleteProgram(handle); + handle = 0; + } + + uniforms.clear(); + + frag_variants.clear(); + frag_source.clear(); + frag_path = std::string(); + + vert_variants.clear(); + vert_source.clear(); + vert_path = std::string(); + + needs_update = false; +} + +std::size_t GL_Program::add_uniform(const char* name) +{ + for(std::size_t i = 0; i < uniforms.size(); ++i) { + if(!uniforms[i].name.compare(name)) { + return i; + } + } + + const std::size_t index = uniforms.size(); + uniforms.push_back(GL_Uniform()); + uniforms[index].location = -1; + uniforms[index].name = name; + return index; +} + +void GL_Program::set_variant_vert(unsigned int variant, unsigned int value) +{ + if(variant < vert_variants.size()) { + if(value != vert_variants[variant].value) { + vert_variants[variant].value = value; + needs_update = true; + } + } +} + +void GL_Program::set_variant_frag(unsigned int variant, unsigned int value) +{ + if(variant < frag_variants.size()) { + if(value != frag_variants[variant].value) { + frag_variants[variant].value = value; + needs_update = true; + } + } +} diff --git a/src/game/client/program.hh b/src/game/client/program.hh new file mode 100644 index 0000000..4d83e14 --- /dev/null +++ b/src/game/client/program.hh @@ -0,0 +1,38 @@ +#ifndef CLIENT_PROGRAM_HH +#define CLIENT_PROGRAM_HH 1 +#pragma once + +struct GL_VariedMacro final { + std::string name; + unsigned long line; + unsigned int value; +}; + +struct GL_Uniform final { + std::string name; + GLint location; +}; + +class GL_Program final { +public: + bool setup(const char* vpath, const char* fpath); + void destroy(void); + bool update(void); + + std::size_t add_uniform(const char* name); + void set_variant_vert(unsigned int variant, unsigned int value); + void set_variant_frag(unsigned int variant, unsigned int value); + +public: + std::string vert_path; + std::string frag_path; + std::vector<std::string> vert_source; + std::vector<std::string> frag_source; + std::vector<GL_VariedMacro> vert_variants; + std::vector<GL_VariedMacro> frag_variants; + std::vector<GL_Uniform> uniforms; + bool needs_update; + GLuint handle; +}; + +#endif /* CLIENT_PROGRAM_HH */ diff --git a/src/game/client/progress_bar.cc b/src/game/client/progress_bar.cc new file mode 100644 index 0000000..dfab705 --- /dev/null +++ b/src/game/client/progress_bar.cc @@ -0,0 +1,110 @@ +#include "client/pch.hh" + +#include "client/progress_bar.hh" + +#include "core/constexpr.hh" + +#include "client/globals.hh" +#include "client/language.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +static std::string str_title; +static std::string str_button; +static progress_bar_action button_action; + +void progress_bar::init(void) +{ + str_title = "Loading"; + str_button = std::string(); + button_action = nullptr; +} + +void progress_bar::layout(void) +{ + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(0.0f, viewport->Size.y * 0.30f); + const auto window_size = ImVec2(viewport->Size.x, viewport->Size.y * 0.70f); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###UIProgress", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 1.0f * globals::gui_scale)); + + const float title_width = ImGui::CalcTextSize(str_title.c_str()).x; + ImGui::SetCursorPosX(0.5f * (window_size.x - title_width)); + ImGui::TextUnformatted(str_title.c_str()); + + ImGui::Dummy(ImVec2(0.0f, 8.0f * globals::gui_scale)); + + const ImVec2 cursor = ImGui::GetCursorPos(); + + const std::size_t num_bars = 32; + const float spinner_width = 0.8f * ImGui::CalcItemWidth(); + const float bar_width = spinner_width / static_cast<float>(num_bars); + const float bar_height = 0.5f * ImGui::GetFrameHeight(); + + const float base_xpos = window_start.x + 0.5f * (window_size.x - spinner_width) + 0.5f; + const float base_ypos = window_start.y + cursor.y; + const float phase = 2.0f * ImGui::GetTime(); + + const ImVec4& background = ImGui::GetStyleColorVec4(ImGuiCol_Button); + const ImVec4& foreground = ImGui::GetStyleColorVec4(ImGuiCol_PlotHistogram); + + for(std::size_t i = 0; i < num_bars; ++i) { + const float sinval = std::sin(M_PI * static_cast<float>(i) / static_cast<float>(num_bars) - phase); + const float modifier = std::exp(-8.0f * (0.5f + 0.5f * sinval)); + + ImVec4 color = {}; + color.x = vx::lerp(background.x, foreground.x, modifier); + color.y = vx::lerp(background.y, foreground.y, modifier); + color.z = vx::lerp(background.z, foreground.z, modifier); + color.w = vx::lerp(background.w, foreground.w, modifier); + + const ImVec2 start = ImVec2(base_xpos + bar_width * i, base_ypos); + const ImVec2 end = ImVec2(start.x + bar_width, start.y + bar_height); + ImGui::GetWindowDrawList()->AddRectFilled(start, end, ImGui::GetColorU32(color)); + } + + // The NewLine call tricks ImGui into correctly padding the + // next widget that comes after the progress_bar spinner; this + // is needed to ensure the button is located in the correct place + ImGui::NewLine(); + + if(!str_button.empty()) { + ImGui::Dummy(ImVec2(0.0f, 32.0f * globals::gui_scale)); + + const float button_width = 0.8f * ImGui::CalcItemWidth(); + ImGui::SetCursorPosX(0.5f * (window_size.x - button_width)); + + if(ImGui::Button(str_button.c_str(), ImVec2(button_width, 0.0f))) { + if(button_action) { + button_action(); + } + } + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} + +void progress_bar::reset(void) +{ + str_title.clear(); + str_button.clear(); + button_action = nullptr; +} + +void progress_bar::set_title(const char* title) +{ + str_title = language::resolve(title); +} + +void progress_bar::set_button(const char* text, const progress_bar_action& action) +{ + str_button = std::format("{}###ProgressBar_Button", language::resolve(text)); + button_action = action; +} diff --git a/src/game/client/progress_bar.hh b/src/game/client/progress_bar.hh new file mode 100644 index 0000000..5375d3f --- /dev/null +++ b/src/game/client/progress_bar.hh @@ -0,0 +1,20 @@ +#ifndef CLIENT_PROGRESS_BAR_HH +#define CLIENT_PROGRESS_BAR_HH 1 +#pragma once + +using progress_bar_action = void (*)(void); + +namespace progress_bar +{ +void init(void); +void layout(void); +} // namespace progress_bar + +namespace progress_bar +{ +void reset(void); +void set_title(const char* title); +void set_button(const char* text, const progress_bar_action& action); +} // namespace progress_bar + +#endif /* CLIENT_PROGRESS_BAR_HH */ diff --git a/src/game/client/receive.cc b/src/game/client/receive.cc new file mode 100644 index 0000000..742d3af --- /dev/null +++ b/src/game/client/receive.cc @@ -0,0 +1,187 @@ +#include "client/pch.hh" + +#include "client/receive.hh" + +#include "shared/dimension.hh" +#include "shared/head.hh" +#include "shared/player.hh" +#include "shared/protocol.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" + +#include "client/chat.hh" +#include "client/factory.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/message_box.hh" +#include "client/session.hh" +#include "client/sound.hh" +#include "client/window_title.hh" + +static bool synchronize_entity_id(Dimension* dimension, entt::entity entity) +{ + if(dimension->entities.valid(entity)) { + // Entity ID already exists + return true; + } + + auto created = dimension->entities.create(entity); + + if(created == entity) { + // Synchronized successfully + return true; + } + + session::disconnect("protocol.entity_id_desync"); + spdlog::critical("receive: entity desync: network {} resolved as client {}", static_cast<std::uint64_t>(entity), + static_cast<std::uint64_t>(created)); + + message_box::reset(); + message_box::set_title("disconnected.disconnected"); + message_box::set_subtitle("protocol.entity_id_desync"); + message_box::add_button("disconnected.back", [](void) { + globals::gui_screen = GUI_PLAY_MENU; + window_title::update(); + }); + + globals::gui_screen = GUI_MESSAGE_BOX; + + return false; +} + +static void on_dimension_info_packet(const protocol::DimensionInfo& packet) +{ + if(session::peer) { + if(globals::dimension) { + delete globals::dimension; + globals::dimension = nullptr; + globals::player = entt::null; + } + + globals::dimension = new Dimension(packet.name.c_str(), packet.gravity); + } +} + +static void on_chunk_voxels_packet(const protocol::ChunkVoxels& packet) +{ + if(session::peer && globals::dimension) { + auto chunk = globals::dimension->create_chunk(packet.chunk); + chunk->set_voxels(packet.voxels); + + ChunkUpdateEvent event; + event.dimension = globals::dimension; + event.cpos = packet.chunk; + event.chunk = chunk; + + globals::dispatcher.trigger(event); + + return; + } +} + +static void on_entity_head_packet(const protocol::EntityHead& packet) +{ + if(session::peer && globals::dimension) { + if(synchronize_entity_id(globals::dimension, packet.entity)) { + auto& component = globals::dimension->entities.get_or_emplace<HeadComponent>(packet.entity); + auto& prevcomp = globals::dimension->entities.get_or_emplace<HeadComponentPrev>(packet.entity); + + // Store the previous component state + prevcomp.angles = component.angles; + prevcomp.offset = component.offset; + + // Assign the new component state + component.angles = packet.angles; + } + } +} + +static void on_entity_transform_packet(const protocol::EntityTransform& packet) +{ + if(session::peer && globals::dimension) { + if(synchronize_entity_id(globals::dimension, packet.entity)) { + auto& component = globals::dimension->entities.get_or_emplace<TransformComponent>(packet.entity); + auto& prevcomp = globals::dimension->entities.get_or_emplace<TransformComponentPrev>(packet.entity); + + // Store the previous component state + prevcomp.angles = component.angles; + prevcomp.chunk = component.chunk; + prevcomp.local = component.local; + + // Assign the new component state + component.angles = packet.angles; + component.chunk = packet.chunk; + component.local = packet.local; + } + } +} + +static void on_entity_velocity_packet(const protocol::EntityVelocity& packet) +{ + if(session::peer && globals::dimension) { + if(synchronize_entity_id(globals::dimension, packet.entity)) { + auto& component = globals::dimension->entities.get_or_emplace<VelocityComponent>(packet.entity); + component.value = packet.value; + } + } +} + +static void on_entity_player_packet(const protocol::EntityPlayer& packet) +{ + if(session::peer && globals::dimension) { + if(synchronize_entity_id(globals::dimension, packet.entity)) { + client_factory::create_player(globals::dimension, packet.entity); + } + } +} + +static void on_spawn_player_packet(const protocol::SpawnPlayer& packet) +{ + if(session::peer && globals::dimension) { + if(synchronize_entity_id(globals::dimension, packet.entity)) { + client_factory::create_player(globals::dimension, packet.entity); + + globals::player = packet.entity; + globals::gui_screen = GUI_SCREEN_NONE; + + client_chat::refresh_timings(); + + window_title::update(); + } + } +} + +static void on_remove_entity_packet(const protocol::RemoveEntity& packet) +{ + if(globals::dimension) { + if(packet.entity == globals::player) { + globals::player = entt::null; + } + + globals::dimension->entities.destroy(packet.entity); + } +} + +static void on_generic_sound_packet(const protocol::GenericSound& packet) +{ + sound::play_generic(packet.sound.c_str(), packet.looping, packet.pitch); +} + +static void on_entity_sound_packet(const protocol::EntitySound& packet) +{ + sound::play_entity(packet.entity, packet.sound.c_str(), packet.looping, packet.pitch); +} + +void client_receive::init(void) +{ + globals::dispatcher.sink<protocol::DimensionInfo>().connect<&on_dimension_info_packet>(); + globals::dispatcher.sink<protocol::ChunkVoxels>().connect<&on_chunk_voxels_packet>(); + globals::dispatcher.sink<protocol::EntityHead>().connect<&on_entity_head_packet>(); + globals::dispatcher.sink<protocol::EntityTransform>().connect<&on_entity_transform_packet>(); + globals::dispatcher.sink<protocol::EntityVelocity>().connect<&on_entity_velocity_packet>(); + globals::dispatcher.sink<protocol::EntityPlayer>().connect<&on_entity_player_packet>(); + globals::dispatcher.sink<protocol::SpawnPlayer>().connect<&on_spawn_player_packet>(); + globals::dispatcher.sink<protocol::RemoveEntity>().connect<&on_remove_entity_packet>(); + globals::dispatcher.sink<protocol::GenericSound>().connect<&on_generic_sound_packet>(); + globals::dispatcher.sink<protocol::EntitySound>().connect<&on_entity_sound_packet>(); +} diff --git a/src/game/client/receive.hh b/src/game/client/receive.hh new file mode 100644 index 0000000..27ed2b2 --- /dev/null +++ b/src/game/client/receive.hh @@ -0,0 +1,10 @@ +#ifndef CLIENT_RECEIVE_HH +#define CLIENT_RECEIVE_HH 1 +#pragma once + +namespace client_receive +{ +void init(void); +} // namespace client_receive + +#endif /* CLIENT_RECEIVE_HH */ diff --git a/src/game/client/scoreboard.cc b/src/game/client/scoreboard.cc new file mode 100644 index 0000000..23010ea --- /dev/null +++ b/src/game/client/scoreboard.cc @@ -0,0 +1,100 @@ +#include "client/pch.hh" + +#include "client/scoreboard.hh" + +#include "core/config.hh" + +#include "shared/protocol.hh" + +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/keybind.hh" +#include "client/session.hh" +#include "client/settings.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground; + +static ConfigKeyBind list_key(GLFW_KEY_TAB); + +static std::vector<std::string> usernames; +static float max_username_size; + +static void on_scoreboard_update_packet(const protocol::ScoreboardUpdate& packet) +{ + usernames = packet.names; + max_username_size = 0.0f; +} + +void scoreboard::init(void) +{ + globals::client_config.add_value("scoreboard.key", list_key); + + settings::add_keybind(3, list_key, settings_location::KEYBOARD_MISC, "key.scoreboard"); + + globals::dispatcher.sink<protocol::ScoreboardUpdate>().connect<&on_scoreboard_update_packet>(); +} + +void scoreboard::layout(void) +{ + if(globals::gui_screen == GUI_SCREEN_NONE && session::is_ingame() && glfwGetKey(globals::window, list_key.get_key()) == GLFW_PRESS) { + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(0.0f, 0.0f); + const auto window_size = ImVec2(viewport->Size.x, viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(!ImGui::Begin("###chat", nullptr, WINDOW_FLAGS)) { + ImGui::End(); + return; + } + + ImGui::PushFont(globals::font_chat); + + const auto& padding = ImGui::GetStyle().FramePadding; + const auto& spacing = ImGui::GetStyle().ItemSpacing; + auto font = globals::font_chat; + + // Figure out the maximum username size + for(const auto& username : usernames) { + const ImVec2 size = ImGui::CalcTextSize(username.c_str(), username.c_str() + username.size()); + + if(size.x > max_username_size) { + max_username_size = size.x; + } + } + + // Having a minimum size allows for + // generally better in-game visibility + const float true_size = vx::max<float>(0.25f * window_size.x, max_username_size); + + // Figure out username rect dimensions + const float rect_start_x = 0.5f * window_size.x - 0.5f * true_size; + const float rect_start_y = 0.15f * window_size.y; + const float rect_size_x = 2.0f * padding.x + true_size; + const float rect_size_y = 2.0f * padding.y + font->FontSize; + + // const ImU32 border_col = ImGui::GetColorU32(ImGuiCol_Border, 1.00f); + const ImU32 rect_col = ImGui::GetColorU32(ImGuiCol_FrameBg, 0.80f); + const ImU32 text_col = ImGui::GetColorU32(ImGuiCol_Text, 1.00f); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + // Slightly space apart individual rows + const float row_step_y = rect_size_y + 0.5f * spacing.y; + + for(std::size_t i = 0; i < usernames.size(); ++i) { + const ImVec2 rect_a = ImVec2(rect_start_x, rect_start_y + i * row_step_y); + const ImVec2 rect_b = ImVec2(rect_a.x + rect_size_x, rect_a.y + rect_size_y); + const ImVec2 text_pos = ImVec2(rect_a.x + padding.x, rect_a.y + padding.y); + + // draw_list->AddRect(rect_a, rect_b, border_col, 0.0f, ImDrawFlags_None, globals::gui_scale); + draw_list->AddRectFilled(rect_a, rect_b, rect_col, 0.0f, ImDrawFlags_None); + draw_list->AddText(font, font->FontSize, text_pos, text_col, usernames[i].c_str(), usernames[i].c_str() + usernames[i].size()); + } + + ImGui::PopFont(); + ImGui::End(); + } +} diff --git a/src/game/client/scoreboard.hh b/src/game/client/scoreboard.hh new file mode 100644 index 0000000..0ae1688 --- /dev/null +++ b/src/game/client/scoreboard.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_SCOREBOARD_HH +#define CLIENT_SCOREBOARD_HH 1 +#pragma once + +namespace scoreboard +{ +void init(void); +void layout(void); +} // namespace scoreboard + +#endif /* CLIENT_SCOREBOARD_HH */ diff --git a/src/game/client/screenshot.cc b/src/game/client/screenshot.cc new file mode 100644 index 0000000..a42a815 --- /dev/null +++ b/src/game/client/screenshot.cc @@ -0,0 +1,82 @@ +#include "client/pch.hh" + +#include "client/screenshot.hh" + +#include "core/config.hh" +#include "core/epoch.hh" + +#include "client/chat.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/keybind.hh" +#include "client/language.hh" +#include "client/settings.hh" +#include "client/toggles.hh" + +static ConfigKeyBind screenshot_key(GLFW_KEY_F2); + +static void stbi_png_physfs_callback(void* context, void* data, int size) +{ + PHYSFS_writeBytes(reinterpret_cast<PHYSFS_File*>(context), data, size); +} + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if(!globals::gui_keybind_ptr && !toggles::is_sequence_await) { + if(screenshot_key.equals(event.key) && (event.action == GLFW_PRESS)) { + screenshot::take(); + return; + } + } +} + +void screenshot::init(void) +{ + globals::client_config.add_value("screenshot.key", screenshot_key); + + settings::add_keybind(0, screenshot_key, settings_location::KEYBOARD_MISC, "key.screenshot"); + + globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>(); +} + +void screenshot::take(void) +{ + auto stride = 3 * globals::width; + auto length = 3 * globals::width * globals::height; + auto pixels = new std::byte[length]; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + GLint old_pack_alignment; + glGetIntegerv(GL_PACK_ALIGNMENT, &old_pack_alignment); + + // The window can be of any size, including irregular + // values such as, say 641x480, while there is a default + // alignment value of sorts that might result in a corrupted + // image; we set GL_PACK_ALIGNMENT to 1, enabling byte-alignment + glPixelStorei(GL_PACK_ALIGNMENT, 1); + + glReadPixels(0, 0, globals::width, globals::height, GL_RGB, GL_UNSIGNED_BYTE, pixels); + + // Restore the old pack alignment value + glPixelStorei(GL_PACK_ALIGNMENT, old_pack_alignment); + + const auto directory = std::string("screenshots"); + const auto filename = std::format("{}.png", epoch::microseconds()); + const auto filepath = std::format("{}/{}", directory, filename); + + PHYSFS_mkdir(directory.c_str()); + + if(auto file = PHYSFS_openWrite(filepath.c_str())) { + stbi_flip_vertically_on_write(true); + stbi_write_png_to_func(&stbi_png_physfs_callback, file, globals::width, globals::height, 3, pixels, stride); + + spdlog::info("screenshot: wrote {}", filepath); + + client_chat::print(std::format("{} {}", language::resolve("chat.screenshot_message"), filename)); + + PHYSFS_close(file); + } + + delete[] pixels; +} diff --git a/src/game/client/screenshot.hh b/src/game/client/screenshot.hh new file mode 100644 index 0000000..9888644 --- /dev/null +++ b/src/game/client/screenshot.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_SCREENSHOT_HH +#define CLIENT_SCREENSHOT_HH 1 +#pragma once + +namespace screenshot +{ +void init(void); +void take(void); +} // namespace screenshot + +#endif /* CLIENT_SCREENSHOT_HH */ diff --git a/src/game/client/session.cc b/src/game/client/session.cc new file mode 100644 index 0000000..d3676f3 --- /dev/null +++ b/src/game/client/session.cc @@ -0,0 +1,300 @@ +#include "client/pch.hh" + +#include "client/session.hh" + +#include "core/config.hh" +#include "core/crc64.hh" + +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/head.hh" +#include "shared/item_registry.hh" +#include "shared/player.hh" +#include "shared/protocol.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" +#include "shared/voxel_registry.hh" + +#include "client/camera.hh" +#include "client/chat.hh" +#include "client/chunk_visibility.hh" +#include "client/game.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/message_box.hh" +#include "client/progress_bar.hh" +#include "client/window_title.hh" + +ENetPeer* session::peer = nullptr; +std::uint16_t session::client_index = UINT16_MAX; +std::uint64_t session::client_identity = UINT64_MAX; + +static std::uint64_t server_password_hash = UINT64_MAX; + +static void set_fixed_tickrate(std::uint16_t tickrate) +{ + globals::fixed_frametime_us = 1000000U / vx::clamp<std::uint64_t>(tickrate, 10U, 300U); + globals::fixed_frametime = static_cast<float>(globals::fixed_frametime_us) / 1000000.0f; + globals::fixed_accumulator = 0; +} + +static void on_login_response_packet(const protocol::LoginResponse& packet) +{ + spdlog::info("session: assigned client_index={}", packet.client_index); + spdlog::info("session: assigned client_identity={}", packet.client_identity); + spdlog::info("session: server ticks at {} TPS", packet.server_tickrate); + + session::client_index = packet.client_index; + session::client_identity = packet.client_identity; + + set_fixed_tickrate(packet.server_tickrate); + + progress_bar::set_title("connecting.loading_world"); +} + +static void on_disconnect_packet(const protocol::Disconnect& packet) +{ + enet_peer_disconnect(session::peer, 0); + + spdlog::info("session: disconnected: {}", packet.reason); + + client_chat::clear(); + + session::peer = nullptr; + session::client_index = UINT16_MAX; + session::client_identity = UINT64_MAX; + + globals::fixed_frametime_us = UINT64_MAX; + globals::fixed_frametime = 0.0f; + globals::fixed_accumulator = 0; + + server_password_hash = UINT64_MAX; + + delete globals::dimension; + globals::player = entt::null; + globals::dimension = nullptr; + + message_box::reset(); + message_box::set_title("disconnected.disconnected"); + message_box::set_subtitle(packet.reason.c_str()); + message_box::add_button("disconnected.back", [](void) { + globals::gui_screen = GUI_PLAY_MENU; + window_title::update(); + }); + + globals::gui_screen = GUI_MESSAGE_BOX; +} + +static void on_set_voxel_packet(const protocol::SetVoxel& packet) +{ + auto cpos = coord::to_chunk(packet.vpos); + auto lpos = coord::to_local(packet.vpos); + auto index = coord::to_index(lpos); + + if(auto chunk = globals::dimension->find_chunk(cpos)) { + if(chunk->get_voxel(index) != packet.voxel) { + chunk->set_voxel(packet.voxel, index); + + ChunkUpdateEvent event; + event.dimension = globals::dimension; + event.chunk = chunk; + event.cpos = cpos; + + // Send a generic ChunkUpdate event to shake + // up the mesher; directly calling world::set_voxel + // here would result in a networked feedback loop + // caused by event handler below tripping + globals::dispatcher.trigger(event); + } + } +} + +// NOTE: [session] is a good place for this since [receive] +// handles entity data sent by the server and [session] handles +// everything else network related that is not player movement +static void on_voxel_set(const VoxelSetEvent& event) +{ + if(session::peer) { + // Propagate changes to the server + // FIXME: should we also validate things here or wait for the server to do so + protocol::SetVoxel packet; + packet.vpos = coord::to_voxel(event.cpos, event.lpos); + packet.voxel = event.voxel; + + protocol::send(session::peer, protocol::encode(packet)); + } +} + +void session::init(void) +{ + session::peer = nullptr; + session::client_index = UINT16_MAX; + session::client_identity = UINT64_MAX; + + globals::fixed_frametime_us = UINT64_MAX; + globals::fixed_frametime = 0.0f; + globals::fixed_accumulator = 0; + + server_password_hash = UINT64_MAX; + + globals::dispatcher.sink<protocol::LoginResponse>().connect<&on_login_response_packet>(); + globals::dispatcher.sink<protocol::Disconnect>().connect<&on_disconnect_packet>(); + globals::dispatcher.sink<protocol::SetVoxel>().connect<&on_set_voxel_packet>(); + + globals::dispatcher.sink<VoxelSetEvent>().connect<&on_voxel_set>(); +} + +void session::deinit(void) +{ + session::disconnect("protocol.client_shutdown"); + + globals::fixed_frametime_us = UINT64_MAX; + globals::fixed_frametime = 0.0f; + globals::fixed_accumulator = 0; +} + +void session::invalidate(void) +{ + if(session::peer) { + enet_peer_reset(session::peer); + + message_box::reset(); + message_box::set_title("disconnected.disconnected"); + message_box::set_subtitle("enet.peer_connection_timeout"); + message_box::add_button("disconnected.back", [](void) { + globals::gui_screen = GUI_PLAY_MENU; + window_title::update(); + }); + + globals::gui_screen = GUI_MESSAGE_BOX; + } + + client_chat::clear(); + + session::peer = nullptr; + session::client_index = UINT16_MAX; + session::client_identity = UINT64_MAX; + + globals::fixed_frametime_us = UINT64_MAX; + globals::fixed_frametime = 0.0f; + globals::fixed_accumulator = 0; + + server_password_hash = UINT64_MAX; + + delete globals::dimension; + globals::player = entt::null; + globals::dimension = nullptr; +} + +void session::connect(const char* host, std::uint16_t port, const char* password) +{ + ENetAddress address; + enet_address_set_host(&address, host); + address.port = port; + + session::peer = enet_host_connect(globals::client_host, &address, 1, 0); + session::client_index = UINT16_MAX; + session::client_identity = UINT64_MAX; + + globals::fixed_frametime_us = UINT64_MAX; + globals::fixed_frametime = 0.0f; + globals::fixed_accumulator = 0; + + server_password_hash = crc64::get(password); + + if(!session::peer) { + server_password_hash = UINT64_MAX; + + message_box::reset(); + message_box::set_title("disconnected.disconnected"); + message_box::set_subtitle("enet.peer_connection_failed"); + message_box::add_button("disconnected.back", [](void) { + globals::gui_screen = GUI_PLAY_MENU; + window_title::update(); + }); + + globals::gui_screen = GUI_MESSAGE_BOX; + + return; + } + + progress_bar::reset(); + progress_bar::set_title("connecting.connecting"); + progress_bar::set_button("connecting.cancel_button", [](void) { + enet_peer_disconnect(session::peer, 0); + + session::peer = nullptr; + session::client_index = UINT16_MAX; + session::client_identity = UINT64_MAX; + + globals::fixed_frametime_us = UINT64_MAX; + globals::fixed_frametime = 0.0f; + globals::fixed_accumulator = 0; + + server_password_hash = UINT64_MAX; + + delete globals::dimension; + globals::player = entt::null; + globals::dimension = nullptr; + + globals::gui_screen = GUI_PLAY_MENU; + }); + + globals::gui_screen = GUI_PROGRESS_BAR; +} + +void session::disconnect(const char* reason) +{ + if(session::peer) { + protocol::Disconnect packet; + packet.reason = std::string(reason); + + protocol::send(session::peer, protocol::encode(packet)); + + enet_host_flush(globals::client_host); + enet_host_service(globals::client_host, nullptr, 50); + enet_peer_reset(session::peer); + + session::peer = nullptr; + session::client_index = UINT16_MAX; + session::client_identity = UINT64_MAX; + + globals::fixed_frametime_us = UINT64_MAX; + globals::fixed_frametime = 0.0f; + globals::fixed_accumulator = 0; + + server_password_hash = UINT64_MAX; + + delete globals::dimension; + globals::player = entt::null; + globals::dimension = nullptr; + + client_chat::clear(); + } +} + +void session::send_login_request(void) +{ + protocol::LoginRequest packet; + packet.version = protocol::VERSION; + packet.voxel_registry_checksum = voxel_registry::calcualte_checksum(); + packet.item_registry_checksum = item_registry::calcualte_checksum(); + packet.password_hash = server_password_hash; + packet.username = client_game::username.get(); + + protocol::send(session::peer, protocol::encode(packet)); + + server_password_hash = UINT64_MAX; + + progress_bar::set_title("connecting.logging_in"); + globals::gui_screen = GUI_PROGRESS_BAR; +} + +bool session::is_ingame(void) +{ + if(globals::dimension) { + return globals::dimension->entities.valid(globals::player); + } else { + return false; + } +} diff --git a/src/game/client/session.hh b/src/game/client/session.hh new file mode 100644 index 0000000..051b57f --- /dev/null +++ b/src/game/client/session.hh @@ -0,0 +1,31 @@ +#ifndef CLIENT_SESSION_HH +#define CLIENT_SESSION_HH 1 +#pragma once + +namespace session +{ +extern ENetPeer* peer; +extern std::uint16_t client_index; +extern std::uint64_t client_identity; +} // namespace session + +namespace session +{ +void init(void); +void deinit(void); +void invalidate(void); +} // namespace session + +namespace session +{ +void connect(const char* hostname, std::uint16_t port, const char* password); +void disconnect(const char* reason); +void send_login_request(void); +} // namespace session + +namespace session +{ +bool is_ingame(void); +} // namespace session + +#endif /* CLIENT_SESSION_HH */ diff --git a/src/game/client/settings.cc b/src/game/client/settings.cc new file mode 100644 index 0000000..46e1bde --- /dev/null +++ b/src/game/client/settings.cc @@ -0,0 +1,1055 @@ +#include "client/pch.hh" + +#include "client/settings.hh" + +#include "core/config.hh" +#include "core/constexpr.hh" + +#include "client/const.hh" +#include "client/gamepad.hh" +#include "client/gamepad_axis.hh" +#include "client/gamepad_button.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/keybind.hh" +#include "client/language.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; +constexpr static unsigned int NUM_LOCATIONS = static_cast<unsigned int>(settings_location::COUNT); + +enum class setting_type : unsigned int { + CHECKBOX = 0x0000U, ///< ConfigBoolean + INPUT_INT = 0x0001U, ///< ConfigNumber<int> + INPUT_FLOAT = 0x0002U, ///< ConfigNumber<float> + INPUT_UINT = 0x0003U, ///< ConfigNumber<unsigned int> + INPUT_STRING = 0x0004U, ///< ConfigString + SLIDER_INT = 0x0005U, ///< ConfigNumber<int> + SLIDER_FLOAT = 0x0006U, ///< ConfigNumber<float> + SLIDER_UINT = 0x0007U, ///< ConfigNumber<unsigned int> + STEPPER_INT = 0x0008U, ///< ConfigNumber<int> + STEPPER_UINT = 0x0009U, ///< ConfigNumber<unsigned int> + KEYBIND = 0x000AU, ///< ConfigKeyBind + GAMEPAD_AXIS = 0x000BU, ///< ConfigGamepadAxis + GAMEPAD_BUTTON = 0x000CU, ///< ConfigGamepadButton + LANGUAGE_SELECT = 0x000DU, ///< ConfigString internally +}; + +class SettingValue { +public: + virtual ~SettingValue(void) = default; + virtual void layout(void) const = 0; + void layout_tooltip(void) const; + void layout_label(void) const; + +public: + setting_type type; + std::string tooltip; + std::string title; + std::string name; + bool has_tooltip; + int priority; +}; + +class SettingValueWID : public SettingValue { +public: + virtual ~SettingValueWID(void) = default; + +public: + std::string wid; +}; + +class SettingValue_CheckBox final : public SettingValue { +public: + virtual ~SettingValue_CheckBox(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + ConfigBoolean* value; + std::string wids[2]; +}; + +class SettingValue_InputInt final : public SettingValueWID { +public: + virtual ~SettingValue_InputInt(void) = default; + virtual void layout(void) const override; + +public: + ConfigInt* value; +}; + +class SettingValue_InputFloat final : public SettingValueWID { +public: + virtual ~SettingValue_InputFloat(void) = default; + virtual void layout(void) const override; + +public: + std::string format; + ConfigFloat* value; +}; + +class SettingValue_InputUnsigned final : public SettingValueWID { +public: + virtual ~SettingValue_InputUnsigned(void) = default; + virtual void layout(void) const override; + +public: + ConfigUnsigned* value; +}; + +class SettingValue_InputString final : public SettingValueWID { +public: + virtual ~SettingValue_InputString(void) = default; + virtual void layout(void) const override; + +public: + ConfigString* value; + bool allow_whitespace; +}; + +class SettingValue_SliderInt final : public SettingValueWID { +public: + virtual ~SettingValue_SliderInt(void) = default; + virtual void layout(void) const override; + +public: + ConfigInt* value; +}; + +class SettingValue_SliderFloat final : public SettingValueWID { +public: + virtual ~SettingValue_SliderFloat(void) = default; + virtual void layout(void) const override; + +public: + std::string format; + ConfigFloat* value; +}; + +class SettingValue_SliderUnsigned final : public SettingValueWID { +public: + virtual ~SettingValue_SliderUnsigned(void) = default; + virtual void layout(void) const override; + +public: + ConfigUnsigned* value; +}; + +class SettingValue_StepperInt final : public SettingValue { +public: + virtual ~SettingValue_StepperInt(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + std::vector<std::string> wids; + ConfigInt* value; +}; + +class SettingValue_StepperUnsigned final : public SettingValue { +public: + virtual ~SettingValue_StepperUnsigned(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + std::vector<std::string> wids; + ConfigUnsigned* value; +}; + +class SettingValue_KeyBind final : public SettingValue { +public: + virtual ~SettingValue_KeyBind(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + std::string wids[2]; + ConfigKeyBind* value; +}; + +class SettingValue_GamepadAxis final : public SettingValue { +public: + virtual ~SettingValue_GamepadAxis(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + std::string wids[2]; + std::string wid_checkbox; + ConfigGamepadAxis* value; +}; + +class SettingValue_GamepadButton final : public SettingValue { +public: + virtual ~SettingValue_GamepadButton(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + std::string wids[2]; + ConfigGamepadButton* value; +}; + +class SettingValue_Language final : public SettingValueWID { +public: + virtual ~SettingValue_Language(void) = default; + virtual void layout(void) const override; +}; + +static std::string str_checkbox_false; +static std::string str_checkbox_true; + +static std::string str_tab_general; +static std::string str_tab_input; +static std::string str_tab_video; +static std::string str_tab_sound; + +static std::string str_input_keyboard; +static std::string str_input_gamepad; +static std::string str_input_mouse; + +static std::string str_keyboard_movement; +static std::string str_keyboard_gameplay; +static std::string str_keyboard_misc; + +static std::string str_gamepad_movement; +static std::string str_gamepad_gameplay; +static std::string str_gamepad_misc; + +static std::string str_gamepad_axis_prefix; +static std::string str_gamepad_button_prefix; +static std::string str_gamepad_checkbox_tooltip; + +static std::string str_video_gui; + +static std::string str_sound_levels; + +static std::vector<SettingValue*> values_all; +static std::vector<SettingValue*> values[NUM_LOCATIONS]; + +void SettingValue::layout_tooltip(void) const +{ + if(has_tooltip) { + ImGui::SameLine(); + ImGui::TextDisabled("[?]"); + + if(ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 16.0f); + ImGui::TextUnformatted(tooltip.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + } +} + +void SettingValue::layout_label(void) const +{ + ImGui::SameLine(); + ImGui::TextUnformatted(title.c_str()); +} + +void SettingValue_CheckBox::refresh_wids(void) +{ + wids[0] = std::format("{}###{}", str_checkbox_false, static_cast<void*>(value)); + wids[1] = std::format("{}###{}", str_checkbox_true, static_cast<void*>(value)); +} + +void SettingValue_CheckBox::layout(void) const +{ + const auto& wid = value->get_value() ? wids[1] : wids[0]; + + if(ImGui::Button(wid.c_str(), ImVec2(ImGui::CalcItemWidth(), 0.0f))) { + value->set_value(!value->get_value()); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_InputInt::layout(void) const +{ + auto current_value = value->get_value(); + + if(ImGui::InputInt(wid.c_str(), ¤t_value)) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_InputFloat::layout(void) const +{ + auto current_value = value->get_value(); + + if(ImGui::InputFloat(wid.c_str(), ¤t_value, 0.0f, 0.0f, format.c_str())) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_InputUnsigned::layout(void) const +{ + auto current_value = static_cast<std::uint32_t>(value->get_value()); + + if(ImGui::InputScalar(wid.c_str(), ImGuiDataType_U32, ¤t_value)) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_InputString::layout(void) const +{ + ImGuiInputTextFlags flags; + std::string current_value = value->get(); + + if(allow_whitespace) { + flags = ImGuiInputTextFlags_AllowTabInput; + } else { + flags = 0; + } + + if(ImGui::InputText(wid.c_str(), ¤t_value, flags)) { + value->set(current_value.c_str()); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_SliderInt::layout(void) const +{ + auto current_value = value->get_value(); + + if(ImGui::SliderInt(wid.c_str(), ¤t_value, value->get_min_value(), value->get_max_value())) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_SliderFloat::layout(void) const +{ + auto current_value = value->get_value(); + + if(ImGui::SliderFloat(wid.c_str(), ¤t_value, value->get_min_value(), value->get_max_value(), format.c_str())) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_SliderUnsigned::layout(void) const +{ + auto current_value = static_cast<std::uint32_t>(value->get_value()); + auto min_value = static_cast<std::uint32_t>(value->get_min_value()); + auto max_value = static_cast<std::uint32_t>(value->get_max_value()); + + if(ImGui::SliderScalar(wid.c_str(), ImGuiDataType_U32, ¤t_value, &min_value, &max_value)) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_StepperInt::layout(void) const +{ + auto current_value = value->get_value(); + auto min_value = value->get_min_value(); + auto max_value = value->get_max_value(); + + auto current_wid = current_value - min_value; + + if(ImGui::Button(wids[current_wid].c_str(), ImVec2(ImGui::CalcItemWidth(), 0.0f))) { + current_value += 1; + } + + if(current_value > max_value) { + value->set_value(min_value); + } else { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_StepperInt::refresh_wids(void) +{ + for(std::size_t i = 0; i < wids.size(); ++i) { + auto key = std::format("settings.value.{}.{}", name, i); + wids[i] = std::format("{}###{}", language::resolve(key.c_str()), static_cast<const void*>(value)); + } +} + +void SettingValue_StepperUnsigned::layout(void) const +{ + auto current_value = value->get_value(); + auto min_value = value->get_min_value(); + auto max_value = value->get_max_value(); + + auto current_wid = current_value - min_value; + + if(ImGui::Button(wids[current_wid].c_str(), ImVec2(ImGui::CalcItemWidth(), 0.0f))) { + current_value += 1U; + } + + if(current_value > max_value) { + value->set_value(min_value); + } else { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_StepperUnsigned::refresh_wids(void) +{ + for(std::size_t i = 0; i < wids.size(); ++i) { + auto key = std::format("settings.value.{}.{}", name, i); + wids[i] = std::format("{}###{}", language::resolve(key.c_str()), static_cast<const void*>(value)); + } +} + +void SettingValue_KeyBind::layout(void) const +{ + const auto is_active = ((globals::gui_keybind_ptr == value) && !globals::gui_gamepad_axis_ptr && !globals::gui_gamepad_button_ptr); + const auto& wid = is_active ? wids[0] : wids[1]; + + if(ImGui::Button(wid.c_str(), ImVec2(ImGui::CalcItemWidth(), 0.0f))) { + auto& io = ImGui::GetIO(); + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard; + globals::gui_keybind_ptr = value; + } + + layout_label(); +} + +void SettingValue_KeyBind::refresh_wids(void) +{ + wids[0] = std::format("...###{}", static_cast<const void*>(value)); + wids[1] = std::format("{}###{}", value->get(), static_cast<const void*>(value)); +} + +void SettingValue_GamepadAxis::layout(void) const +{ + const auto is_active = ((globals::gui_gamepad_axis_ptr == value) && !globals::gui_keybind_ptr && !globals::gui_gamepad_button_ptr); + const auto& wid = is_active ? wids[0] : wids[1]; + auto is_inverted = value->is_inverted(); + + if(ImGui::Button(wid.c_str(), ImVec2(ImGui::CalcItemWidth() - ImGui::GetFrameHeight() - ImGui::GetStyle().ItemSpacing.x, 0.0f))) { + auto& io = ImGui::GetIO(); + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard; + globals::gui_gamepad_axis_ptr = value; + } + + ImGui::SameLine(); + + if(ImGui::Checkbox(wid_checkbox.c_str(), &is_inverted)) { + value->set_inverted(is_inverted); + } + + if(ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 16.0f); + ImGui::TextUnformatted(str_gamepad_checkbox_tooltip.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + layout_label(); +} + +void SettingValue_GamepadAxis::refresh_wids(void) +{ + wids[0] = std::format("...###{}", static_cast<const void*>(value)); + wids[1] = std::format("{}###{}", value->get_name(), static_cast<const void*>(value)); + wid_checkbox = std::format("###CHECKBOX_{}", static_cast<const void*>(value)); +} + +void SettingValue_GamepadButton::layout(void) const +{ + const auto is_active = ((globals::gui_gamepad_button_ptr == value) && !globals::gui_keybind_ptr && !globals::gui_gamepad_axis_ptr); + const auto& wid = is_active ? wids[0] : wids[1]; + + if(ImGui::Button(wid.c_str(), ImVec2(ImGui::CalcItemWidth(), 0.0f))) { + auto& io = ImGui::GetIO(); + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard; + globals::gui_gamepad_button_ptr = value; + } + + layout_label(); +} + +void SettingValue_GamepadButton::refresh_wids(void) +{ + wids[0] = std::format("...###{}", static_cast<const void*>(value)); + wids[1] = std::format("{}###{}", value->get(), static_cast<const void*>(value)); +} + +void SettingValue_Language::layout(void) const +{ + auto current_language = language::get_current(); + + if(ImGui::BeginCombo(wid.c_str(), current_language->endonym.c_str())) { + for(auto it = language::cbegin(); it != language::cend(); ++it) { + if(ImGui::Selectable(it->display.c_str(), it == current_language)) { + language::set(it); + continue; + } + } + + ImGui::EndCombo(); + } + + layout_label(); + layout_tooltip(); +} + +static void refresh_input_wids(void) +{ + for(SettingValue* value : values_all) { + if(value->type == setting_type::KEYBIND) { + auto keybind = static_cast<SettingValue_KeyBind*>(value); + keybind->refresh_wids(); + continue; + } + + if(value->type == setting_type::GAMEPAD_AXIS) { + auto gamepad_axis = static_cast<SettingValue_GamepadAxis*>(value); + gamepad_axis->refresh_wids(); + continue; + } + + if(value->type == setting_type::GAMEPAD_BUTTON) { + auto gamepad_button = static_cast<SettingValue_GamepadButton*>(value); + gamepad_button->refresh_wids(); + } + } +} + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if((event.action == GLFW_PRESS) && (event.key != DEBUG_KEY)) { + if(globals::gui_keybind_ptr || globals::gui_gamepad_axis_ptr || globals::gui_gamepad_button_ptr) { + if(event.key == GLFW_KEY_ESCAPE) { + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + globals::gui_keybind_ptr = nullptr; + globals::gui_gamepad_axis_ptr = nullptr; + globals::gui_gamepad_button_ptr = nullptr; + + return; + } + + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + globals::gui_keybind_ptr->set_key(event.key); + globals::gui_keybind_ptr = nullptr; + + refresh_input_wids(); + + return; + } + + if((event.key == GLFW_KEY_ESCAPE) && (globals::gui_screen == GUI_SETTINGS)) { + globals::gui_screen = GUI_MAIN_MENU; + return; + } + } +} + +static void on_gamepad_axis(const GamepadAxisEvent& event) +{ + if(globals::gui_gamepad_axis_ptr) { + auto& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + globals::gui_gamepad_axis_ptr->set_axis(event.axis); + globals::gui_gamepad_axis_ptr = nullptr; + + refresh_input_wids(); + + return; + } +} + +static void on_gamepad_button(const GamepadButtonEvent& event) +{ + if(globals::gui_gamepad_button_ptr) { + auto& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + globals::gui_gamepad_button_ptr->set_button(event.button); + globals::gui_gamepad_button_ptr = nullptr; + + refresh_input_wids(); + + return; + } +} + +static void on_language_set(const LanguageSetEvent& event) +{ + str_checkbox_false = language::resolve("settings.checkbox.false"); + str_checkbox_true = language::resolve("settings.checkbox.true"); + + str_tab_general = language::resolve("settings.tab.general"); + str_tab_input = language::resolve("settings.tab.input"); + str_tab_video = language::resolve("settings.tab.video"); + str_tab_sound = language::resolve("settings.tab.sound"); + + str_input_keyboard = language::resolve("settings.input.keyboard"); + str_input_gamepad = language::resolve("settings.input.gamepad"); + str_input_mouse = language::resolve("settings.input.mouse"); + + str_keyboard_movement = language::resolve("settings.keyboard.movement"); + str_keyboard_gameplay = language::resolve("settings.keyboard.gameplay"); + str_keyboard_misc = language::resolve("settings.keyboard.misc"); + + str_gamepad_movement = language::resolve("settings.gamepad.movement"); + str_gamepad_gameplay = language::resolve("settings.gamepad.gameplay"); + str_gamepad_misc = language::resolve("settings.gamepad.misc"); + + str_gamepad_axis_prefix = language::resolve("settings.gamepad.axis"); + str_gamepad_button_prefix = language::resolve("settings.gamepad.button"); + str_gamepad_checkbox_tooltip = language::resolve("settings.gamepad.checkbox_tooltip"); + + str_video_gui = language::resolve("settings.video.gui"); + + str_sound_levels = language::resolve("settings.sound.levels"); + + for(SettingValue* value : values_all) { + if(value->type == setting_type::CHECKBOX) { + auto checkbox = static_cast<SettingValue_CheckBox*>(value); + checkbox->refresh_wids(); + } + + if(value->type == setting_type::STEPPER_INT) { + auto stepper = static_cast<SettingValue_StepperInt*>(value); + stepper->refresh_wids(); + } + + if(value->type == setting_type::STEPPER_UINT) { + auto stepper = static_cast<SettingValue_StepperUnsigned*>(value); + stepper->refresh_wids(); + } + + value->title = language::resolve(std::format("settings.value.{}", value->name).c_str()); + + if(value->has_tooltip) { + value->tooltip = language::resolve(std::format("settings.tooltip.{}", value->name).c_str()); + } + } +} + +static void layout_values(settings_location location) +{ + ImGui::PushItemWidth(ImGui::CalcItemWidth() * 0.70f); + + for(const SettingValue* value : values[static_cast<unsigned int>(location)]) { + value->layout(); + } + + ImGui::PopItemWidth(); +} + +static void layout_general(void) +{ + if(ImGui::BeginChild("###settings.general.child")) { + layout_values(settings_location::GENERAL); + } + + ImGui::EndChild(); +} + +static void layout_input_keyboard(void) +{ + if(ImGui::BeginChild("###settings.input.keyboard.child")) { + ImGui::SeparatorText(str_keyboard_movement.c_str()); + layout_values(settings_location::KEYBOARD_MOVEMENT); + ImGui::SeparatorText(str_keyboard_gameplay.c_str()); + layout_values(settings_location::KEYBOARD_GAMEPLAY); + ImGui::SeparatorText(str_keyboard_misc.c_str()); + layout_values(settings_location::KEYBOARD_MISC); + } + + ImGui::EndChild(); +} + +static void layout_input_gamepad(void) +{ + if(ImGui::BeginChild("###settings.input.gamepad.child")) { + layout_values(settings_location::GAMEPAD); + ImGui::SeparatorText(str_gamepad_movement.c_str()); + layout_values(settings_location::GAMEPAD_MOVEMENT); + ImGui::SeparatorText(str_gamepad_gameplay.c_str()); + layout_values(settings_location::GAMEPAD_GAMEPLAY); + ImGui::SeparatorText(str_gamepad_misc.c_str()); + layout_values(settings_location::GAMEPAD_MISC); + } + + ImGui::EndChild(); +} + +static void layout_input_mouse(void) +{ + if(ImGui::BeginChild("###settings.input.mouse.child")) { + layout_values(settings_location::MOUSE); + } + + ImGui::EndChild(); +} + +static void layout_input(void) +{ + if(ImGui::BeginTabBar("###settings.input.tabs", ImGuiTabBarFlags_FittingPolicyResizeDown)) { + if(ImGui::BeginTabItem(str_input_keyboard.c_str())) { + layout_input_keyboard(); + ImGui::EndTabItem(); + } + + if(gamepad::available) { + if(ImGui::BeginTabItem(str_input_gamepad.c_str())) { + globals::gui_keybind_ptr = nullptr; + layout_input_gamepad(); + ImGui::EndTabItem(); + } + } + + if(ImGui::BeginTabItem(str_input_mouse.c_str())) { + globals::gui_keybind_ptr = nullptr; + layout_input_mouse(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } +} + +static void layout_video(void) +{ + if(ImGui::BeginChild("###settings.video.child")) { + layout_values(settings_location::VIDEO); + ImGui::SeparatorText(str_video_gui.c_str()); + layout_values(settings_location::VIDEO_GUI); + } + + ImGui::EndChild(); +} + +static void layout_sound(void) +{ + if(ImGui::BeginChild("###settings.sound.child")) { + layout_values(settings_location::SOUND); + ImGui::SeparatorText(str_sound_levels.c_str()); + layout_values(settings_location::SOUND_LEVELS); + } + + ImGui::EndChild(); +} + +void settings::init(void) +{ + globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<GamepadAxisEvent>().connect<&on_gamepad_axis>(); + globals::dispatcher.sink<GamepadButtonEvent>().connect<&on_gamepad_button>(); + globals::dispatcher.sink<LanguageSetEvent>().connect<&on_language_set>(); +} + +void settings::init_late(void) +{ + for(std::size_t i = 0; i < NUM_LOCATIONS; ++i) { + std::sort(values[i].begin(), values[i].end(), [](const SettingValue* a, const SettingValue* b) { + return a->priority < b->priority; + }); + } + + refresh_input_wids(); +} + +void settings::deinit(void) +{ + for(const SettingValue* value : values_all) + delete value; + for(std::size_t i = 0; i < NUM_LOCATIONS; values[i++].clear()) + ; + values_all.clear(); +} + +void settings::layout(void) +{ + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImVec2 window_start = ImVec2(viewport->Size.x * 0.05f, viewport->Size.y * 0.05f); + const ImVec2 window_size = ImVec2(viewport->Size.x * 0.90f, viewport->Size.y * 0.90f); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###settings", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f * globals::gui_scale, 3.0f * globals::gui_scale)); + + if(ImGui::BeginTabBar("###settings.tabs", ImGuiTabBarFlags_FittingPolicyResizeDown)) { + if(ImGui::TabItemButton("<<")) { + globals::gui_screen = GUI_MAIN_MENU; + globals::gui_keybind_ptr = nullptr; + } + + if(ImGui::BeginTabItem(str_tab_general.c_str())) { + globals::gui_keybind_ptr = nullptr; + layout_general(); + ImGui::EndTabItem(); + } + + if(ImGui::BeginTabItem(str_tab_input.c_str())) { + layout_input(); + ImGui::EndTabItem(); + } + + if(ImGui::BeginTabItem(str_tab_video.c_str())) { + globals::gui_keybind_ptr = nullptr; + layout_video(); + ImGui::EndTabItem(); + } + + if(globals::sound_ctx && globals::sound_dev) { + if(ImGui::BeginTabItem(str_tab_sound.c_str())) { + globals::gui_keybind_ptr = nullptr; + layout_sound(); + ImGui::EndTabItem(); + } + } + + ImGui::EndTabBar(); + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} + +void settings::add_checkbox(int priority, ConfigBoolean& value, settings_location location, const char* name, bool tooltip) +{ + auto setting_value = new SettingValue_CheckBox; + setting_value->type = setting_type::CHECKBOX; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_input(int priority, ConfigInt& value, settings_location location, const char* name, bool tooltip) +{ + auto setting_value = new SettingValue_InputInt; + setting_value->type = setting_type::INPUT_INT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_input(int priority, ConfigFloat& value, settings_location location, const char* name, bool tooltip, const char* format) +{ + auto setting_value = new SettingValue_InputFloat; + setting_value->type = setting_type::INPUT_FLOAT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_input(int priority, ConfigUnsigned& value, settings_location location, const char* name, bool tooltip) +{ + auto setting_value = new SettingValue_InputUnsigned; + setting_value->type = setting_type::INPUT_UINT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_input( + int priority, ConfigString& value, settings_location location, const char* name, bool tooltip, bool allow_whitespace) +{ + auto setting_value = new SettingValue_InputString; + setting_value->type = setting_type::INPUT_STRING; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->allow_whitespace = allow_whitespace; + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_slider(int priority, ConfigInt& value, settings_location location, const char* name, bool tooltip) +{ + auto setting_value = new SettingValue_SliderInt; + setting_value->type = setting_type::SLIDER_INT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_slider(int priority, ConfigFloat& value, settings_location location, const char* name, bool tooltip, const char* format) +{ + auto setting_value = new SettingValue_SliderFloat; + setting_value->type = setting_type::SLIDER_FLOAT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->format = format; + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_slider(int priority, ConfigUnsigned& value, settings_location location, const char* name, bool tooltip) +{ + auto setting_value = new SettingValue_SliderUnsigned; + setting_value->type = setting_type::SLIDER_UINT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_stepper(int priority, ConfigInt& value, settings_location location, const char* name, bool tooltip) +{ + auto setting_value = new SettingValue_StepperInt; + setting_value->type = setting_type::STEPPER_INT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wids.resize(value.get_max_value() - value.get_min_value() + 1); + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_stepper(int priority, ConfigUnsigned& value, settings_location location, const char* name, bool tooltip) +{ + auto setting_value = new SettingValue_StepperUnsigned; + setting_value->type = setting_type::STEPPER_UINT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wids.resize(value.get_max_value() - value.get_min_value() + 1); + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_keybind(int priority, ConfigKeyBind& value, settings_location location, const char* name) +{ + auto setting_value = new SettingValue_KeyBind; + setting_value->type = setting_type::KEYBIND; + setting_value->priority = priority; + setting_value->has_tooltip = false; + setting_value->value = &value; + setting_value->name = name; + + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_gamepad_axis(int priority, ConfigGamepadAxis& value, settings_location location, const char* name) +{ + auto setting_value = new SettingValue_GamepadAxis; + setting_value->type = setting_type::GAMEPAD_AXIS; + setting_value->priority = priority; + setting_value->has_tooltip = false; + setting_value->value = &value; + setting_value->name = name; + + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_gamepad_button(int priority, ConfigGamepadButton& value, settings_location location, const char* name) +{ + auto setting_value = new SettingValue_GamepadButton; + setting_value->type = setting_type::GAMEPAD_BUTTON; + setting_value->priority = priority; + setting_value->has_tooltip = false; + setting_value->value = &value; + setting_value->name = name; + + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_language_select(int priority, settings_location location, const char* name) +{ + auto setting_value = new SettingValue_Language; + setting_value->type = setting_type::LANGUAGE_SELECT; + setting_value->priority = priority; + setting_value->has_tooltip = false; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} diff --git a/src/game/client/settings.hh b/src/game/client/settings.hh new file mode 100644 index 0000000..4704a58 --- /dev/null +++ b/src/game/client/settings.hh @@ -0,0 +1,83 @@ +#ifndef CLIENT_SETTINGS_HH +#define CLIENT_SETTINGS_HH 1 +#pragma once + +class ConfigBoolean; +class ConfigString; + +class ConfigInt; +class ConfigFloat; +class ConfigUnsigned; + +class ConfigKeyBind; +class ConfigGamepadAxis; +class ConfigGamepadButton; + +enum class settings_location : unsigned int { + GENERAL = 0x0000U, + KEYBOARD_MOVEMENT = 0x0001U, + KEYBOARD_GAMEPLAY = 0x0002U, + KEYBOARD_MISC = 0x0003U, + GAMEPAD = 0x0004U, + GAMEPAD_MOVEMENT = 0x0005U, + GAMEPAD_GAMEPLAY = 0x0006U, + GAMEPAD_MISC = 0x0007U, + MOUSE = 0x0008U, + VIDEO = 0x0009U, + VIDEO_GUI = 0x000AU, + SOUND = 0x000BU, + SOUND_LEVELS = 0x000CU, + COUNT = 0x000DU, +}; + +namespace settings +{ +void init(void); +void init_late(void); +void deinit(void); +void layout(void); +} // namespace settings + +namespace settings +{ +void add_checkbox(int priority, ConfigBoolean& value, settings_location location, const char* name, bool tooltip); +} // namespace settings + +namespace settings +{ +void add_input(int priority, ConfigInt& value, settings_location location, const char* name, bool tooltip); +void add_input(int priority, ConfigFloat& value, settings_location location, const char* name, bool tooltip, const char* format = "%.3f"); +void add_input(int priority, ConfigUnsigned& value, settings_location location, const char* name, bool tooltip); +void add_input(int priority, ConfigString& value, settings_location location, const char* name, bool tooltip, bool allow_whitespace); +} // namespace settings + +namespace settings +{ +void add_slider(int priority, ConfigInt& value, settings_location location, const char* name, bool tooltip); +void add_slider(int priority, ConfigFloat& value, settings_location location, const char* name, bool tooltip, const char* format = "%.3f"); +void add_slider(int priority, ConfigUnsigned& value, settings_location location, const char* name, bool tooltip); +} // namespace settings + +namespace settings +{ +void add_stepper(int priority, ConfigInt& value, settings_location location, const char* name, bool tooltip); +void add_stepper(int priority, ConfigUnsigned& value, settings_location location, const char* name, bool tooltip); +} // namespace settings + +namespace settings +{ +void add_keybind(int priority, ConfigKeyBind& value, settings_location location, const char* name); +} // namespace settings + +namespace settings +{ +void add_gamepad_axis(int priority, ConfigGamepadAxis& value, settings_location location, const char* name); +void add_gamepad_button(int priority, ConfigGamepadButton& value, settings_location location, const char* name); +} // namespace settings + +namespace settings +{ +void add_language_select(int priority, settings_location location, const char* name); +} // namespace settings + +#endif /* CLIENT_SETTINGS_HH */ diff --git a/src/game/client/skybox.cc b/src/game/client/skybox.cc new file mode 100644 index 0000000..8578f0e --- /dev/null +++ b/src/game/client/skybox.cc @@ -0,0 +1,11 @@ +#include "client/pch.hh" + +#include "client/skybox.hh" + +glm::fvec3 skybox::fog_color; + +void skybox::init(void) +{ + // https://convertingcolors.com/hex-color-B1F3FF.html + skybox::fog_color = glm::fvec3(0.690f, 0.950f, 1.000f); +} diff --git a/src/game/client/skybox.hh b/src/game/client/skybox.hh new file mode 100644 index 0000000..a3853f0 --- /dev/null +++ b/src/game/client/skybox.hh @@ -0,0 +1,15 @@ +#ifndef CLIENT_SKYBOX_HH +#define CLIENT_SKYBOX_HH 1 +#pragma once + +namespace skybox +{ +extern glm::fvec3 fog_color; +} // namespace skybox + +namespace skybox +{ +void init(void); +} // namespace skybox + +#endif /* CLIENT_SKYBOX_HH */ diff --git a/src/game/client/sound.cc b/src/game/client/sound.cc new file mode 100644 index 0000000..28e0143 --- /dev/null +++ b/src/game/client/sound.cc @@ -0,0 +1,199 @@ +#include "client/pch.hh" + +#include "client/sound.hh" + +#include "core/config.hh" +#include "core/constexpr.hh" +#include "core/resource.hh" + +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/protocol.hh" + +#include "client/camera.hh" +#include "client/const.hh" +#include "client/globals.hh" +#include "client/session.hh" +#include "client/settings.hh" +#include "client/sound_effect.hh" +#include "client/sound_emitter.hh" + +ConfigFloat sound::volume_master(100.0f, 0.0f, 100.0f); +ConfigFloat sound::volume_effects(100.0f, 0.0f, 100.0f); +ConfigFloat sound::volume_music(100.0f, 0.0f, 100.0f); +ConfigFloat sound::volume_ui(100.0f, 0.0f, 100.0f); + +static ALuint generic_source; +static ALuint player_source; +static ALuint ui_source; + +static resource_ptr<SoundEffect> sfx_generic; +static resource_ptr<SoundEffect> sfx_player; +static resource_ptr<SoundEffect> sfx_ui; + +void sound::init_config(void) +{ + globals::client_config.add_value("sound.volume_master", sound::volume_master); + globals::client_config.add_value("sound.volume_effects", sound::volume_effects); + globals::client_config.add_value("sound.volume_music", sound::volume_music); + globals::client_config.add_value("sound.volume_ui", sound::volume_ui); + + settings::add_slider(1, sound::volume_master, settings_location::SOUND, "sound.volume_master", false, "%.0f%%"); + + settings::add_slider(0, sound::volume_effects, settings_location::SOUND_LEVELS, "sound.volume_effects", false, "%.0f%%"); + settings::add_slider(1, sound::volume_music, settings_location::SOUND_LEVELS, "sound.volume_music", false, "%.0f%%"); + settings::add_slider(2, sound::volume_ui, settings_location::SOUND_LEVELS, "sound.volume_ui", false, "%.0f%%"); +} + +void sound::init(void) +{ + alGenSources(1, &generic_source); + alSourcei(generic_source, AL_SOURCE_RELATIVE, AL_TRUE); + alSource3f(generic_source, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSource3f(generic_source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + + alGenSources(1, &player_source); + alSourcei(player_source, AL_SOURCE_RELATIVE, AL_TRUE); + alSource3f(player_source, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSource3f(player_source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + + alGenSources(1, &ui_source); + alSourcei(ui_source, AL_SOURCE_RELATIVE, AL_TRUE); + alSource3f(ui_source, AL_POSITION, 0.0f, 0.0f, 0.0f); + alSource3f(ui_source, AL_VELOCITY, 0.0f, 0.0f, 0.0f); + + sfx_generic = nullptr; + sfx_player = nullptr; + sfx_ui = nullptr; +} + +void sound::init_late(void) +{ +} + +void sound::deinit(void) +{ + sfx_ui = nullptr; + sfx_player = nullptr; + sfx_generic = nullptr; + + alDeleteBuffers(1, &ui_source); + alDeleteSources(1, &generic_source); + alDeleteSources(1, &player_source); +} + +void sound::update(void) +{ + auto effects_gain = vx::clamp(0.01f * sound::volume_effects.get_value(), 0.0f, 1.0f); + alSourcef(generic_source, AL_GAIN, effects_gain); + alSourcef(player_source, AL_GAIN, effects_gain); + + auto ui_gain = vx::clamp(0.01f * sound::volume_ui.get_value(), 0.0f, 1.0f); + alSourcef(ui_source, AL_GAIN, ui_gain); +} + +void sound::play_generic(const char* sound, bool looping, float pitch) +{ + if(sound) { + sound::play_generic(resource::load<SoundEffect>(sound), looping, pitch); + } else { + sound::play_generic(static_cast<const char*>(nullptr), looping, pitch); + } +} + +void sound::play_entity(entt::entity entity, const char* sound, bool looping, float pitch) +{ + if(sound) { + sound::play_entity(entity, resource::load<SoundEffect>(sound), looping, pitch); + } else { + sound::play_entity(entity, static_cast<const char*>(nullptr), looping, pitch); + } +} + +void sound::play_player(const char* sound, bool looping, float pitch) +{ + if(sound) { + sound::play_player(resource::load<SoundEffect>(sound), looping, pitch); + } else { + sound::play_player(static_cast<const char*>(nullptr), looping, pitch); + } +} + +void sound::play_ui(const char* sound, bool looping, float pitch) +{ + if(sound) { + sound::play_ui(resource::load<SoundEffect>(sound), looping, pitch); + } else { + sound::play_ui(static_cast<const char*>(nullptr), looping, pitch); + } +} + +void sound::play_generic(resource_ptr<SoundEffect> sound, bool looping, float pitch) +{ + alSourceRewind(generic_source); + + sfx_generic = sound; + + if(sfx_generic) { + alSourcei(generic_source, AL_BUFFER, sfx_generic->buffer); + alSourcei(generic_source, AL_LOOPING, looping); + alSourcef(generic_source, AL_PITCH, vx::clamp(pitch, MIN_PITCH, MAX_PITCH)); + alSourcePlay(generic_source); + } +} + +void sound::play_entity(entt::entity entity, resource_ptr<SoundEffect> sound, bool looping, float pitch) +{ + if(globals::dimension && globals::dimension->entities.valid(entity)) { + if(auto emitter = globals::dimension->entities.try_get<SoundEmitterComponent>(entity)) { + alSourceRewind(emitter->source); + + emitter->sound = sound; + + if(emitter->sound) { + alSourcei(emitter->source, AL_BUFFER, emitter->sound->buffer); + alSourcei(emitter->source, AL_LOOPING, looping); + alSourcef(emitter->source, AL_PITCH, vx::clamp(pitch, MIN_PITCH, MAX_PITCH)); + alSourcePlay(emitter->source); + } + } + } +} + +void sound::play_player(resource_ptr<SoundEffect> sound, bool looping, float pitch) +{ + if(sound && session::is_ingame()) { + protocol::EntitySound packet; + packet.entity = globals::player; + packet.sound = sound->name; + packet.looping = looping; + packet.pitch = pitch; + + protocol::send(session::peer, protocol::encode(packet)); + } + + alSourceRewind(player_source); + + sfx_player = sound; + + if(sfx_player) { + alSourcei(player_source, AL_BUFFER, sfx_player->buffer); + alSourcei(player_source, AL_LOOPING, looping); + alSourcef(player_source, AL_PITCH, vx::clamp(pitch, MIN_PITCH, MAX_PITCH)); + alSourcePlay(player_source); + } +} + +void sound::play_ui(resource_ptr<SoundEffect> sound, bool looping, float pitch) +{ + alSourceRewind(ui_source); + + sfx_ui = sound; + + if(sfx_ui) { + alSourcei(ui_source, AL_BUFFER, sfx_ui->buffer); + alSourcei(ui_source, AL_LOOPING, looping); + alSourcef(ui_source, AL_PITCH, vx::clamp(pitch, MIN_PITCH, MAX_PITCH)); + alSourcePlay(ui_source); + } +}
\ No newline at end of file diff --git a/src/game/client/sound.hh b/src/game/client/sound.hh new file mode 100644 index 0000000..2701ae0 --- /dev/null +++ b/src/game/client/sound.hh @@ -0,0 +1,43 @@ +#ifndef CLIENT_SOUND_HH +#define CLIENT_SOUND_HH 1 +#pragma once + +#include "core/resource.hh" + +class ConfigFloat; +struct SoundEffect; + +namespace sound +{ +extern ConfigFloat volume_master; +extern ConfigFloat volume_effects; +extern ConfigFloat volume_music; +extern ConfigFloat volume_ui; +} // namespace sound + +namespace sound +{ +void init_config(void); +void init(void); +void init_late(void); +void deinit(void); +void update(void); +} // namespace sound + +namespace sound +{ +void play_generic(const char* sound, bool looping, float pitch); +void play_entity(entt::entity entity, const char* sound, bool looping, float pitch); +void play_player(const char* sound, bool looping, float pitch); +void play_ui(const char* sound, bool looping, float pitch); +} // namespace sound + +namespace sound +{ +void play_generic(resource_ptr<SoundEffect> sound, bool looping, float pitch); +void play_entity(entt::entity entity, resource_ptr<SoundEffect> sound, bool looping, float pitch); +void play_player(resource_ptr<SoundEffect> sound, bool looping, float pitch); +void play_ui(resource_ptr<SoundEffect> sound, bool looping, float pitch); +} // namespace sound + +#endif /* CLIENT_SOUND_HH */ diff --git a/src/game/client/sound_effect.cc b/src/game/client/sound_effect.cc new file mode 100644 index 0000000..182f49d --- /dev/null +++ b/src/game/client/sound_effect.cc @@ -0,0 +1,115 @@ +#include "client/pch.hh" + +#include "client/sound_effect.hh" + +#include "core/resource.hh" + +#include "client/globals.hh" + +static emhash8::HashMap<std::string, resource_ptr<SoundEffect>> resource_map; + +static std::size_t drwav_read_physfs(void* file, void* output, std::size_t count) +{ + return static_cast<std::size_t>(PHYSFS_readBytes(reinterpret_cast<PHYSFS_File*>(file), output, count)); +} + +static drwav_bool32 drwav_seek_physfs(void* file, int offset, drwav_seek_origin origin) +{ + if(origin == drwav_seek_origin_current) { + return PHYSFS_seek(reinterpret_cast<PHYSFS_File*>(file), PHYSFS_tell(reinterpret_cast<PHYSFS_File*>(file)) + offset); + } else { + return PHYSFS_seek(reinterpret_cast<PHYSFS_File*>(file), offset); + } +} + +template<> +resource_ptr<SoundEffect> resource::load<SoundEffect>(const char* name, unsigned int flags) +{ + auto it = resource_map.find(name); + + if(it != resource_map.cend()) { + // Return an existing resource + return it->second; + } + + if(globals::sound_ctx == nullptr) { + // Sound is disabled + return nullptr; + } + + auto file = PHYSFS_openRead(name); + + if(file == nullptr) { + spdlog::warn("resource: {} [SoundEffect]: {}", name, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + return nullptr; + } + + drwav wav_info; + + if(!drwav_init(&wav_info, &drwav_read_physfs, &drwav_seek_physfs, file, nullptr)) { + spdlog::warn("resource: {} [SoundEffect]: drwav_init failed", name); + PHYSFS_close(file); + return nullptr; + } + + if(wav_info.channels != 1) { + spdlog::warn("resource: {} [SoundEffect]: only mono sound files are allowed", name); + drwav_uninit(&wav_info); + PHYSFS_close(file); + return nullptr; + } + + auto samples = new ALshort[wav_info.totalPCMFrameCount]; + auto count = drwav_read_pcm_frames_s16(&wav_info, wav_info.totalPCMFrameCount, reinterpret_cast<drwav_int16*>(samples)); + auto sample_rate = static_cast<ALsizei>(wav_info.sampleRate); + auto length = static_cast<ALsizei>(count * sizeof(ALshort)); + + drwav_uninit(&wav_info); + PHYSFS_close(file); + + auto new_resource = std::make_shared<SoundEffect>(); + new_resource->name = std::string(name); + + alGenBuffers(1, &new_resource->buffer); + alBufferData(new_resource->buffer, AL_FORMAT_MONO16, samples, length, sample_rate); + + delete[] samples; + + return resource_map.insert_or_assign(name, new_resource).first->second; +} + +template<> +void resource::hard_cleanup<SoundEffect>(void) +{ + for(const auto& it : resource_map) { + if(it.second.use_count() > 1L) { + spdlog::warn("resource: zombie resource [SoundEffect] {} [use_count={}]", it.first, it.second.use_count()); + } else { + spdlog::debug("resource: releasing [SoundEffect] {}", it.first); + } + + alDeleteBuffers(1, &it.second->buffer); + } + + resource_map.clear(); +} + +template<> +void resource::soft_cleanup<SoundEffect>(void) +{ + auto iter = resource_map.cbegin(); + + while(iter != resource_map.cend()) { + if(iter->second.use_count() == 1L) { + spdlog::debug("resource: releasing [SoundEffect] {}", iter->first); + + alDeleteBuffers(1, &iter->second->buffer); + + iter = resource_map.erase(iter); + + continue; + } + + iter = std::next(iter); + } +} diff --git a/src/game/client/sound_effect.hh b/src/game/client/sound_effect.hh new file mode 100644 index 0000000..83b5e4d --- /dev/null +++ b/src/game/client/sound_effect.hh @@ -0,0 +1,10 @@ +#ifndef CLIENT_SOUND_EFFECT_HH +#define CLIENT_SOUND_EFFECT_HH 1 +#pragma once + +struct SoundEffect final { + std::string name; + ALuint buffer; +}; + +#endif /* CLIENT_SOUND_EFFECT_HH */ diff --git a/src/game/client/sound_emitter.cc b/src/game/client/sound_emitter.cc new file mode 100644 index 0000000..9b84df2 --- /dev/null +++ b/src/game/client/sound_emitter.cc @@ -0,0 +1,58 @@ +#include "client/pch.hh" + +#include "client/sound_emitter.hh" + +#include "core/config.hh" +#include "core/constexpr.hh" + +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" + +#include "client/camera.hh" +#include "client/globals.hh" +#include "client/sound.hh" + +SoundEmitterComponent::SoundEmitterComponent(void) +{ + alGenSources(1, &source); + sound = nullptr; +} + +SoundEmitterComponent::~SoundEmitterComponent(void) +{ + alSourceStop(source); + alDeleteSources(1, &source); +} + +void SoundEmitterComponent::update(void) +{ + if(globals::dimension) { + const auto view = globals::dimension->entities.view<SoundEmitterComponent>(); + + const auto& pivot = camera::position_chunk; + const auto gain = vx::clamp(sound::volume_effects.get_value() * 0.01f, 0.0f, 1.0f); + + for(const auto [entity, emitter] : view.each()) { + alSourcef(emitter.source, AL_GAIN, gain); + + if(const auto transform = globals::dimension->entities.try_get<TransformComponentIntr>(entity)) { + auto position = coord::to_relative(pivot, transform->chunk, transform->local); + alSource3f(emitter.source, AL_POSITION, position.x, position.y, position.z); + } + + if(const auto velocity = globals::dimension->entities.try_get<VelocityComponent>(entity)) { + alSource3f(emitter.source, AL_VELOCITY, velocity->value.x, velocity->value.y, velocity->value.z); + } + + ALint source_state; + alGetSourcei(emitter.source, AL_SOURCE_STATE, &source_state); + + if(source_state == AL_STOPPED) { + alSourceRewind(emitter.source); + emitter.sound = nullptr; + } + } + } +} diff --git a/src/game/client/sound_emitter.hh b/src/game/client/sound_emitter.hh new file mode 100644 index 0000000..88d99eb --- /dev/null +++ b/src/game/client/sound_emitter.hh @@ -0,0 +1,21 @@ +#ifndef CLIENT_SOUND_EMITTER_HH +#define CLIENT_SOUND_EMITTER_HH 1 +#pragma once + +#include "core/resource.hh" + +struct SoundEffect; + +struct SoundEmitterComponent final { + resource_ptr<SoundEffect> sound; + ALuint source; + +public: + explicit SoundEmitterComponent(void); + virtual ~SoundEmitterComponent(void); + +public: + static void update(void); +}; + +#endif /* CLIENT_SOUND_EMITTER_HH */ diff --git a/src/game/client/splash.cc b/src/game/client/splash.cc new file mode 100644 index 0000000..f9c616b --- /dev/null +++ b/src/game/client/splash.cc @@ -0,0 +1,171 @@ +#include "client/pch.hh" + +#include "client/splash.hh" + +#include "core/cmdline.hh" +#include "core/constexpr.hh" +#include "core/epoch.hh" +#include "core/resource.hh" + +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/language.hh" +#include "client/texture_gui.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +constexpr static int SPLASH_COUNT = 4; +constexpr static std::size_t DELAY_MICROSECONDS = 2000000; +constexpr static const char* SPLASH_PATH = "textures/gui/client_splash.png"; + +static resource_ptr<TextureGUI> texture; +static float texture_aspect; +static float texture_alpha; + +static std::uint64_t end_time; +static std::string current_text; + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + end_time = UINT64_C(0); +} + +static void on_glfw_mouse_button(const GlfwMouseButtonEvent& event) +{ + end_time = UINT64_C(0); +} + +static void on_glfw_scroll(const GlfwScrollEvent& event) +{ + end_time = UINT64_C(0); +} + +void client_splash::init(void) +{ + if(cmdline::contains("nosplash")) { + texture = nullptr; + texture_aspect = 0.0f; + texture_alpha = 0.0f; + return; + } + + std::random_device randev; + std::uniform_int_distribution<int> dist(0, SPLASH_COUNT - 1); + + texture = resource::load<TextureGUI>(SPLASH_PATH, TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + texture_aspect = 0.0f; + texture_alpha = 0.0f; + + if(texture) { + if(texture->size.x > texture->size.y) { + texture_aspect = static_cast<float>(texture->size.x) / static_cast<float>(texture->size.y); + } else { + texture_aspect = static_cast<float>(texture->size.y) / static_cast<float>(texture->size.x); + } + + texture_alpha = 1.0f; + } +} + +void client_splash::init_late(void) +{ + if(!texture) { + // We don't have to waste time + // rendering the missing client_splash texture + return; + } + + end_time = epoch::microseconds() + DELAY_MICROSECONDS; + + globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<GlfwMouseButtonEvent>().connect<&on_glfw_mouse_button>(); + globals::dispatcher.sink<GlfwScrollEvent>().connect<&on_glfw_scroll>(); + + current_text = language::resolve("splash.skip_prompt"); + + while(!glfwWindowShouldClose(globals::window)) { + const std::uint64_t curtime = epoch::microseconds(); + const std::uint64_t remains = end_time - curtime; + + if(curtime >= end_time) { + break; + } + + texture_alpha = vx::smoothstep(0.25f, 0.6f, static_cast<float>(remains) / static_cast<float>(DELAY_MICROSECONDS)); + + client_splash::render(); + } + + globals::dispatcher.sink<GlfwKeyEvent>().disconnect<&on_glfw_key>(); + globals::dispatcher.sink<GlfwMouseButtonEvent>().disconnect<&on_glfw_mouse_button>(); + globals::dispatcher.sink<GlfwScrollEvent>().disconnect<&on_glfw_scroll>(); + + texture = nullptr; + texture_aspect = 0.0f; + texture_alpha = 0.0f; + end_time = UINT64_C(0); +} + +void client_splash::render(void) +{ + if(!texture) { + // We don't have to waste time + // rendering the missing client_splash texture + return; + } + + // The client_splash is rendered outside the main + // render loop, so we have to manually begin + // and render both window and ImGui frames + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + glDisable(GL_DEPTH_TEST); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, globals::width, globals::height); + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + auto viewport = ImGui::GetMainViewport(); + auto window_start = ImVec2(0.0f, 0.0f); + auto window_size = ImVec2(viewport->Size.x, viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###client_splash", nullptr, WINDOW_FLAGS)) { + const float image_width = 0.60f * viewport->Size.x; + const float image_height = image_width / texture_aspect; + const ImVec2 image_size = ImVec2(image_width, image_height); + + const float image_x = 0.5f * (viewport->Size.x - image_width); + const float image_y = 0.5f * (viewport->Size.y - image_height); + const ImVec2 image_pos = ImVec2(image_x, image_y); + + if(!current_text.empty()) { + ImGui::PushFont(globals::font_chat); + ImGui::SetCursorPos(ImVec2(16.0f, 16.0f)); + ImGui::TextDisabled("%s", current_text.c_str()); + ImGui::PopFont(); + } + + const ImVec2 uv_a = ImVec2(0.0f, 0.0f); + const ImVec2 uv_b = ImVec2(1.0f, 1.0f); + const ImVec4 tint = ImVec4(1.0f, 1.0f, 1.0f, texture_alpha); + + ImGui::SetCursorPos(image_pos); + ImGui::ImageWithBg(texture->handle, image_size, uv_a, uv_b, ImVec4(0.0f, 0.0f, 0.0f, 0.0f), tint); + } + + ImGui::End(); + + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + glfwSwapBuffers(globals::window); + + glfwPollEvents(); +} diff --git a/src/game/client/splash.hh b/src/game/client/splash.hh new file mode 100644 index 0000000..2b66e2c --- /dev/null +++ b/src/game/client/splash.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_SPLASH_HH +#define CLIENT_SPLASH_HH 1 +#pragma once + +namespace client_splash +{ +void init(void); +void init_late(void); +void render(void); +} // namespace client_splash + +#endif /* CLIENT_SPLASH_HH */ diff --git a/src/game/client/status_lines.cc b/src/game/client/status_lines.cc new file mode 100644 index 0000000..ea7293c --- /dev/null +++ b/src/game/client/status_lines.cc @@ -0,0 +1,79 @@ +#include "client/pch.hh" + +#include "client/status_lines.hh" + +#include "client/globals.hh" +#include "client/imdraw_ext.hh" + +static float line_offsets[STATUS_COUNT]; +static ImFont* line_fonts[STATUS_COUNT]; + +static ImVec4 line_text_colors[STATUS_COUNT]; +static ImVec4 line_shadow_colors[STATUS_COUNT]; +static std::string line_strings[STATUS_COUNT]; +static std::uint64_t line_spawns[STATUS_COUNT]; +static float line_fadeouts[STATUS_COUNT]; + +void status_lines::init(void) +{ + for(unsigned int i = 0U; i < STATUS_COUNT; ++i) { + line_text_colors[i] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + line_shadow_colors[i] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + line_strings[i] = std::string(); + line_spawns[i] = UINT64_MAX; + line_fadeouts[i] = 0.0f; + } +} + +void status_lines::init_late(void) +{ + line_offsets[STATUS_DEBUG] = 64.0f; + line_offsets[STATUS_HOTBAR] = 40.0f; +} + +void status_lines::layout(void) +{ + line_fonts[STATUS_DEBUG] = globals::font_debug; + line_fonts[STATUS_HOTBAR] = globals::font_chat; + + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetForegroundDrawList(); + + for(unsigned int i = 0U; i < STATUS_COUNT; ++i) { + auto offset = line_offsets[i] * globals::gui_scale; + auto& text = line_strings[i]; + auto* font = line_fonts[i]; + + auto size = font->CalcTextSizeA(font->FontSize, FLT_MAX, 0.0f, text.c_str(), text.c_str() + text.size()); + auto pos = ImVec2(0.5f * (viewport->Size.x - size.x), viewport->Size.y - offset); + + auto spawn = line_spawns[i]; + auto fadeout = line_fadeouts[i]; + auto alpha = std::exp(-1.0f * std::pow(1.0e-6f * static_cast<float>(globals::curtime - spawn) / fadeout, 10.0f)); + + auto& color = line_text_colors[i]; + auto& shadow = line_shadow_colors[i]; + auto color_U32 = ImGui::GetColorU32(ImVec4(color.x, color.y, color.z, color.w * alpha)); + auto shadow_U32 = ImGui::GetColorU32(ImVec4(shadow.x, shadow.y, shadow.z, color.w * alpha)); + + imdraw_ext::text_shadow(text, pos, color_U32, shadow_U32, font, draw_list); + } +} + +void status_lines::set(unsigned int line, const std::string& text, const ImVec4& color, float fadeout) +{ + line_text_colors[line] = ImVec4(color.x, color.y, color.z, color.w); + line_shadow_colors[line] = ImVec4(color.x * 0.1f, color.y * 0.1f, color.z * 0.1f, color.w); + line_strings[line] = std::string(text); + line_spawns[line] = globals::curtime; + line_fadeouts[line] = fadeout; +} + +void status_lines::unset(unsigned int line) +{ + line_text_colors[line] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + line_shadow_colors[line] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + line_strings[line] = std::string(); + line_spawns[line] = UINT64_C(0); + line_fadeouts[line] = 0.0f; +} diff --git a/src/game/client/status_lines.hh b/src/game/client/status_lines.hh new file mode 100644 index 0000000..c252a34 --- /dev/null +++ b/src/game/client/status_lines.hh @@ -0,0 +1,22 @@ +#ifndef CLIENT_STATUS_LINES_HH +#define CLIENT_STATUS_LINES_HH 1 +#pragma once + +constexpr static unsigned int STATUS_DEBUG = 0x0000; // generic debug line +constexpr static unsigned int STATUS_HOTBAR = 0x0001; // hotbar item line +constexpr static unsigned int STATUS_COUNT = 0x0002; + +namespace status_lines +{ +void init(void); +void init_late(void); +void layout(void); +} // namespace status_lines + +namespace status_lines +{ +void set(unsigned int line, const std::string& text, const ImVec4& color, float fadeout); +void unset(unsigned int line); +} // namespace status_lines + +#endif /* CLIENT_STATUS_LINES_HH */ diff --git a/src/game/client/texture_gui.cc b/src/game/client/texture_gui.cc new file mode 100644 index 0000000..265f6c2 --- /dev/null +++ b/src/game/client/texture_gui.cc @@ -0,0 +1,110 @@ +#include "client/pch.hh" + +#include "client/texture_gui.hh" + +#include "core/image.hh" +#include "core/resource.hh" + +static emhash8::HashMap<std::string, resource_ptr<TextureGUI>> resource_map; + +template<> +resource_ptr<TextureGUI> resource::load<TextureGUI>(const char* name, unsigned int flags) +{ + auto it = resource_map.find(name); + + if(it != resource_map.cend()) { + // Return an existing resource + return it->second; + } + + unsigned int image_load_flags = 0U; + + if(flags & TEXTURE_GUI_LOAD_VFLIP) { + image_load_flags |= IMAGE_LOAD_FLIP; + } + + if(flags & TEXTURE_GUI_LOAD_GRAYSCALE) { + image_load_flags |= IMAGE_LOAD_GRAY; + } + + if(auto image = resource::load<Image>(name, image_load_flags)) { + GLuint gl_texture; + + glGenTextures(1, &gl_texture); + glBindTexture(GL_TEXTURE_2D, gl_texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image->size.x, image->size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, image->pixels); + + if(flags & TEXTURE_GUI_LOAD_CLAMP_S) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + } else { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + } + + if(flags & TEXTURE_GUI_LOAD_CLAMP_T) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } else { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + } + + if(flags & TEXTURE_GUI_LOAD_LINEAR_MAG) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + } else { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + + if(flags & TEXTURE_GUI_LOAD_LINEAR_MIN) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + } else { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + } + + auto new_resource = std::make_shared<TextureGUI>(); + new_resource->handle = static_cast<ImTextureID>(gl_texture); + new_resource->size.x = image->size.x; + new_resource->size.y = image->size.y; + + return resource_map.insert_or_assign(name, new_resource).first->second; + } + + return nullptr; +} + +template<> +void resource::hard_cleanup<TextureGUI>(void) +{ + for(const auto& it : resource_map) { + if(it.second.use_count() > 1L) { + spdlog::warn("resource: zombie resource [TextureGUI] {} [use_count={}]", it.first, it.second.use_count()); + } else { + spdlog::debug("resource: releasing [TextureGUI] {}", it.first); + } + + auto gl_texture = static_cast<GLuint>(it.second->handle); + + glDeleteTextures(1, &gl_texture); + } + + resource_map.clear(); +} + +template<> +void resource::soft_cleanup<TextureGUI>(void) +{ + auto iter = resource_map.cbegin(); + + while(iter != resource_map.cend()) { + if(iter->second.use_count() == 1L) { + spdlog::debug("resource: releasing [TextureGUI] {}", iter->first); + + auto gl_texture = static_cast<GLuint>(iter->second->handle); + + glDeleteTextures(1, &gl_texture); + + iter = resource_map.erase(iter); + + continue; + } + + iter = std::next(iter); + } +} diff --git a/src/game/client/texture_gui.hh b/src/game/client/texture_gui.hh new file mode 100644 index 0000000..97a91b4 --- /dev/null +++ b/src/game/client/texture_gui.hh @@ -0,0 +1,17 @@ +#ifndef CLIENT_TEXTURE2D_HH +#define CLIENT_TEXTURE2D_HH 1 +#pragma once + +constexpr static unsigned int TEXTURE_GUI_LOAD_CLAMP_S = 0x0001; +constexpr static unsigned int TEXTURE_GUI_LOAD_CLAMP_T = 0x0002; +constexpr static unsigned int TEXTURE_GUI_LOAD_LINEAR_MAG = 0x0004; +constexpr static unsigned int TEXTURE_GUI_LOAD_LINEAR_MIN = 0x0008; +constexpr static unsigned int TEXTURE_GUI_LOAD_VFLIP = 0x0010; +constexpr static unsigned int TEXTURE_GUI_LOAD_GRAYSCALE = 0x0020; + +struct TextureGUI { + ImTextureID handle; + glm::ivec2 size; +}; + +#endif /* CLIENT_TEXTURE2D_HH */ diff --git a/src/game/client/toggles.cc b/src/game/client/toggles.cc new file mode 100644 index 0000000..f40d6ed --- /dev/null +++ b/src/game/client/toggles.cc @@ -0,0 +1,154 @@ +#include "client/pch.hh" + +#include "client/toggles.hh" + +#include "core/config.hh" + +#include "client/chat.hh" +#include "client/const.hh" +#include "client/gamepad.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/language.hh" + +struct ToggleInfo final { + const char* description; + int glfw_keycode; + bool is_enabled; +}; + +bool toggles::is_sequence_await = false; + +static ToggleInfo toggle_infos[TOGGLE_COUNT]; + +static void print_toggle_state(const ToggleInfo& info) +{ + if(info.description) { + if(info.is_enabled) { + client_chat::print(std::format("[toggles] {} ON", info.description)); + } else { + client_chat::print(std::format("[toggles] {} OFF", info.description)); + } + } +} + +static void toggle_value(ToggleInfo& info, toggle_type type) +{ + if(info.is_enabled) { + info.is_enabled = false; + globals::dispatcher.trigger(ToggleDisabledEvent { type }); + } else { + info.is_enabled = true; + globals::dispatcher.trigger(ToggleEnabledEvent { type }); + } + + print_toggle_state(info); +} + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if(globals::gui_keybind_ptr) { + // The UI keybind subsystem has the authority + // over debug toggles and it hogs the input keys + return; + } + + if(event.key == DEBUG_KEY) { + if(event.action == GLFW_PRESS) { + toggles::is_sequence_await = true; + ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard; + return; + } + + if(event.action == GLFW_RELEASE) { + toggles::is_sequence_await = false; + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + return; + } + } + + if((event.action == GLFW_PRESS) && toggles::is_sequence_await) { + if(event.key == GLFW_KEY_L) { + // This causes the language subsystem + // to re-parse the JSON file essentially + // causing the game to soft-reload language + language::set(language::get_current()); + return; + } + + for(toggle_type i = 0; i < TOGGLE_COUNT; ++i) { + if(event.key == toggle_infos[i].glfw_keycode) { + toggle_value(toggle_infos[i], i); + return; + } + } + } +} + +void toggles::init(void) +{ + toggle_infos[TOGGLE_WIREFRAME].description = "wireframe"; + toggle_infos[TOGGLE_WIREFRAME].glfw_keycode = GLFW_KEY_Z; + toggle_infos[TOGGLE_WIREFRAME].is_enabled = false; + + toggle_infos[TOGGLE_FULLBRIGHT].description = "fullbright"; + toggle_infos[TOGGLE_FULLBRIGHT].glfw_keycode = GLFW_KEY_J; + toggle_infos[TOGGLE_FULLBRIGHT].is_enabled = false; + + toggle_infos[TOGGLE_CHUNK_AABB].description = "chunk Borders"; + toggle_infos[TOGGLE_CHUNK_AABB].glfw_keycode = GLFW_KEY_G; + toggle_infos[TOGGLE_CHUNK_AABB].is_enabled = false; + + toggle_infos[TOGGLE_METRICS_UI].description = nullptr; + toggle_infos[TOGGLE_METRICS_UI].glfw_keycode = GLFW_KEY_V; + toggle_infos[TOGGLE_METRICS_UI].is_enabled = false; + + toggle_infos[TOGGLE_USE_GAMEPAD].description = "gamepad input"; + toggle_infos[TOGGLE_USE_GAMEPAD].glfw_keycode = GLFW_KEY_P; + toggle_infos[TOGGLE_USE_GAMEPAD].is_enabled = false; + + toggle_infos[TOGGLE_PM_FLIGHT].description = "flight mode"; + toggle_infos[TOGGLE_PM_FLIGHT].glfw_keycode = GLFW_KEY_F; + toggle_infos[TOGGLE_PM_FLIGHT].is_enabled = false; + +#ifndef NDEBUG + toggle_infos[TOGGLE_WIREFRAME].is_enabled = true; +#endif + + globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>(); +} + +void toggles::init_late(void) +{ + for(toggle_type i = 0; i < TOGGLE_COUNT; ++i) { + if(toggle_infos[i].is_enabled) { + globals::dispatcher.trigger(ToggleEnabledEvent { i }); + } else { + globals::dispatcher.trigger(ToggleDisabledEvent { i }); + } + } +} + +bool toggles::get(toggle_type type) +{ + if(type < TOGGLE_COUNT) { + return toggle_infos[type].is_enabled; + } else { + return false; + } +} + +void toggles::set(toggle_type type, bool value) +{ + if(type < TOGGLE_COUNT) { + if(value) { + toggle_infos[type].is_enabled = true; + globals::dispatcher.trigger(ToggleEnabledEvent { type }); + } else { + toggle_infos[type].is_enabled = false; + globals::dispatcher.trigger(ToggleDisabledEvent { type }); + } + + print_toggle_state(toggle_infos[type]); + } +} diff --git a/src/game/client/toggles.hh b/src/game/client/toggles.hh new file mode 100644 index 0000000..d892d33 --- /dev/null +++ b/src/game/client/toggles.hh @@ -0,0 +1,39 @@ +#ifndef CLIENT_TOGGLES_HH +#define CLIENT_TOGGLES_HH 1 +#pragma once + +using toggle_type = unsigned int; +constexpr static toggle_type TOGGLE_WIREFRAME = 0x0000U; // Render things in wireframe mode +constexpr static toggle_type TOGGLE_FULLBRIGHT = 0x0001U; // Render things without lighting +constexpr static toggle_type TOGGLE_CHUNK_AABB = 0x0002U; // Render chunk bounding boxes +constexpr static toggle_type TOGGLE_METRICS_UI = 0x0003U; // Render debug metrics overlay +constexpr static toggle_type TOGGLE_USE_GAMEPAD = 0x0004U; // Use gamepad for player movement +constexpr static toggle_type TOGGLE_PM_FLIGHT = 0x0005U; // Enable flight for player movement +constexpr static std::size_t TOGGLE_COUNT = 0x0006U; + +struct ToggleEnabledEvent final { + toggle_type type; +}; + +struct ToggleDisabledEvent final { + toggle_type type; +}; + +namespace toggles +{ +// The value is true whenever the debug +// toggles manager awaits for a sequenced key +// to be pressed. During this no input should +// be processed by any other gameplay system +extern bool is_sequence_await; +} // namespace toggles + +namespace toggles +{ +void init(void); +void init_late(void); +bool get(toggle_type type); +void set(toggle_type type, bool value); +} // namespace toggles + +#endif /* CLIENT_TOGGLES_HH */ diff --git a/src/game/client/vclient.ico b/src/game/client/vclient.ico Binary files differnew file mode 100644 index 0000000..5f3ccd2 --- /dev/null +++ b/src/game/client/vclient.ico diff --git a/src/game/client/vclient.rc b/src/game/client/vclient.rc new file mode 100644 index 0000000..0268ca0 --- /dev/null +++ b/src/game/client/vclient.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "vclient.ico" diff --git a/src/game/client/voxel_anims.cc b/src/game/client/voxel_anims.cc new file mode 100644 index 0000000..4e0f168 --- /dev/null +++ b/src/game/client/voxel_anims.cc @@ -0,0 +1,29 @@ +#include "client/pch.hh" + +#include "client/voxel_anims.hh" + +#include "core/config.hh" +#include "core/constexpr.hh" + +#include "client/globals.hh" + +static ConfigUnsigned base_framerate(16U, 1U, 16U); + +std::uint64_t voxel_anims::nextframe = 0U; +std::uint32_t voxel_anims::frame = 0U; + +void voxel_anims::init(void) +{ + globals::client_config.add_value("voxel_anims.base_framerate", base_framerate); + + voxel_anims::nextframe = 0U; + voxel_anims::frame = 0U; +} + +void voxel_anims::update(void) +{ + if(globals::curtime >= voxel_anims::nextframe) { + voxel_anims::nextframe = globals::curtime + static_cast<std::uint64_t>(1000000.0 / static_cast<float>(base_framerate.get_value())); + voxel_anims::frame += 1U; + } +} diff --git a/src/game/client/voxel_anims.hh b/src/game/client/voxel_anims.hh new file mode 100644 index 0000000..5d6b9f9 --- /dev/null +++ b/src/game/client/voxel_anims.hh @@ -0,0 +1,17 @@ +#ifndef CLIENT_VOXEL_ANIMS_HH +#define CLIENT_VOXEL_ANIMS_HH 1 +#pragma once + +namespace voxel_anims +{ +extern std::uint64_t nextframe; +extern std::uint32_t frame; +} // namespace voxel_anims + +namespace voxel_anims +{ +void init(void); +void update(void); +} // namespace voxel_anims + +#endif /* CLIENT_VOXEL_ANIMS_HH */ diff --git a/src/game/client/voxel_atlas.cc b/src/game/client/voxel_atlas.cc new file mode 100644 index 0000000..ef996e2 --- /dev/null +++ b/src/game/client/voxel_atlas.cc @@ -0,0 +1,183 @@ +#include "client/pch.hh" + +#include "client/voxel_atlas.hh" + +#include "core/constexpr.hh" +#include "core/crc64.hh" +#include "core/image.hh" +#include "core/resource.hh" + +struct AtlasPlane final { + std::unordered_map<std::size_t, std::size_t> lookup; + std::vector<AtlasStrip> strips; + std::size_t layer_count_max; + std::size_t layer_count; + std::size_t plane_id; + GLuint gl_texture; +}; + +static int atlas_width; +static int atlas_height; +static std::size_t atlas_count; +static std::vector<AtlasPlane> planes; + +// Certain animated and varied voxels just double their +// textures (see the "default" texture part in VoxelInfoBuilder::build) +// so there could either be six UNIQUE atlas strips or only one +// https://crypto.stackexchange.com/questions/55162/best-way-to-hash-two-values-into-one +static std::size_t vector_hash(const std::vector<std::string>& strings) +{ + std::size_t source = 0; + for(const std::string& str : strings) + source += crc64::get(str); + return crc64::get(&source, sizeof(source)); +} + +static void plane_setup(AtlasPlane& plane) +{ + glGenTextures(1, &plane.gl_texture); + glBindTexture(GL_TEXTURE_2D_ARRAY, plane.gl_texture); + glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, atlas_width, atlas_height, plane.layer_count_max, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT); +} + +static AtlasStrip* plane_lookup(AtlasPlane& plane, std::size_t hash_value) +{ + const auto it = plane.lookup.find(hash_value); + + if(it != plane.lookup.cend()) { + return &plane.strips[it->second]; + } + + return nullptr; +} + +static AtlasStrip* plane_new_strip(AtlasPlane& plane, const std::vector<std::string>& paths, std::size_t hash_value) +{ + AtlasStrip strip = {}; + strip.offset = plane.layer_count; + strip.plane = plane.plane_id; + + glBindTexture(GL_TEXTURE_2D_ARRAY, plane.gl_texture); + + for(std::size_t i = 0; i < paths.size(); ++i) { + if(auto image = resource::load<Image>(paths[i].c_str(), IMAGE_LOAD_FLIP)) { + if((image->size.x != atlas_width) || (image->size.y != atlas_height)) { + spdlog::warn("atlas: {}: size mismatch", paths[i]); + continue; + } + + const std::size_t offset = strip.offset + i; + glTexSubImage3D( + GL_TEXTURE_2D_ARRAY, 0, 0, 0, offset, image->size.x, image->size.y, 1, GL_RGBA, GL_UNSIGNED_BYTE, image->pixels); + } + } + + plane.layer_count += paths.size(); + + const std::size_t index = plane.strips.size(); + plane.lookup.emplace(hash_value, index); + plane.strips.push_back(std::move(strip)); + return &plane.strips[index]; +} + +void voxel_atlas::create(int width, int height, std::size_t count) +{ + GLint max_plane_layers; + + atlas_width = 1 << vx::log2(width); + atlas_height = 1 << vx::log2(height); + + // Clipping this at OpenGL 4.5 limit of 2048 is important due to + // how voxel quad meshes are packed in memory: each texture index is + // confined in 11 bits so having bigger atlas planes makes no sense; + glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &max_plane_layers); + max_plane_layers = vx::clamp(max_plane_layers, 256, 2048); + + for(long i = count; i > 0L; i -= max_plane_layers) { + AtlasPlane plane = {}; + plane.plane_id = planes.size(); + plane.layer_count_max = vx::min<std::size_t>(max_plane_layers, i); + plane.layer_count = 0; + + const std::size_t save_id = plane.plane_id; + planes.push_back(std::move(plane)); + plane_setup(planes[save_id]); + } + + spdlog::debug("voxel_atlas: count={}", count); + spdlog::debug("voxel_atlas: atlas_size=[{}x{}]", atlas_width, atlas_height); + spdlog::debug("voxel_atlas: max_plane_layers={}", max_plane_layers); +} + +void voxel_atlas::destroy(void) +{ + for(const AtlasPlane& plane : planes) + glDeleteTextures(1, &plane.gl_texture); + atlas_width = 0; + atlas_height = 0; + planes.clear(); +} + +std::size_t voxel_atlas::plane_count(void) +{ + return planes.size(); +} + +GLuint voxel_atlas::plane_texture(std::size_t plane_id) +{ + if(plane_id < planes.size()) { + return planes[plane_id].gl_texture; + } else { + return 0; + } +} + +void voxel_atlas::generate_mipmaps(void) +{ + for(const AtlasPlane& plane : planes) { + glBindTexture(GL_TEXTURE_2D_ARRAY, plane.gl_texture); + glGenerateMipmap(GL_TEXTURE_2D_ARRAY); + } +} + +AtlasStrip* voxel_atlas::find_or_load(const std::vector<std::string>& paths) +{ + const std::size_t hash_value = vector_hash(paths); + + for(AtlasPlane& plane : planes) { + if(AtlasStrip* strip = plane_lookup(plane, hash_value)) { + return strip; + } + + continue; + } + + for(AtlasPlane& plane : planes) { + if((plane.layer_count + paths.size()) <= plane.layer_count_max) { + return plane_new_strip(plane, paths, hash_value); + } + + continue; + } + + return nullptr; +} + +AtlasStrip* voxel_atlas::find(const std::vector<std::string>& paths) +{ + const std::size_t hash_value = vector_hash(paths); + + for(AtlasPlane& plane : planes) { + if(AtlasStrip* strip = plane_lookup(plane, hash_value)) { + return strip; + } + + continue; + } + + return nullptr; +} diff --git a/src/game/client/voxel_atlas.hh b/src/game/client/voxel_atlas.hh new file mode 100644 index 0000000..67036ca --- /dev/null +++ b/src/game/client/voxel_atlas.hh @@ -0,0 +1,29 @@ +#ifndef CLIENT_VOXEL_ATLAS_HH +#define CLIENT_VOXEL_ATLAS_HH 1 +#pragma once + +struct AtlasStrip final { + std::size_t offset; + std::size_t plane; +}; + +namespace voxel_atlas +{ +void create(int width, int height, std::size_t count); +void destroy(void); +} // namespace voxel_atlas + +namespace voxel_atlas +{ +std::size_t plane_count(void); +GLuint plane_texture(std::size_t plane_id); +void generate_mipmaps(void); +} // namespace voxel_atlas + +namespace voxel_atlas +{ +AtlasStrip* find_or_load(const std::vector<std::string>& paths); +AtlasStrip* find(const std::vector<std::string>& paths); +} // namespace voxel_atlas + +#endif /* CLIENT_VOXEL_ATLAS_HH */ diff --git a/src/game/client/voxel_sounds.cc b/src/game/client/voxel_sounds.cc new file mode 100644 index 0000000..fd02a8e --- /dev/null +++ b/src/game/client/voxel_sounds.cc @@ -0,0 +1,86 @@ +#include "client/pch.hh" + +#include "client/voxel_sounds.hh" + +#include "client/sound_effect.hh" + +constexpr static std::size_t NUM_SURFACES = static_cast<std::size_t>(voxel_surface::COUNT); + +static std::vector<resource_ptr<SoundEffect>> footsteps_sounds[NUM_SURFACES]; +static std::mt19937_64 randomizer; + +static void add_footsteps_effect(voxel_surface surface, const char* name) +{ + if(auto effect = resource::load<SoundEffect>(name)) { + auto surface_index = static_cast<std::size_t>(surface); + footsteps_sounds[surface_index].push_back(effect); + } +} + +static resource_ptr<SoundEffect> get_footsteps_effect(voxel_surface surface) +{ + auto surface_index = static_cast<std::size_t>(surface); + + if(surface_index >= NUM_SURFACES) { + // Surface index out of range + return nullptr; + } + + const auto& sounds = footsteps_sounds[surface_index]; + + if(sounds.empty()) { + // No sounds for this surface + return nullptr; + } + + auto dist = std::uniform_int_distribution<std::size_t>(0, sounds.size() - 1); + return sounds.at(dist(randomizer)); +} + +void voxel_sounds::init(void) +{ + add_footsteps_effect(voxel_surface::DEFAULT, "sounds/surface/default1.wav"); + add_footsteps_effect(voxel_surface::DEFAULT, "sounds/surface/default2.wav"); + add_footsteps_effect(voxel_surface::DEFAULT, "sounds/surface/default3.wav"); + add_footsteps_effect(voxel_surface::DEFAULT, "sounds/surface/default4.wav"); + + add_footsteps_effect(voxel_surface::DIRT, "sounds/surface/dirt1.wav"); + + add_footsteps_effect(voxel_surface::GRASS, "sounds/surface/grass1.wav"); + add_footsteps_effect(voxel_surface::GRASS, "sounds/surface/grass2.wav"); + add_footsteps_effect(voxel_surface::GRASS, "sounds/surface/grass3.wav"); + + add_footsteps_effect(voxel_surface::GRAVEL, "sounds/surface/gravel1.wav"); + + add_footsteps_effect(voxel_surface::SAND, "sounds/surface/sand1.wav"); + add_footsteps_effect(voxel_surface::SAND, "sounds/surface/sand2.wav"); + + add_footsteps_effect(voxel_surface::WOOD, "sounds/surface/wood1.wav"); + add_footsteps_effect(voxel_surface::WOOD, "sounds/surface/wood2.wav"); + add_footsteps_effect(voxel_surface::WOOD, "sounds/surface/wood3.wav"); +} + +void voxel_sounds::deinit(void) +{ + for(std::size_t i = 0; i < NUM_SURFACES; ++i) { + footsteps_sounds[i].clear(); + } +} + +resource_ptr<SoundEffect> voxel_sounds::get_footsteps(voxel_surface surface) +{ + if(auto effect = get_footsteps_effect(surface)) { + return effect; + } + + if(auto effect = get_footsteps_effect(voxel_surface::DEFAULT)) { + return effect; + } + + return nullptr; +} + +resource_ptr<SoundEffect> voxel_sounds::get_placebreak(voxel_surface surface) +{ + return nullptr; +} diff --git a/src/game/client/voxel_sounds.hh b/src/game/client/voxel_sounds.hh new file mode 100644 index 0000000..ed11c0f --- /dev/null +++ b/src/game/client/voxel_sounds.hh @@ -0,0 +1,23 @@ +#ifndef CLIENT_VOXEL_SOUNDS_HH +#define CLIENT_VOXEL_SOUNDS_HH 1 +#pragma once + +#include "core/resource.hh" + +#include "shared/voxel_registry.hh" + +struct SoundEffect; + +namespace voxel_sounds +{ +void init(void); +void deinit(void); +} // namespace voxel_sounds + +namespace voxel_sounds +{ +resource_ptr<SoundEffect> get_footsteps(voxel_surface surface); +resource_ptr<SoundEffect> get_placebreak(voxel_surface surface); +} // namespace voxel_sounds + +#endif /* CLIENT_VOXEL_SOUNDS_HH */ diff --git a/src/game/client/window_title.cc b/src/game/client/window_title.cc new file mode 100644 index 0000000..517429d --- /dev/null +++ b/src/game/client/window_title.cc @@ -0,0 +1,22 @@ +#include "client/pch.hh" + +#include "client/window_title.hh" + +#include "core/version.hh" + +#include "shared/splash.hh" + +#include "client/globals.hh" + +void window_title::update(void) +{ + std::string title; + + if(globals::sound_ctx && globals::sound_dev) { + title = std::format("Voxelius {}: {}", project_version_string, splash::get()); + } else { + title = std::format("Voxelius {}: {} [NOSOUND]", project_version_string, splash::get()); + } + + glfwSetWindowTitle(globals::window, title.c_str()); +} diff --git a/src/game/client/window_title.hh b/src/game/client/window_title.hh new file mode 100644 index 0000000..25051c8 --- /dev/null +++ b/src/game/client/window_title.hh @@ -0,0 +1,10 @@ +#ifndef CLIENT_WINDOW_TITLE_HH +#define CLIENT_WINDOW_TITLE_HH 1 +#pragma once + +namespace window_title +{ +void update(void); +} // namespace window_title + +#endif /* CLIENT_WINDOW_TITLE_HH */ diff --git a/src/game/server/CMakeLists.txt b/src/game/server/CMakeLists.txt new file mode 100644 index 0000000..bd0eff3 --- /dev/null +++ b/src/game/server/CMakeLists.txt @@ -0,0 +1,39 @@ +add_executable(vserver + "${CMAKE_CURRENT_LIST_DIR}/chat.cc" + "${CMAKE_CURRENT_LIST_DIR}/chat.hh" + "${CMAKE_CURRENT_LIST_DIR}/game.cc" + "${CMAKE_CURRENT_LIST_DIR}/game.hh" + "${CMAKE_CURRENT_LIST_DIR}/globals.cc" + "${CMAKE_CURRENT_LIST_DIR}/globals.hh" + "${CMAKE_CURRENT_LIST_DIR}/inhabited.hh" + "${CMAKE_CURRENT_LIST_DIR}/main.cc" + "${CMAKE_CURRENT_LIST_DIR}/overworld.cc" + "${CMAKE_CURRENT_LIST_DIR}/overworld.hh" + "${CMAKE_CURRENT_LIST_DIR}/pch.hh" + "${CMAKE_CURRENT_LIST_DIR}/receive.cc" + "${CMAKE_CURRENT_LIST_DIR}/receive.hh" + "${CMAKE_CURRENT_LIST_DIR}/sessions.cc" + "${CMAKE_CURRENT_LIST_DIR}/sessions.hh" + "${CMAKE_CURRENT_LIST_DIR}/status.cc" + "${CMAKE_CURRENT_LIST_DIR}/status.hh" + "${CMAKE_CURRENT_LIST_DIR}/universe.cc" + "${CMAKE_CURRENT_LIST_DIR}/universe.hh" + "${CMAKE_CURRENT_LIST_DIR}/unloader.cc" + "${CMAKE_CURRENT_LIST_DIR}/unloader.hh" + "${CMAKE_CURRENT_LIST_DIR}/whitelist.cc" + "${CMAKE_CURRENT_LIST_DIR}/whitelist.hh" + "${CMAKE_CURRENT_LIST_DIR}/worldgen.cc" + "${CMAKE_CURRENT_LIST_DIR}/worldgen.hh") +target_compile_features(vserver PUBLIC cxx_std_20) +target_include_directories(vserver PUBLIC "${DEPS_INCLUDE_DIR}") +target_include_directories(vserver PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_include_directories(vserver PRIVATE "${PROJECT_SOURCE_DIR}/src/game") +target_precompile_headers(vserver PRIVATE "${CMAKE_CURRENT_LIST_DIR}/pch.hh") +target_link_libraries(vserver PUBLIC shared) + +if(WIN32) + enable_language(RC) + target_sources(vserver PRIVATE "${CMAKE_CURRENT_LIST_DIR}/vserver.rc") +endif() + +install(TARGETS vserver RUNTIME DESTINATION ".") diff --git a/src/game/server/chat.cc b/src/game/server/chat.cc new file mode 100644 index 0000000..1634c59 --- /dev/null +++ b/src/game/server/chat.cc @@ -0,0 +1,54 @@ +#include "server/pch.hh" + +#include "server/chat.hh" + +#include "server/globals.hh" +#include "server/sessions.hh" +#include "shared/protocol.hh" + +static void on_chat_message_packet(const protocol::ChatMessage& packet) +{ + if(packet.type == protocol::ChatMessage::TEXT_MESSAGE) { + if(auto session = sessions::find(packet.peer)) { + server_chat::broadcast(packet.message.c_str(), session->client_username.c_str()); + } else { + server_chat::broadcast(packet.message.c_str(), packet.sender.c_str()); + } + } +} + +void server_chat::init(void) +{ + globals::dispatcher.sink<protocol::ChatMessage>().connect<&on_chat_message_packet>(); +} + +void server_chat::broadcast(const char* message) +{ + server_chat::broadcast(message, "server"); +} + +void server_chat::broadcast(const char* message, const char* sender) +{ + protocol::ChatMessage packet; + packet.type = protocol::ChatMessage::TEXT_MESSAGE; + packet.message = std::string(message); + packet.sender = std::string(sender); + + protocol::broadcast(globals::server_host, protocol::encode(packet)); + + spdlog::info("<{}> {}", sender, message); +} + +void server_chat::send(Session* session, const char* message) +{ + server_chat::send(session, message, "server"); +} + +void server_chat::send(Session* session, const char* message, const char* sender) +{ + protocol::ChatMessage packet; + packet.type = protocol::ChatMessage::TEXT_MESSAGE; + packet.message = std::string(message); + packet.sender = std::string(sender); + protocol::broadcast(globals::server_host, protocol::encode(packet)); +} diff --git a/src/game/server/chat.hh b/src/game/server/chat.hh new file mode 100644 index 0000000..2557290 --- /dev/null +++ b/src/game/server/chat.hh @@ -0,0 +1,16 @@ +#ifndef SERVER_CHAT_HH +#define SERVER_CHAT_HH 1 +#pragma once + +struct Session; + +namespace server_chat +{ +void init(void); +void broadcast(const char* message); +void broadcast(const char* message, const char* sender); +void send(Session* session, const char* message); +void send(Session* session, const char* message, const char* sender); +} // namespace server_chat + +#endif /* SERVER_CHAT_HH */ diff --git a/src/game/server/game.cc b/src/game/server/game.cc new file mode 100644 index 0000000..978474c --- /dev/null +++ b/src/game/server/game.cc @@ -0,0 +1,148 @@ +#include "server/pch.hh" + +#include "server/game.hh" + +#include "core/cmdline.hh" +#include "core/config.hh" +#include "core/constexpr.hh" +#include "core/crc64.hh" +#include "core/epoch.hh" + +#include "shared/collision.hh" +#include "shared/dimension.hh" +#include "shared/game_items.hh" +#include "shared/game_voxels.hh" +#include "shared/gravity.hh" +#include "shared/head.hh" +#include "shared/player.hh" +#include "shared/protocol.hh" +#include "shared/splash.hh" +#include "shared/stasis.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" + +#include "server/chat.hh" +#include "server/globals.hh" +#include "server/receive.hh" +#include "server/sessions.hh" +#include "server/status.hh" +#include "server/universe.hh" +#include "server/unloader.hh" +#include "server/whitelist.hh" +#include "server/worldgen.hh" + +ConfigUnsigned server_game::view_distance(4U, 4U, 32U); + +std::uint64_t server_game::password_hash = UINT64_MAX; + +static ConfigNumber<enet_uint16> listen_port(protocol::PORT, 1024U, UINT16_MAX); +static ConfigUnsigned status_peers(2U, 1U, 16U); +static ConfigString password_string(""); + +void server_game::init(void) +{ + globals::server_config.add_value("game.listen_port", listen_port); + globals::server_config.add_value("game.status_peers", status_peers); + globals::server_config.add_value("game.password", password_string); + globals::server_config.add_value("game.view_distance", server_game::view_distance); + + sessions::init(); + + whitelist::init(); + + splash::init_server(); + + status::init(); + + server_chat::init(); + server_recieve::init(); + + worldgen::init(); + + unloader::init(); + universe::init(); +} + +void server_game::init_late(void) +{ + server_game::password_hash = crc64::get(password_string.get()); + + sessions::init_late(); + + whitelist::init_late(); + + ENetAddress address; + address.host = ENET_HOST_ANY; + address.port = listen_port.get_value(); + + globals::server_host = enet_host_create(&address, sessions::max_players.get_value() + status_peers.get_value(), 1, 0, 0); + + if(!globals::server_host) { + spdlog::critical("game: unable to setup an ENet host"); + std::terminate(); + } + + spdlog::info("game: host: {} player + {} status peers", sessions::max_players.get_value(), status_peers.get_value()); + spdlog::info("game: host: listening on UDP port {}", address.port); + + game_voxels::populate(); + game_items::populate(); + + unloader::init_late(); + universe::init_late(); + + sessions::init_post_universe(); +} + +void server_game::deinit(void) +{ + protocol::Disconnect packet; + packet.reason = "protocol.server_shutdown"; + protocol::broadcast(globals::server_host, protocol::encode(packet)); + + whitelist::deinit(); + + sessions::deinit(); + + enet_host_flush(globals::server_host); + enet_host_service(globals::server_host, nullptr, 500); + enet_host_destroy(globals::server_host); + + universe::deinit(); +} + +void server_game::fixed_update(void) +{ + // FIXME: threading + for(auto dimension : globals::dimensions) { + CollisionComponent::fixed_update(dimension.second); + VelocityComponent::fixed_update(dimension.second); + TransformComponent::fixed_update(dimension.second); + GravityComponent::fixed_update(dimension.second); + StasisComponent::fixed_update(dimension.second); + } +} + +void server_game::fixed_update_late(void) +{ + ENetEvent enet_event; + + while(0 < enet_host_service(globals::server_host, &enet_event, 0)) { + if(enet_event.type == ENET_EVENT_TYPE_DISCONNECT) { + sessions::destroy(sessions::find(enet_event.peer)); + sessions::refresh_scoreboard(); + continue; + } + + if(enet_event.type == ENET_EVENT_TYPE_RECEIVE) { + protocol::decode(globals::dispatcher, enet_event.packet, enet_event.peer); + enet_packet_destroy(enet_event.packet); + continue; + } + } + + // FIXME: threading + for(auto dimension : globals::dimensions) { + unloader::fixed_update_late(dimension.second); + } +} diff --git a/src/game/server/game.hh b/src/game/server/game.hh new file mode 100644 index 0000000..98c6bf3 --- /dev/null +++ b/src/game/server/game.hh @@ -0,0 +1,26 @@ +#ifndef SERVER_GAME_HH +#define SERVER_GAME_HH 1 +#pragma once + +class ConfigUnsigned; + +namespace server_game +{ +extern ConfigUnsigned view_distance; +} // namespace server_game + +namespace server_game +{ +extern std::uint64_t password_hash; +} // namespace server_game + +namespace server_game +{ +void init(void); +void init_late(void); +void deinit(void); +void fixed_update(void); +void fixed_update_late(void); +} // namespace server_game + +#endif /* SERVER_GAME_HH */ diff --git a/src/game/server/globals.cc b/src/game/server/globals.cc new file mode 100644 index 0000000..883588b --- /dev/null +++ b/src/game/server/globals.cc @@ -0,0 +1,18 @@ +#include "server/pch.hh" + +#include "server/globals.hh" + +#include "core/config.hh" + +#include "shared/protocol.hh" + +Config globals::server_config; + +ENetHost* globals::server_host; + +bool globals::is_running; +unsigned int globals::tickrate; +std::uint64_t globals::tickrate_dt; + +Dimension* globals::spawn_dimension; +std::unordered_map<std::string, Dimension*> globals::dimensions; diff --git a/src/game/server/globals.hh b/src/game/server/globals.hh new file mode 100644 index 0000000..54f025a --- /dev/null +++ b/src/game/server/globals.hh @@ -0,0 +1,25 @@ +#ifndef SERVER_GLOBALS_HH +#define SERVER_GLOBALS_HH 1 +#pragma once + +#include "shared/globals.hh" + +class Config; + +class Dimension; + +namespace globals +{ +extern Config server_config; + +extern ENetHost* server_host; + +extern bool is_running; +extern unsigned int tickrate; +extern std::uint64_t tickrate_dt; + +extern Dimension* spawn_dimension; +extern std::unordered_map<std::string, Dimension*> dimensions; +} // namespace globals + +#endif /* SERVER_GLOBALS_HH */ diff --git a/src/game/server/inhabited.hh b/src/game/server/inhabited.hh new file mode 100644 index 0000000..c68ddaa --- /dev/null +++ b/src/game/server/inhabited.hh @@ -0,0 +1,7 @@ +#ifndef SERVER_INHABITED_HH +#define SERVER_INHABITED_HH 1 +#pragma once + +struct InhabitedComponent final {}; + +#endif /* SERVER_INHABITED_HH */ diff --git a/src/game/server/main.cc b/src/game/server/main.cc new file mode 100644 index 0000000..734ae6a --- /dev/null +++ b/src/game/server/main.cc @@ -0,0 +1,103 @@ +#include "server/pch.hh" + +#include "core/binfile.hh" +#include "core/cmdline.hh" +#include "core/config.hh" +#include "core/constexpr.hh" +#include "core/epoch.hh" +#include "core/image.hh" +#include "core/resource.hh" +#include "core/version.hh" + +#include "shared/game.hh" +#include "shared/protocol.hh" +#include "shared/threading.hh" + +#include "server/game.hh" +#include "server/globals.hh" + +static ConfigUnsigned server_tickrate(protocol::TICKRATE, 10U, 300U); + +static void on_termination_signal(int) +{ + spdlog::warn("server: received termination signal"); + globals::is_running = false; +} + +int main(int argc, char** argv) +{ + cmdline::create(argc, argv); + + shared_game::init(argc, argv); + + spdlog::info("Voxelius Server {}", project_version_string); + + globals::fixed_frametime = 0.0f; + globals::fixed_frametime_avg = 0.0f; + globals::fixed_frametime_us = 0; + globals::fixed_framecount = 0; + + globals::curtime = epoch::microseconds(); + + globals::is_running = true; + + std::signal(SIGINT, &on_termination_signal); + std::signal(SIGTERM, &on_termination_signal); + + server_game::init(); + + threading::init(); + + globals::server_config.add_value("server.tickrate", server_tickrate); + globals::server_config.load_file("server.conf"); + globals::server_config.load_cmdline(); + + globals::tickrate = server_tickrate.get_value(); + globals::tickrate_dt = static_cast<std::uint64_t>(1000000.0f / static_cast<float>(globals::tickrate)); + + server_game::init_late(); + + std::uint64_t last_curtime = globals::curtime; + + while(globals::is_running) { + globals::curtime = epoch::microseconds(); + + globals::fixed_frametime_us = globals::curtime - last_curtime; + globals::fixed_frametime = static_cast<float>(globals::fixed_frametime_us) / 1000000.0f; + globals::fixed_frametime_avg += globals::fixed_frametime; + globals::fixed_frametime_avg *= 0.5f; + + last_curtime = globals::curtime; + + server_game::fixed_update(); + server_game::fixed_update_late(); + + globals::dispatcher.update(); + + globals::fixed_framecount += 1; + + std::this_thread::sleep_for(std::chrono::microseconds(globals::tickrate_dt)); + + resource::soft_cleanup<BinFile>(); + resource::soft_cleanup<Image>(); + + threading::update(); + } + + server_game::deinit(); + + resource::hard_cleanup<BinFile>(); + resource::hard_cleanup<Image>(); + + threading::deinit(); + + spdlog::info("server: shutdown after {} frames", globals::fixed_framecount); + spdlog::info("server: average framerate: {:.03f} TPS", 1.0f / globals::fixed_frametime_avg); + spdlog::info("server: average frametime: {:.03f} MSPT", 1000.0f * globals::fixed_frametime_avg); + + globals::server_config.save_file("server.conf"); + + shared_game::deinit(); + + return EXIT_SUCCESS; +} diff --git a/src/game/server/overworld.cc b/src/game/server/overworld.cc new file mode 100644 index 0000000..a280e06 --- /dev/null +++ b/src/game/server/overworld.cc @@ -0,0 +1,380 @@ +#include "server/pch.hh" + +#include "server/overworld.hh" + +#include "core/vectors.hh" + +#include "shared/coord.hh" +#include "shared/game_voxels.hh" +#include "shared/voxel_storage.hh" + +// FIXME: load these from a file +static void compute_tree_feature(unsigned int height, Feature& feature, voxel_id log_voxel, voxel_id leaves_voxel) +{ + // Ensure the tree height is too small + height = vx::max<unsigned int>(height, 4U); + + // Put down a single piece of dirt + feature.push_back({ voxel_pos(0, -1, 0), game_voxels::dirt, true }); + + // Generate tree stem + for(unsigned int i = 0; i < height; ++i) { + feature.push_back({ voxel_pos(0, i, 0), log_voxel, true }); + } + + auto leaves_start = height - 3U; + auto leaves_thick_end = height - 2U; + auto leaves_thin_end = height - 1U; + + // Generate the thin 3x3 layer of leaves that + // starts from leaves_start and ends at leaves_thin_end + for(unsigned int i = leaves_start; i <= leaves_thin_end; ++i) { + feature.push_back({ local_pos(-1, i, -1), leaves_voxel, false }); + feature.push_back({ local_pos(-1, i, +0), leaves_voxel, false }); + feature.push_back({ local_pos(-1, i, +1), leaves_voxel, false }); + feature.push_back({ local_pos(+0, i, -1), leaves_voxel, false }); + feature.push_back({ local_pos(+0, i, +1), leaves_voxel, false }); + feature.push_back({ local_pos(+1, i, -1), leaves_voxel, false }); + feature.push_back({ local_pos(+1, i, +0), leaves_voxel, false }); + feature.push_back({ local_pos(+1, i, +1), leaves_voxel, false }); + } + + // Generate the tree cap; a 3x3 patch of leaves + // that is slapped right on top of the thin 3x3 layer + feature.push_back({ local_pos(-1, height, +0), leaves_voxel, false }); + feature.push_back({ local_pos(+0, height, -1), leaves_voxel, false }); + feature.push_back({ local_pos(+0, height, +0), leaves_voxel, false }); + feature.push_back({ local_pos(+0, height, +1), leaves_voxel, false }); + feature.push_back({ local_pos(+1, height, +0), leaves_voxel, false }); + + // Generate the thin 5x5 layer of leaves that + // starts from leaves_start and ends at leaves_thin_end + for(unsigned int i = leaves_start; i <= leaves_thick_end; ++i) { + feature.push_back({ local_pos(-1, i, -2), leaves_voxel, false }); + feature.push_back({ local_pos(-1, i, +2), leaves_voxel, false }); + feature.push_back({ local_pos(-2, i, -1), leaves_voxel, false }); + feature.push_back({ local_pos(-2, i, -2), leaves_voxel, false }); + feature.push_back({ local_pos(-2, i, +0), leaves_voxel, false }); + feature.push_back({ local_pos(-2, i, +1), leaves_voxel, false }); + feature.push_back({ local_pos(-2, i, +2), leaves_voxel, false }); + feature.push_back({ local_pos(+0, i, -2), leaves_voxel, false }); + feature.push_back({ local_pos(+0, i, +2), leaves_voxel, false }); + feature.push_back({ local_pos(+1, i, -2), leaves_voxel, false }); + feature.push_back({ local_pos(+1, i, +2), leaves_voxel, false }); + feature.push_back({ local_pos(+2, i, -1), leaves_voxel, false }); + feature.push_back({ local_pos(+2, i, -2), leaves_voxel, false }); + feature.push_back({ local_pos(+2, i, +0), leaves_voxel, false }); + feature.push_back({ local_pos(+2, i, +1), leaves_voxel, false }); + feature.push_back({ local_pos(+2, i, +2), leaves_voxel, false }); + } +} + +Overworld::Overworld(const char* name) : Dimension(name, -30.0f) +{ + m_bottommost_chunk.set_limits(-64, -4); + m_terrain_variation.set_limits(16, 256); + + compute_tree_feature(4U, m_feat_tree[0], game_voxels::oak_log, game_voxels::oak_leaves); + compute_tree_feature(5U, m_feat_tree[1], game_voxels::oak_log, game_voxels::oak_leaves); + compute_tree_feature(6U, m_feat_tree[2], game_voxels::oak_log, game_voxels::oak_leaves); + compute_tree_feature(8U, m_feat_tree[3], game_voxels::oak_log, game_voxels::oak_leaves); +} + +void Overworld::init(Config& config) +{ + m_terrain_variation.set_value(64); + m_bottommost_chunk.set_value(-4); + + config.add_value("overworld.terrain_variation", m_terrain_variation); + config.add_value("overworld.bottommost_chunk", m_bottommost_chunk); +} + +void Overworld::init_late(std::uint64_t global_seed) +{ + std::mt19937 twister(global_seed); + + m_fnl_variation = fnlCreateState(); + m_fnl_variation.seed = static_cast<int>(twister()); + m_fnl_variation.noise_type = FNL_NOISE_PERLIN; + m_fnl_variation.frequency = 0.001f; + + m_fnl_terrain = fnlCreateState(); + m_fnl_terrain.seed = static_cast<int>(twister()); + m_fnl_terrain.noise_type = FNL_NOISE_OPENSIMPLEX2S; + m_fnl_terrain.fractal_type = FNL_FRACTAL_FBM; + m_fnl_terrain.frequency = 0.005f; + m_fnl_terrain.octaves = 4; + + m_fnl_caves_a = fnlCreateState(); + m_fnl_caves_a.seed = static_cast<int>(twister()); + m_fnl_caves_a.noise_type = FNL_NOISE_PERLIN; + m_fnl_caves_a.fractal_type = FNL_FRACTAL_RIDGED; + m_fnl_caves_a.frequency = 0.0125f; + m_fnl_caves_a.octaves = 1; + + m_fnl_caves_b = fnlCreateState(); + m_fnl_caves_b.seed = static_cast<int>(twister()); + m_fnl_caves_b.noise_type = FNL_NOISE_OPENSIMPLEX2S; + m_fnl_caves_b.fractal_type = FNL_FRACTAL_RIDGED; + m_fnl_caves_b.frequency = 0.0125f; + m_fnl_caves_b.octaves = 1; + + m_fnl_nvdi = fnlCreateState(); + m_fnl_nvdi.seed = static_cast<int>(twister()); + m_fnl_nvdi.noise_type = FNL_NOISE_OPENSIMPLEX2S; + m_fnl_nvdi.frequency = 1.0f; + + m_metamap.clear(); +} + +bool Overworld::generate(const chunk_pos& cpos, VoxelStorage& voxels) +{ + if(cpos.y <= m_bottommost_chunk.get_value()) { + // If the player asks the generator + // to generate a lot of stuff below + // the surface, it will happily chew + // through all the server threads + return false; + } + + voxels.fill(NULL_VOXEL_ID); + + m_mutex.lock(); + generate_terrain(cpos, voxels); + m_mutex.unlock(); + + m_mutex.lock(); + generate_surface(cpos, voxels); + m_mutex.unlock(); + + m_mutex.lock(); + generate_caves(cpos, voxels); + m_mutex.unlock(); + + m_mutex.lock(); + generate_features(cpos, voxels); + m_mutex.unlock(); + + return true; +} + +bool Overworld::is_inside_cave(const voxel_pos& vpos) +{ + auto noise_a = fnlGetNoise3D(&m_fnl_caves_a, vpos.x, vpos.y * 2.0f, vpos.z); + auto noise_b = fnlGetNoise3D(&m_fnl_caves_b, vpos.x, vpos.y * 2.0f, vpos.z); + return (noise_a > 0.95f) && (noise_b > 0.85f); +} + +bool Overworld::is_inside_terrain(const voxel_pos& vpos) +{ + auto variation_noise = fnlGetNoise3D(&m_fnl_terrain, vpos.x, vpos.y, vpos.z); + auto variation = m_terrain_variation.get_value() * (1.0f - (variation_noise * variation_noise)); + auto noise = variation * fnlGetNoise3D(&m_fnl_terrain, vpos.x, vpos.y, vpos.z) - vpos.y; + return noise > 0.0f; +} + +const Overworld_Metadata& Overworld::get_or_create_metadata(const chunk_pos_xz& cpos) +{ + auto it = m_metamap.find(cpos); + + if(it != m_metamap.cend()) { + // Metadata is present + return it->second; + } + + auto& metadata = m_metamap.insert_or_assign(cpos, Overworld_Metadata()).first->second; + metadata.entropy.fill(std::numeric_limits<std::uint64_t>::max()); + metadata.heightmap.fill(std::numeric_limits<voxel_pos::value_type>::min()); + + auto twister = std::mt19937_64(std::hash<chunk_pos_xz>()(cpos)); + auto variation = m_terrain_variation.get_value(); + + // Generator might need some randomness + // that depends on 2D coordinates, so we + // generate this entropy ahead of time + for(int i = 0; i < CHUNK_AREA; ++i) { + metadata.entropy[i] = twister(); + } + + // Generate speculative heightmap; + // Cave generation might have issues with placing + // surface features such as trees but I genuinely don't give a shit + for(int lx = 0; lx < CHUNK_SIZE; lx += 1) { + for(int lz = 0; lz < CHUNK_SIZE; lz += 1) { + auto hdx = static_cast<std::size_t>(lx + lz * CHUNK_SIZE); + auto vpos = coord::to_voxel(chunk_pos(cpos.x, 0, cpos.y), local_pos(lx, 0, lz)); + + for(vpos.y = variation; vpos.y >= -variation; vpos.y -= 1) { + if(is_inside_terrain(vpos)) { + metadata.heightmap[hdx] = vpos.y; + break; + } + } + } + } + + auto nvdi_value = 0.5f + 0.5f * fnlGetNoise2D(&m_fnl_nvdi, cpos.x, cpos.y); + auto tree_density = (nvdi_value >= 0.33f) ? vx::floor<unsigned int>(nvdi_value * 4.0f) : 0U; + + for(unsigned int i = 0U; i < tree_density; ++i) { + auto lpos = local_pos((twister() % CHUNK_SIZE), (twister() % OW_NUM_TREES), (twister() % CHUNK_SIZE)); + auto is_unique = true; + + for(const auto& check_lpos : metadata.trees) { + if(vx::distance2(check_lpos, lpos) <= 9) { + is_unique = false; + break; + } + } + + if(is_unique) { + metadata.trees.push_back(lpos); + } + } + + return metadata; +} + +void Overworld::generate_terrain(const chunk_pos& cpos, VoxelStorage& voxels) +{ + auto& metadata = get_or_create_metadata(chunk_pos_xz(cpos.x, cpos.z)); + auto variation = m_terrain_variation.get_value(); + + for(unsigned long i = 0; i < CHUNK_VOLUME; ++i) { + auto lpos = coord::to_local(i); + auto vpos = coord::to_voxel(cpos, lpos); + + if(vpos.y > variation) { + voxels[i] = NULL_VOXEL_ID; + continue; + } + + if(vpos.y < -variation) { + voxels[i] = game_voxels::stone; + continue; + } + + if(is_inside_terrain(vpos)) { + voxels[i] = game_voxels::stone; + continue; + } + } +} + +void Overworld::generate_surface(const chunk_pos& cpos, VoxelStorage& voxels) +{ + auto& metadata = get_or_create_metadata(chunk_pos_xz(cpos.x, cpos.z)); + auto variation = m_terrain_variation.get_value(); + + for(unsigned long i = 0; i < CHUNK_VOLUME; ++i) { + auto lpos = coord::to_local(i); + auto vpos = coord::to_voxel(cpos, lpos); + auto hdx = static_cast<std::size_t>(lpos.x + lpos.z * CHUNK_SIZE); + + if((vpos.y > variation) || (vpos.y < -variation)) { + // Speculative optimization + continue; + } + + if(voxels[i] == NULL_VOXEL_ID) { + // Surface voxel checks only apply for solid voxels; + // it's kind of obvious you can't replace air with grass + continue; + } + + unsigned int depth = 0U; + + for(unsigned int dy = 0U; dy < 5U; dy += 1U) { + auto d_lpos = local_pos(lpos.x, lpos.y + dy + 1, lpos.z); + auto d_vpos = coord::to_voxel(cpos, d_lpos); + auto d_index = coord::to_index(d_lpos); + + if(d_lpos.y >= CHUNK_SIZE) { + if(!is_inside_terrain(d_vpos)) { + break; + } + + depth += 1U; + } else { + if(voxels[d_index] == NULL_VOXEL_ID) { + break; + } + + depth += 1U; + } + } + + if(depth < 5U) { + if(depth == 0U) { + voxels[i] = game_voxels::grass; + } else { + voxels[i] = game_voxels::dirt; + } + } + } +} + +void Overworld::generate_caves(const chunk_pos& cpos, VoxelStorage& voxels) +{ + auto& metadata = get_or_create_metadata(chunk_pos_xz(cpos.x, cpos.z)); + auto variation = m_terrain_variation.get_value(); + + for(unsigned long i = 0U; i < CHUNK_VOLUME; ++i) { + auto lpos = coord::to_local(i); + auto vpos = coord::to_voxel(cpos, lpos); + + if(vpos.y > variation) { + // Speculative optimization - there's no solid + // terrain above variation to carve caves out from + continue; + } + + if(is_inside_cave(vpos)) { + voxels[i] = NULL_VOXEL_ID; + continue; + } + } +} + +void Overworld::generate_features(const chunk_pos& cpos, VoxelStorage& voxels) +{ + const chunk_pos_xz tree_chunks[] = { + chunk_pos_xz(cpos.x - 0, cpos.z - 1), + chunk_pos_xz(cpos.x - 1, cpos.z - 1), + chunk_pos_xz(cpos.x - 1, cpos.z + 0), + chunk_pos_xz(cpos.x - 1, cpos.z + 1), + chunk_pos_xz(cpos.x + 0, cpos.z + 0), + chunk_pos_xz(cpos.x + 0, cpos.z + 1), + chunk_pos_xz(cpos.x + 1, cpos.z - 1), + chunk_pos_xz(cpos.x + 1, cpos.z + 0), + chunk_pos_xz(cpos.x + 1, cpos.z + 1), + }; + + for(unsigned int i = 0U; i < vx::array_size(tree_chunks); ++i) { + const auto& cpos_xz = tree_chunks[i]; + const auto& metadata = get_or_create_metadata(cpos_xz); + + for(const auto& tree_info : metadata.trees) { + auto hdx = static_cast<std::size_t>(tree_info.x + tree_info.z * CHUNK_SIZE); + auto height = metadata.heightmap[hdx]; + + if(height == std::numeric_limits<voxel_pos::value_type>::min()) { + // What happened? Cave happened + continue; + } + + auto cpos_xyz = chunk_pos(cpos_xz.x, 0, cpos_xz.y); + auto lpos_xyz = local_pos(tree_info.x, 0, tree_info.z); + + auto vpos = coord::to_voxel(cpos_xyz, lpos_xyz); + vpos.y = height; + + if(is_inside_cave(vpos)) { + // Cave is in the way + continue; + } + + m_feat_tree[tree_info.y].place(vpos + DIR_UP<voxel_pos::value_type>, cpos, voxels); + } + } +} diff --git a/src/game/server/overworld.hh b/src/game/server/overworld.hh new file mode 100644 index 0000000..972a91d --- /dev/null +++ b/src/game/server/overworld.hh @@ -0,0 +1,63 @@ +#ifndef SERVER_OVERWORLD_HH +#define SERVER_OVERWORLD_HH 1 +#pragma once + +#include "core/config.hh" + +#include "shared/const.hh" +#include "shared/dimension.hh" +#include "shared/feature.hh" + +constexpr static unsigned int OW_NUM_TREES = 4U; + +struct Overworld_Metadata final { + dimension_entropy_map entropy; + dimension_height_map heightmap; + std::vector<local_pos> trees; +}; + +class Overworld final : public Dimension { +public: + explicit Overworld(const char* name); + virtual ~Overworld(void) = default; + +public: + virtual void init(Config& config) override; + virtual void init_late(std::uint64_t global_seed) override; + virtual bool generate(const chunk_pos& cpos, VoxelStorage& voxels) override; + +private: + bool is_inside_cave(const voxel_pos& vpos); + bool is_inside_terrain(const voxel_pos& vpos); + +private: + const Overworld_Metadata& get_or_create_metadata(const chunk_pos_xz& cpos); + +private: + void generate_terrain(const chunk_pos& cpos, VoxelStorage& voxels); + void generate_surface(const chunk_pos& cpos, VoxelStorage& voxels); + void generate_caves(const chunk_pos& cpos, VoxelStorage& voxels); + void generate_features(const chunk_pos& cpos, VoxelStorage& voxels); + +private: + ConfigInt m_terrain_variation; + ConfigInt m_bottommost_chunk; + +private: + emhash8::HashMap<chunk_pos_xz, Overworld_Metadata> m_metamap; + +private: + fnl_state m_fnl_variation; + fnl_state m_fnl_terrain; + fnl_state m_fnl_caves_a; + fnl_state m_fnl_caves_b; + fnl_state m_fnl_nvdi; + +private: + Feature m_feat_tree[OW_NUM_TREES]; + +private: + std::mutex m_mutex; +}; + +#endif /* SERVER_OVERWORLD_HH */ diff --git a/src/game/server/pch.hh b/src/game/server/pch.hh new file mode 100644 index 0000000..89b396b --- /dev/null +++ b/src/game/server/pch.hh @@ -0,0 +1,7 @@ +#ifndef SERVER_PCH_HH +#define SERVER_PCH_HH 1 +#pragma once + +#include <shared/pch.hh> + +#endif /* SERVER_PCH_HH */ diff --git a/src/game/server/receive.cc b/src/game/server/receive.cc new file mode 100644 index 0000000..7674122 --- /dev/null +++ b/src/game/server/receive.cc @@ -0,0 +1,170 @@ +#include "server/pch.hh" + +#include "server/receive.hh" + +#include "core/config.hh" + +#include "shared/chunk_aabb.hh" +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/head.hh" +#include "shared/protocol.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" + +#include "server/game.hh" +#include "server/globals.hh" +#include "server/inhabited.hh" +#include "server/sessions.hh" +#include "server/universe.hh" +#include "server/worldgen.hh" + +static void on_entity_transform_packet(const protocol::EntityTransform& packet) +{ + if(auto session = sessions::find(packet.peer)) { + if(session->dimension && session->dimension->entities.valid(session->player_entity)) { + auto& component = session->dimension->entities.emplace_or_replace<TransformComponent>(session->player_entity); + component.angles = packet.angles; + component.chunk = packet.chunk; + component.local = packet.local; + + protocol::EntityTransform response; + response.entity = session->player_entity; + response.angles = component.angles; + response.chunk = component.chunk; + response.local = component.local; + + // Propagate changes to the rest of the world + // except the peer that has sent the packet in the first place + sessions::broadcast(session->dimension, protocol::encode(response), session->peer); + } + } +} + +static void on_entity_velocity_packet(const protocol::EntityVelocity& packet) +{ + if(auto session = sessions::find(packet.peer)) { + if(session->dimension && session->dimension->entities.valid(session->player_entity)) { + auto& component = session->dimension->entities.emplace_or_replace<VelocityComponent>(session->player_entity); + component.value = packet.value; + + protocol::EntityVelocity response; + response.entity = session->player_entity; + response.value = component.value; + + // Propagate changes to the rest of the world + // except the peer that has sent the packet in the first place + sessions::broadcast(session->dimension, protocol::encode(response), session->peer); + } + } +} + +static void on_entity_head_packet(const protocol::EntityHead& packet) +{ + if(auto session = sessions::find(packet.peer)) { + if(session->dimension && session->dimension->entities.valid(session->player_entity)) { + auto& component = session->dimension->entities.emplace_or_replace<HeadComponent>(session->player_entity); + component.angles = packet.angles; + + protocol::EntityHead response; + response.entity = session->player_entity; + response.angles = component.angles; + + // Propagate changes to the rest of the world + // except the peer that has sent the packet in the first place + sessions::broadcast(session->dimension, protocol::encode(response), session->peer); + } + } +} + +static void on_set_voxel_packet(const protocol::SetVoxel& packet) +{ + if(auto session = sessions::find(packet.peer)) { + if(session->dimension && !session->dimension->set_voxel(packet.voxel, packet.vpos)) { + auto cpos = coord::to_chunk(packet.vpos); + auto lpos = coord::to_local(packet.vpos); + auto index = coord::to_index(lpos); + + if(worldgen::is_generating(session->dimension, cpos)) { + // The chunk is currently being generated; + // ignore all requests from players to build there + return; + } + + auto chunk = session->dimension->find_chunk(cpos); + + if(chunk == nullptr) { + // The chunk is not loaded, so we must + // ignore any requests from players to build there + return; + } + + chunk->set_voxel(packet.voxel, index); + + session->dimension->chunks.emplace_or_replace<InhabitedComponent>(chunk->get_entity()); + + protocol::SetVoxel response; + response.vpos = packet.vpos; + response.voxel = packet.voxel; + sessions::broadcast(session->dimension, protocol::encode(response), session->peer); + + return; + } + } +} + +static void on_request_chunk_packet(const protocol::RequestChunk& packet) +{ + if(auto session = sessions::find(packet.peer)) { + if(!session->dimension || !session->dimension->entities.valid(session->player_entity)) { + // De-spawned sessions cannot request + // chunks from the server; that's cheating!!! + return; + } + + if(auto transform = session->dimension->entities.try_get<TransformComponent>(session->player_entity)) { + ChunkAABB view_box; + view_box.min = transform->chunk - static_cast<chunk_pos::value_type>(server_game::view_distance.get_value()); + view_box.max = transform->chunk + static_cast<chunk_pos::value_type>(server_game::view_distance.get_value()); + + if(view_box.contains(packet.cpos)) { + if(auto chunk = universe::load_chunk(session->dimension, packet.cpos)) { + protocol::ChunkVoxels response; + response.chunk = packet.cpos; + response.voxels = chunk->get_voxels(); + protocol::send(packet.peer, protocol::encode(response)); + } else { + worldgen::request_chunk(session, packet.cpos); + } + } + } + } +} + +static void on_entity_sound_packet(const protocol::EntitySound& packet) +{ + if(auto session = sessions::find(packet.peer)) { + if(!session->dimension || !session->dimension->entities.valid(session->player_entity)) { + // De-spawned sessions cannot play sounds + return; + } + + protocol::EntitySound response; + response.entity = session->player_entity; + response.sound = packet.sound; + response.looping = packet.looping; + response.pitch = packet.pitch; + + sessions::broadcast(session->dimension, protocol::encode(response), packet.peer); + } +} + +void server_recieve::init(void) +{ + globals::dispatcher.sink<protocol::EntityTransform>().connect<&on_entity_transform_packet>(); + globals::dispatcher.sink<protocol::EntityVelocity>().connect<&on_entity_velocity_packet>(); + globals::dispatcher.sink<protocol::EntityHead>().connect<&on_entity_head_packet>(); + globals::dispatcher.sink<protocol::SetVoxel>().connect<&on_set_voxel_packet>(); + globals::dispatcher.sink<protocol::RequestChunk>().connect<&on_request_chunk_packet>(); + globals::dispatcher.sink<protocol::EntitySound>().connect<&on_entity_sound_packet>(); +} diff --git a/src/game/server/receive.hh b/src/game/server/receive.hh new file mode 100644 index 0000000..57090b5 --- /dev/null +++ b/src/game/server/receive.hh @@ -0,0 +1,10 @@ +#ifndef SERVER_RECEIVE_HH +#define SERVER_RECEIVE_HH 1 +#pragma once + +namespace server_recieve +{ +void init(void); +} // namespace server_recieve + +#endif /* SERVER_RECEIVE_HH */ diff --git a/src/game/server/sessions.cc b/src/game/server/sessions.cc new file mode 100644 index 0000000..43abbb7 --- /dev/null +++ b/src/game/server/sessions.cc @@ -0,0 +1,419 @@ +#include "server/pch.hh" + +#include "server/sessions.hh" + +#include "core/config.hh" +#include "core/constexpr.hh" +#include "core/crc64.hh" +#include "core/strtools.hh" + +#include "shared/chunk.hh" +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/factory.hh" +#include "shared/head.hh" +#include "shared/item_registry.hh" +#include "shared/player.hh" +#include "shared/protocol.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" +#include "shared/voxel_registry.hh" + +#include "server/game.hh" +#include "server/globals.hh" +#include "server/whitelist.hh" + +class DimensionListener final { +public: + explicit DimensionListener(Dimension* dimension); + void on_destroy_entity(const entt::registry& registry, entt::entity entity); + +private: + Dimension* dimension; +}; + +ConfigUnsigned sessions::max_players(8U, 1U, 128U); +unsigned int sessions::num_players = 0U; + +static emhash8::HashMap<std::string, Session*> username_map; +static emhash8::HashMap<std::uint64_t, Session*> identity_map; +static std::vector<DimensionListener> dimension_listeners; +static std::vector<Session> sessions_vector; + +static void on_login_request_packet(const protocol::LoginRequest& packet) +{ + if(packet.version > protocol::VERSION) { + protocol::Disconnect response; + response.reason = "protocol.outdated_server"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(packet.version < protocol::VERSION) { + protocol::Disconnect response; + response.reason = "protocol.outdated_client"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + // FIXME: calculate voxel registry checksum ahead of time + // instead of figuring it out every time a new player connects + if(packet.voxel_registry_checksum != voxel_registry::calcualte_checksum()) { + protocol::Disconnect response; + response.reason = "protocol.voxel_registry_checksum"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(packet.item_registry_checksum != item_registry::calcualte_checksum()) { + protocol::Disconnect response; + response.reason = "protocol.item_registry_checksum"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + // Don't assign new usernames and just kick the player if + // an another client using the same username is already connected + // and playing; since we have a whitelist, adding "(1)" isn't feasible anymore + if(username_map.contains(packet.username)) { + protocol::Disconnect response; + response.reason = "protocol.username_taken"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(whitelist::enabled.get_value()) { + if(!whitelist::contains(packet.username.c_str())) { + protocol::Disconnect response; + response.reason = "protocol.not_whitelisted"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(!whitelist::matches(packet.username.c_str(), packet.password_hash)) { + protocol::Disconnect response; + response.reason = "protocol.password_incorrect"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + } else if(packet.password_hash != server_game::password_hash) { + protocol::Disconnect response; + response.reason = "protocol.password_incorrect"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(Session* session = sessions::create(packet.peer, packet.username.c_str())) { + protocol::LoginResponse response; + response.client_index = session->client_index; + response.client_identity = session->client_identity; + response.server_tickrate = globals::tickrate; + protocol::send(packet.peer, protocol::encode(response)); + + protocol::DimensionInfo dim_info; + dim_info.name = globals::spawn_dimension->get_name(); + dim_info.gravity = globals::spawn_dimension->get_gravity(); + protocol::send(packet.peer, protocol::encode(dim_info)); + + spdlog::info("sessions: {} [{}] logged in with client_index={} in {}", session->client_username, session->client_identity, + session->client_index, globals::spawn_dimension->get_name()); + + // FIXME: only send entities that are present within the current + // player's view bounding box; this also would mean we're not sending + // anything here and just straight up spawing the player and await them + // to receive all the chunks and entites they feel like requesting + for(auto entity : globals::spawn_dimension->entities.view<entt::entity>()) { + if(const auto head = globals::spawn_dimension->entities.try_get<HeadComponent>(entity)) { + protocol::EntityHead head_packet; + head_packet.entity = entity; + head_packet.angles = head->angles; + protocol::send(session->peer, protocol::encode(head_packet)); + } + + if(const auto transform = globals::spawn_dimension->entities.try_get<TransformComponent>(entity)) { + protocol::EntityTransform transform_packet; + transform_packet.entity = entity; + transform_packet.angles = transform->angles; + transform_packet.chunk = transform->chunk; + transform_packet.local = transform->local; + protocol::send(session->peer, protocol::encode(transform_packet)); + } + + if(const auto velocity = globals::spawn_dimension->entities.try_get<VelocityComponent>(entity)) { + protocol::EntityVelocity velocity_packet; + velocity_packet.entity = entity; + velocity_packet.value = velocity->value; + protocol::send(session->peer, protocol::encode(velocity_packet)); + } + + if(globals::spawn_dimension->entities.all_of<PlayerComponent>(entity)) { + protocol::EntityPlayer player_packet; + player_packet.entity = entity; + protocol::send(session->peer, protocol::encode(player_packet)); + } + } + + session->dimension = globals::spawn_dimension; + session->player_entity = globals::spawn_dimension->entities.create(); + shared_factory::create_player(globals::spawn_dimension, session->player_entity); + + const auto& head = globals::spawn_dimension->entities.get<HeadComponent>(session->player_entity); + const auto& transform = globals::spawn_dimension->entities.get<TransformComponent>(session->player_entity); + const auto& velocity = globals::spawn_dimension->entities.get<VelocityComponent>(session->player_entity); + + protocol::EntityHead head_packet; + head_packet.entity = session->player_entity; + head_packet.angles = head.angles; + + protocol::EntityTransform transform_packet; + transform_packet.entity = session->player_entity; + transform_packet.angles = transform.angles; + transform_packet.chunk = transform.chunk; + transform_packet.local = transform.local; + + protocol::EntityVelocity velocity_packet; + velocity_packet.entity = session->player_entity; + velocity_packet.value = velocity.value; + + protocol::EntityPlayer player_packet; + player_packet.entity = session->player_entity; + + protocol::broadcast(globals::server_host, protocol::encode(head_packet)); + protocol::broadcast(globals::server_host, protocol::encode(transform_packet)); + protocol::broadcast(globals::server_host, protocol::encode(velocity_packet)); + protocol::broadcast(globals::server_host, protocol::encode(player_packet)); + + protocol::SpawnPlayer spawn_packet; + spawn_packet.entity = session->player_entity; + + // SpawnPlayer serves a different purpose compared to EntityPlayer + // The latter is used to construct entities (as in "attach a component") + // whilst the SpawnPlayer packet is used to notify client-side that the + // entity identifier in the packet is to be treated as the local player entity + protocol::send(session->peer, protocol::encode(spawn_packet)); + + protocol::ChatMessage message; + message.type = protocol::ChatMessage::PLAYER_JOIN; + message.sender = session->client_username; + message.message = std::string(); + + protocol::broadcast(globals::server_host, protocol::encode(message)); + + sessions::refresh_scoreboard(); + + return; + } + + protocol::Disconnect response; + response.reason = "protocol.server_full"; + protocol::send(packet.peer, protocol::encode(response)); +} + +static void on_disconnect_packet(const protocol::Disconnect& packet) +{ + if(Session* session = sessions::find(packet.peer)) { + protocol::ChatMessage message; + message.type = protocol::ChatMessage::PLAYER_LEAVE; + message.sender = session->client_username; + message.message = packet.reason; + + protocol::broadcast(globals::server_host, protocol::encode(message), session->peer); + + spdlog::info("{} disconnected ({})", session->client_username, packet.reason); + + sessions::destroy(session); + sessions::refresh_scoreboard(); + } +} + +// NOTE: [sessions] is a good place for this since [receive] +// handles entity data sent by players and [sessions] handles +// everything else network related that is not player movement +static void on_voxel_set(const VoxelSetEvent& event) +{ + protocol::SetVoxel packet; + packet.vpos = coord::to_voxel(event.cpos, event.lpos); + packet.voxel = event.voxel; + packet.flags = 0U; // UNDONE + protocol::broadcast(globals::server_host, protocol::encode(packet)); +} + +DimensionListener::DimensionListener(Dimension* dimension) +{ + this->dimension = dimension; +} + +void DimensionListener::on_destroy_entity(const entt::registry& registry, entt::entity entity) +{ + protocol::RemoveEntity packet; + packet.entity = entity; + sessions::broadcast(dimension, protocol::encode(packet)); +} + +void sessions::init(void) +{ + globals::server_config.add_value("sessions.max_players", sessions::max_players); + + globals::dispatcher.sink<protocol::LoginRequest>().connect<&on_login_request_packet>(); + globals::dispatcher.sink<protocol::Disconnect>().connect<&on_disconnect_packet>(); + + globals::dispatcher.sink<VoxelSetEvent>().connect<&on_voxel_set>(); +} + +void sessions::init_late(void) +{ + sessions::num_players = 0U; + + username_map.clear(); + identity_map.clear(); + sessions_vector.resize(sessions::max_players.get_value(), Session()); + + for(unsigned int i = 0U; i < sessions::max_players.get_value(); ++i) { + sessions_vector[i].client_index = UINT16_MAX; + sessions_vector[i].client_identity = UINT64_MAX; + sessions_vector[i].client_username = std::string(); + sessions_vector[i].player_entity = entt::null; + sessions_vector[i].peer = nullptr; + } +} + +void sessions::init_post_universe(void) +{ + for(auto& dimension : globals::dimensions) { + dimension_listeners.push_back(DimensionListener(dimension.second)); + dimension.second->entities.on_destroy<entt::entity>().connect<&DimensionListener::on_destroy_entity>(dimension_listeners.back()); + } +} + +void sessions::deinit(void) +{ + username_map.clear(); + identity_map.clear(); + sessions_vector.clear(); + dimension_listeners.clear(); +} + +Session* sessions::create(ENetPeer* peer, const char* client_username) +{ + for(unsigned int i = 0U; i < sessions::max_players.get_value(); ++i) { + if(!sessions_vector[i].peer) { + std::uint64_t client_identity = crc64::get(client_username); + + sessions_vector[i].client_index = i; + sessions_vector[i].client_identity = client_identity; + sessions_vector[i].client_username = client_username; + sessions_vector[i].player_entity = entt::null; + sessions_vector[i].peer = peer; + + username_map[client_username] = &sessions_vector[i]; + identity_map[client_identity] = &sessions_vector[i]; + + peer->data = &sessions_vector[i]; + + sessions::num_players += 1U; + + return &sessions_vector[i]; + } + } + + return nullptr; +} + +Session* sessions::find(const char* client_username) +{ + const auto it = username_map.find(client_username); + if(it != username_map.cend()) { + return it->second; + } else { + return nullptr; + } +} + +Session* sessions::find(std::uint16_t client_index) +{ + if(client_index < sessions_vector.size()) { + if(!sessions_vector[client_index].peer) { + return nullptr; + } else { + return &sessions_vector[client_index]; + } + } + + return nullptr; +} + +Session* sessions::find(std::uint64_t client_identity) +{ + const auto it = identity_map.find(client_identity); + + if(it != identity_map.cend()) { + return it->second; + } else { + return nullptr; + } +} + +Session* sessions::find(ENetPeer* peer) +{ + if(peer != nullptr) { + return reinterpret_cast<Session*>(peer->data); + } else { + return nullptr; + } +} + +void sessions::destroy(Session* session) +{ + if(session) { + if(session->peer) { + // Make sure we don't leave a mark + session->peer->data = nullptr; + } + + if(session->dimension) { + session->dimension->entities.destroy(session->player_entity); + } + + username_map.erase(session->client_username); + identity_map.erase(session->client_identity); + + session->client_index = UINT16_MAX; + session->client_identity = UINT64_MAX; + session->client_username = std::string(); + session->player_entity = entt::null; + session->peer = nullptr; + + sessions::num_players -= 1U; + } +} + +void sessions::broadcast(const Dimension* dimension, ENetPacket* packet) +{ + for(const auto& session : sessions_vector) { + if(session.peer && (session.dimension == dimension)) { + enet_peer_send(session.peer, protocol::CHANNEL, packet); + } + } +} + +void sessions::broadcast(const Dimension* dimension, ENetPacket* packet, ENetPeer* except) +{ + for(const auto& session : sessions_vector) { + if(session.peer && (session.peer != except)) { + enet_peer_send(session.peer, protocol::CHANNEL, packet); + } + } +} + +void sessions::refresh_scoreboard(void) +{ + protocol::ScoreboardUpdate packet; + + for(std::size_t i = 0; i < sessions::max_players.get_value(); ++i) { + if(sessions_vector[i].peer) { + packet.names.push_back(sessions_vector[i].client_username); + } + } + + protocol::broadcast(globals::server_host, protocol::encode(packet)); +} diff --git a/src/game/server/sessions.hh b/src/game/server/sessions.hh new file mode 100644 index 0000000..b9a6348 --- /dev/null +++ b/src/game/server/sessions.hh @@ -0,0 +1,53 @@ +#ifndef SERVER_SESSIONS_HH +#define SERVER_SESSIONS_HH 1 +#pragma once + +class Dimension; + +class ConfigUnsigned; + +struct Session final { + std::uint16_t client_index; + std::uint64_t client_identity; + std::string client_username; + entt::entity player_entity; + Dimension* dimension; + ENetPeer* peer; +}; + +namespace sessions +{ +extern ConfigUnsigned max_players; +extern unsigned int num_players; +} // namespace sessions + +namespace sessions +{ +void init(void); +void init_late(void); +void init_post_universe(void); +void deinit(void); +} // namespace sessions + +namespace sessions +{ +Session* create(ENetPeer* peer, const char* client_username); +Session* find(const char* client_username); +Session* find(std::uint16_t client_index); +Session* find(std::uint64_t client_identity); +Session* find(ENetPeer* peer); +void destroy(Session* session); +} // namespace sessions + +namespace sessions +{ +void broadcast(const Dimension* dimension, ENetPacket* packet); +void broadcast(const Dimension* dimension, ENetPacket* packet, ENetPeer* except); +} // namespace sessions + +namespace sessions +{ +void refresh_scoreboard(void); +} // namespace sessions + +#endif /* SERVER_SESSIONS_HH */ diff --git a/src/game/server/status.cc b/src/game/server/status.cc new file mode 100644 index 0000000..6b64719 --- /dev/null +++ b/src/game/server/status.cc @@ -0,0 +1,26 @@ +#include "server/pch.hh" + +#include "server/status.hh" + +#include "core/config.hh" + +#include "shared/protocol.hh" +#include "shared/splash.hh" + +#include "server/globals.hh" +#include "server/sessions.hh" + +static void on_status_request_packet(const protocol::StatusRequest& packet) +{ + protocol::StatusResponse response; + response.version = protocol::VERSION; + response.max_players = sessions::max_players.get_value(); + response.num_players = sessions::num_players; + response.motd = splash::get(); + protocol::send(packet.peer, protocol::encode(response)); +} + +void status::init(void) +{ + globals::dispatcher.sink<protocol::StatusRequest>().connect<&on_status_request_packet>(); +} diff --git a/src/game/server/status.hh b/src/game/server/status.hh new file mode 100644 index 0000000..5f939f7 --- /dev/null +++ b/src/game/server/status.hh @@ -0,0 +1,10 @@ +#ifndef SERVER_STATUS_HH +#define SERVER_STATUS_HH 1 +#pragma once + +namespace status +{ +void init(void); +} // namespace status + +#endif /* SERVER_STATUS_HH */ diff --git a/src/game/server/universe.cc b/src/game/server/universe.cc new file mode 100644 index 0000000..96884cf --- /dev/null +++ b/src/game/server/universe.cc @@ -0,0 +1,215 @@ +#include "server/pch.hh" + +#include "server/universe.hh" + +#include "core/buffer.hh" +#include "core/config.hh" +#include "core/epoch.hh" + +#include "shared/chunk.hh" +#include "shared/dimension.hh" + +#include "server/globals.hh" +#include "server/inhabited.hh" +#include "server/overworld.hh" + +struct DimensionMetadata final { + std::string config_path; + std::string zvox_dir; + Config config; +}; + +static ConfigString universe_name("save"); + +static Config universe_config; +static ConfigUnsigned64 universe_config_seed; +static ConfigString universe_spawn_dimension("world"); + +static std::string universe_config_path; +static std::unordered_map<Dimension*, DimensionMetadata*> metadata_map; + +static std::string make_chunk_filename(const DimensionMetadata* metadata, const chunk_pos& cpos) +{ + const auto unsigned_x = static_cast<std::uint32_t>(cpos.x); + const auto unsigned_y = static_cast<std::uint32_t>(cpos.y); + const auto unsigned_z = static_cast<std::uint32_t>(cpos.z); + return std::format("{}/{:08X}-{:08X}-{:08X}.zvox", metadata->zvox_dir, unsigned_x, unsigned_y, unsigned_z); +} + +static void add_new_dimension(Dimension* dimension) +{ + if(globals::dimensions.count(dimension->get_name())) { + spdlog::critical("universe: dimension named {} already exists", dimension->get_name()); + std::terminate(); + } + + auto dimension_dir = std::format("{}/{}", universe_name.get(), dimension->get_name()); + + if(!PHYSFS_mkdir(dimension_dir.c_str())) { + spdlog::critical("universe: {}: {}", dimension_dir, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + std::terminate(); + } + + auto metadata = new DimensionMetadata; + metadata->config_path = std::format("{}/dimension.conf", dimension_dir); + metadata->zvox_dir = std::format("{}/chunk", dimension_dir); + + if(!PHYSFS_mkdir(metadata->zvox_dir.c_str())) { + spdlog::critical("universe: {}: {}", metadata->zvox_dir, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + std::terminate(); + } + + globals::dimensions.insert_or_assign(dimension->get_name(), dimension); + + auto& mapped_metadata = metadata_map.insert_or_assign(dimension, metadata).first->second; + + dimension->init(mapped_metadata->config); + + mapped_metadata->config.load_file(mapped_metadata->config_path.c_str()); + + dimension->init_late(universe_config_seed.get_value()); +} + +static void internal_save_chunk(const DimensionMetadata* metadata, const Dimension* dimension, const chunk_pos& cpos, const Chunk* chunk) +{ + auto path = make_chunk_filename(metadata, cpos); + + WriteBuffer buffer; + chunk->get_voxels().serialize(buffer); + + if(auto file = buffer.to_file(path.c_str())) { + PHYSFS_close(file); + return; + } +} + +void universe::init(void) +{ + // If the world is newly created, the seed will + // be chosed based on the current system's view on UNIX time + universe_config_seed.set_value(epoch::microseconds()); + + // We're going to read files from directory named with + // the value of this config value. Since config is also + // read from command line, the [--universe <name>] parameter still works + globals::server_config.add_value("universe", universe_name); + + universe_config.add_value("global_seed", universe_config_seed); + universe_config.add_value("spawn_dimension", universe_spawn_dimension); +} + +void universe::init_late(void) +{ + const auto universe_dir = std::string(universe_name.get()); + + if(!PHYSFS_mkdir(universe_dir.c_str())) { + spdlog::critical("universe: {}: {}", universe_dir, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + std::terminate(); + } + + universe_config_path = std::format("{}/universe.conf", universe_dir); + universe_config.load_file(universe_config_path.c_str()); + + add_new_dimension(new Overworld("world")); + + // UNDONE: lua scripts to setup dimensions + if(globals::dimensions.empty()) { + spdlog::critical("universe: no dimensions"); + std::terminate(); + } + + auto spawn_dimension = globals::dimensions.find(universe_spawn_dimension.get()); + + if(spawn_dimension == globals::dimensions.cend()) { + spdlog::critical("universe: {} is not a valid dimension name", universe_spawn_dimension.get()); + std::terminate(); + } + + globals::spawn_dimension = spawn_dimension->second; +} + +void universe::deinit(void) +{ + for(const auto metadata : metadata_map) { + metadata.second->config.save_file(metadata.second->config_path.c_str()); + delete metadata.second; + } + + metadata_map.clear(); + + for(const auto dimension : globals::dimensions) { + universe::save_all_chunks(dimension.second); + delete dimension.second; + } + + globals::dimensions.clear(); + globals::spawn_dimension = nullptr; + + universe_config.save_file(universe_config_path.c_str()); +} + +Chunk* universe::load_chunk(Dimension* dimension, const chunk_pos& cpos) +{ + if(auto chunk = dimension->find_chunk(cpos)) { + // Just return the existing chunk which is + // most probable to be up to date compared to + // whatever the hell is currently stored on disk + return chunk; + } + + auto metadata = metadata_map.find(dimension); + + if(metadata == metadata_map.cend()) { + // The dimension is for sure a weird one + return nullptr; + } + + if(auto file = PHYSFS_openRead(make_chunk_filename(metadata->second, cpos).c_str())) { + VoxelStorage voxels; + ReadBuffer buffer(file); + voxels.deserialize(buffer); + + PHYSFS_close(file); + + auto chunk = dimension->create_chunk(cpos); + chunk->set_voxels(voxels); + + // Make sure we're going to save it later + dimension->chunks.emplace_or_replace<InhabitedComponent>(chunk->get_entity()); + + return chunk; + } + + return nullptr; +} + +void universe::save_chunk(Dimension* dimension, const chunk_pos& cpos) +{ + auto metadata = metadata_map.find(dimension); + + if(metadata == metadata_map.cend()) { + // Cannot save a chunk in a dimension + // that doesn't have a metadata struct + return; + } + + if(auto chunk = dimension->find_chunk(cpos)) { + internal_save_chunk(metadata->second, dimension, cpos, chunk); + } +} + +void universe::save_all_chunks(Dimension* dimension) +{ + auto group = dimension->chunks.group(entt::get<ChunkComponent, InhabitedComponent>); + auto metadata = metadata_map.find(dimension); + + if(metadata == metadata_map.cend()) { + // Cannot save a chunk in a dimension + // that doesn't have a metadata struct + return; + } + + for(auto [entity, chunk] : group.each()) { + internal_save_chunk(metadata->second, dimension, chunk.cpos, chunk.chunk); + } +} diff --git a/src/game/server/universe.hh b/src/game/server/universe.hh new file mode 100644 index 0000000..2a16806 --- /dev/null +++ b/src/game/server/universe.hh @@ -0,0 +1,25 @@ +#ifndef SERVER_UNIVERSE_HH +#define SERVER_UNIVERSE_HH 1 +#pragma once + +#include "shared/types.hh" + +class Chunk; +class Dimension; +class Session; + +namespace universe +{ +void init(void); +void init_late(void); +void deinit(void); +} // namespace universe + +namespace universe +{ +Chunk* load_chunk(Dimension* dimension, const chunk_pos& cpos); +void save_chunk(Dimension* dimension, const chunk_pos& cpos); +void save_all_chunks(Dimension* dimension); +} // namespace universe + +#endif /* SERVER_UNIVERSE_HH */ diff --git a/src/game/server/unloader.cc b/src/game/server/unloader.cc new file mode 100644 index 0000000..f986a61 --- /dev/null +++ b/src/game/server/unloader.cc @@ -0,0 +1,76 @@ +#include "server/pch.hh" + +#include "server/unloader.hh" + +#include "core/config.hh" + +#include "shared/chunk.hh" +#include "shared/chunk_aabb.hh" +#include "shared/dimension.hh" +#include "shared/player.hh" +#include "shared/transform.hh" + +#include "server/game.hh" +#include "server/globals.hh" +#include "server/inhabited.hh" +#include "server/universe.hh" + +static void on_chunk_update(const ChunkUpdateEvent& event) +{ + event.dimension->chunks.emplace_or_replace<InhabitedComponent>(event.chunk->get_entity()); +} + +static void on_voxel_set(const VoxelSetEvent& event) +{ + event.dimension->chunks.emplace_or_replace<InhabitedComponent>(event.chunk->get_entity()); +} + +void unloader::init(void) +{ + globals::dispatcher.sink<ChunkUpdateEvent>().connect<&on_chunk_update>(); + globals::dispatcher.sink<VoxelSetEvent>().connect<&on_voxel_set>(); +} + +void unloader::init_late(void) +{ +} + +void unloader::fixed_update_late(Dimension* dimension) +{ + auto group = dimension->entities.group(entt::get<PlayerComponent, TransformComponent>); + auto boxes = std::vector<ChunkAABB>(); + + for(const auto [entity, transform] : group.each()) { + ChunkAABB aabb; + aabb.min = transform.chunk - static_cast<chunk_pos::value_type>(server_game::view_distance.get_value()); + aabb.max = transform.chunk + static_cast<chunk_pos::value_type>(server_game::view_distance.get_value()); + boxes.push_back(aabb); + } + + auto view = dimension->chunks.view<ChunkComponent>(); + auto chunk_in_view = false; + + for(const auto [entity, chunk] : view.each()) { + chunk_in_view = false; + + for(const auto& aabb : boxes) { + if(aabb.contains(chunk.cpos)) { + chunk_in_view = true; + break; + } + } + + if(chunk_in_view) { + // The chunk is within view box of at least + // a single player; we shouldn't unload it now + continue; + } + + if(dimension->chunks.any_of<InhabitedComponent>(entity)) { + // Only store inhabited chunks on disk + universe::save_chunk(dimension, chunk.cpos); + } + + dimension->remove_chunk(entity); + } +} diff --git a/src/game/server/unloader.hh b/src/game/server/unloader.hh new file mode 100644 index 0000000..414cdc4 --- /dev/null +++ b/src/game/server/unloader.hh @@ -0,0 +1,14 @@ +#ifndef SERVER_UNLOADER_HH +#define SERVER_UNLOADER_HH 1 +#pragma once + +class Dimension; + +namespace unloader +{ +void init(void); +void init_late(void); +void fixed_update_late(Dimension* dimension); +} // namespace unloader + +#endif /* SERVER_UNLOADER_HH */ diff --git a/src/game/server/vserver.ico b/src/game/server/vserver.ico Binary files differnew file mode 100644 index 0000000..02ff006 --- /dev/null +++ b/src/game/server/vserver.ico diff --git a/src/game/server/vserver.rc b/src/game/server/vserver.rc new file mode 100644 index 0000000..b6828bf --- /dev/null +++ b/src/game/server/vserver.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "vserver.ico" diff --git a/src/game/server/whitelist.cc b/src/game/server/whitelist.cc new file mode 100644 index 0000000..6635122 --- /dev/null +++ b/src/game/server/whitelist.cc @@ -0,0 +1,95 @@ +#include "server/pch.hh" + +#include "server/whitelist.hh" + +#include "core/config.hh" +#include "core/crc64.hh" +#include "core/strtools.hh" + +#include "server/game.hh" +#include "server/globals.hh" + +constexpr static const char* DEFAULT_FILENAME = "whitelist.txt"; +constexpr static char SEPARATOR_CHAR = ':'; + +ConfigBoolean whitelist::enabled(false); +ConfigString whitelist::filename(DEFAULT_FILENAME); + +static emhash8::HashMap<std::string, std::uint64_t> whitelist_map; + +void whitelist::init(void) +{ + globals::server_config.add_value("whitelist.enabled", whitelist::enabled); + globals::server_config.add_value("whitelist.filename", whitelist::filename); +} + +void whitelist::init_late(void) +{ + whitelist_map.clear(); + + if(!whitelist::enabled.get_value()) { + // Not enabled, shouldn't + // even bother with parsing + // the whitelist file + return; + } + + if(strtools::is_whitespace(whitelist::filename.get())) { + spdlog::warn("whitelist: enabled but filename is empty, using default ({})", DEFAULT_FILENAME); + whitelist::filename.set(DEFAULT_FILENAME); + } + + PHYSFS_File* file = PHYSFS_openRead(whitelist::filename.get()); + + if(file == nullptr) { + spdlog::warn("whitelist: {}: {}", whitelist::filename.get(), PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + whitelist::enabled.set_value(false); + return; + } + + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + std::istringstream stream(source); + std::string line; + + while(std::getline(stream, line)) { + const auto location = line.find_last_of(SEPARATOR_CHAR); + + if(location == std::string::npos) { + // Entries that don't define a password field default + // to the global server password; this allows easier adding + // of guest accounts which can later be edited to use a better password + whitelist_map[line] = server_game::password_hash; + } else { + const auto username = line.substr(0, location); + const auto password = line.substr(location + 1); + whitelist_map[username] = crc64::get(password); + } + } + + PHYSFS_close(file); +} + +void whitelist::deinit(void) +{ + // UNDONE: implement saving +} + +bool whitelist::contains(const char* username) +{ + return whitelist_map.contains(username); +} + +bool whitelist::matches(const char* username, std::uint64_t password_hash) +{ + const auto it = whitelist_map.find(username); + + if(it == whitelist_map.cend()) { + // Not whitelisted, no match + return false; + } + + return it->second == password_hash; +} diff --git a/src/game/server/whitelist.hh b/src/game/server/whitelist.hh new file mode 100644 index 0000000..fe7656b --- /dev/null +++ b/src/game/server/whitelist.hh @@ -0,0 +1,27 @@ +#ifndef SERVER_WHITELIST_HH +#define SERVER_WHITELIST_HH 1 +#pragma once + +class ConfigBoolean; +class ConfigString; + +namespace whitelist +{ +extern ConfigBoolean enabled; +extern ConfigString filename; +} // namespace whitelist + +namespace whitelist +{ +void init(void); +void init_late(void); +void deinit(void); +} // namespace whitelist + +namespace whitelist +{ +bool contains(const char* username); +bool matches(const char* username, std::uint64_t password_hash); +} // namespace whitelist + +#endif /* SERVER_WHITELIST_HH */ diff --git a/src/game/server/worldgen.cc b/src/game/server/worldgen.cc new file mode 100644 index 0000000..5c74d47 --- /dev/null +++ b/src/game/server/worldgen.cc @@ -0,0 +1,148 @@ +#include "server/pch.hh" + +#include "server/worldgen.hh" + +#include "core/cmdline.hh" + +#include "shared/chunk.hh" +#include "shared/dimension.hh" +#include "shared/protocol.hh" +#include "shared/threading.hh" + +#include "server/globals.hh" +#include "server/inhabited.hh" +#include "server/sessions.hh" + +static bool aggressive_caching; + +static emhash8::HashMap<Dimension*, emhash8::HashMap<chunk_pos, std::unordered_set<Session*>>> active_tasks; + +class WorldgenTask final : public Task { +public: + explicit WorldgenTask(Dimension* dimension, const chunk_pos& cpos); + virtual ~WorldgenTask(void) = default; + virtual void process(void) override; + virtual void finalize(void) override; + +private: + Dimension* m_dimension; + VoxelStorage m_voxels; + chunk_pos m_cpos; +}; + +WorldgenTask::WorldgenTask(Dimension* dimension, const chunk_pos& cpos) +{ + m_dimension = dimension; + m_voxels.fill(rand()); // trolling + m_cpos = cpos; +} + +void WorldgenTask::process(void) +{ + if(!m_dimension->generate(m_cpos, m_voxels)) { + set_status(task_status::CANCELLED); + } +} + +void WorldgenTask::finalize(void) +{ + auto dim_tasks = active_tasks.find(m_dimension); + + if(dim_tasks == active_tasks.cend()) { + // Normally this should never happen but + // one can never be sure about anything + // when that anything is threaded out + return; + } + + auto it = dim_tasks->second.find(m_cpos); + + if(it == dim_tasks->second.cend()) { + // Normally this should never happen but + // one can never be sure about anything + // when that anything is threaded out + return; + } + + auto chunk = m_dimension->create_chunk(m_cpos); + chunk->set_voxels(m_voxels); + + if(aggressive_caching) { + // Marking the chunk with InhabitedComponent makes + // it so that it is saved regardles of whether it was + // modified by players or not. This isn't particularly + // good for server-side disk usage but it might improve performance + m_dimension->chunks.emplace<InhabitedComponent>(chunk->get_entity()); + } + + protocol::ChunkVoxels response; + response.voxels = m_voxels; + response.chunk = m_cpos; + + auto packet = protocol::encode(response); + + for(auto session : it->second) { + if(session->peer) { + // Respond with the voxels to every session + // that has requested this specific chunk for this dimension + enet_peer_send(session->peer, protocol::CHANNEL, packet); + } + } + + dim_tasks->second.erase(it); + + if(dim_tasks->second.empty()) { + // There are no more requests + // to generate a chunk for that + // dimension, at least for now + active_tasks.erase(dim_tasks); + } +} + +void worldgen::init(void) +{ + aggressive_caching = cmdline::contains("aggressive-caching"); +} + +bool worldgen::is_generating(Dimension* dimension, const chunk_pos& cpos) +{ + auto dim_tasks = active_tasks.find(dimension); + + if(dim_tasks == active_tasks.cend()) { + // No tasks for this dimension + return false; + } + + auto it = dim_tasks->second.find(cpos); + + if(it == dim_tasks->second.cend()) { + // Not generating this chunk + return false; + } + + return true; +} + +void worldgen::request_chunk(Session* session, const chunk_pos& cpos) +{ + if(session->dimension) { + auto dim_tasks = active_tasks.find(session->dimension); + + if(dim_tasks == active_tasks.cend()) { + dim_tasks = active_tasks.emplace(session->dimension, emhash8::HashMap<chunk_pos, std::unordered_set<Session*>>()).first; + } + + auto it = dim_tasks->second.find(cpos); + + if(it == dim_tasks->second.cend()) { + auto& sessions = dim_tasks->second.insert_or_assign(cpos, std::unordered_set<Session*>()).first->second; + sessions.insert(session); + + threading::submit<WorldgenTask>(session->dimension, cpos); + + return; + } + + it->second.insert(session); + } +} diff --git a/src/game/server/worldgen.hh b/src/game/server/worldgen.hh new file mode 100644 index 0000000..b0127e5 --- /dev/null +++ b/src/game/server/worldgen.hh @@ -0,0 +1,21 @@ +#ifndef SERVER_WORLDGEN_HH +#define SERVER_WORLDGEN_HH 1 +#pragma once + +#include "shared/types.hh" + +class Dimension; +class Session; + +namespace worldgen +{ +void init(void); +} // namespace worldgen + +namespace worldgen +{ +bool is_generating(Dimension* dimension, const chunk_pos& cpos); +void request_chunk(Session* session, const chunk_pos& cpos); +} // namespace worldgen + +#endif /* SERVER_WORLDGEN_HH */ diff --git a/src/game/shared/CMakeLists.txt b/src/game/shared/CMakeLists.txt new file mode 100644 index 0000000..d72a6e8 --- /dev/null +++ b/src/game/shared/CMakeLists.txt @@ -0,0 +1,57 @@ +add_library(shared STATIC + "${CMAKE_CURRENT_LIST_DIR}/chunk_aabb.cc" + "${CMAKE_CURRENT_LIST_DIR}/chunk_aabb.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk.cc" + "${CMAKE_CURRENT_LIST_DIR}/chunk.hh" + "${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt" + "${CMAKE_CURRENT_LIST_DIR}/collision.cc" + "${CMAKE_CURRENT_LIST_DIR}/collision.hh" + "${CMAKE_CURRENT_LIST_DIR}/const.hh" + "${CMAKE_CURRENT_LIST_DIR}/coord.hh" + "${CMAKE_CURRENT_LIST_DIR}/dimension.cc" + "${CMAKE_CURRENT_LIST_DIR}/dimension.hh" + "${CMAKE_CURRENT_LIST_DIR}/factory.cc" + "${CMAKE_CURRENT_LIST_DIR}/factory.hh" + "${CMAKE_CURRENT_LIST_DIR}/feature.cc" + "${CMAKE_CURRENT_LIST_DIR}/feature.hh" + "${CMAKE_CURRENT_LIST_DIR}/game_items.cc" + "${CMAKE_CURRENT_LIST_DIR}/game_items.hh" + "${CMAKE_CURRENT_LIST_DIR}/game_voxels.cc" + "${CMAKE_CURRENT_LIST_DIR}/game_voxels.hh" + "${CMAKE_CURRENT_LIST_DIR}/game.cc" + "${CMAKE_CURRENT_LIST_DIR}/game.hh" + "${CMAKE_CURRENT_LIST_DIR}/globals.cc" + "${CMAKE_CURRENT_LIST_DIR}/globals.hh" + "${CMAKE_CURRENT_LIST_DIR}/gravity.cc" + "${CMAKE_CURRENT_LIST_DIR}/gravity.hh" + "${CMAKE_CURRENT_LIST_DIR}/grounded.hh" + "${CMAKE_CURRENT_LIST_DIR}/head.hh" + "${CMAKE_CURRENT_LIST_DIR}/item_registry.cc" + "${CMAKE_CURRENT_LIST_DIR}/item_registry.hh" + "${CMAKE_CURRENT_LIST_DIR}/pch.hh" + "${CMAKE_CURRENT_LIST_DIR}/player.hh" + "${CMAKE_CURRENT_LIST_DIR}/protocol.cc" + "${CMAKE_CURRENT_LIST_DIR}/protocol.hh" + "${CMAKE_CURRENT_LIST_DIR}/ray_dda.cc" + "${CMAKE_CURRENT_LIST_DIR}/ray_dda.hh" + "${CMAKE_CURRENT_LIST_DIR}/splash.cc" + "${CMAKE_CURRENT_LIST_DIR}/splash.hh" + "${CMAKE_CURRENT_LIST_DIR}/stasis.cc" + "${CMAKE_CURRENT_LIST_DIR}/stasis.hh" + "${CMAKE_CURRENT_LIST_DIR}/threading.cc" + "${CMAKE_CURRENT_LIST_DIR}/threading.hh" + "${CMAKE_CURRENT_LIST_DIR}/transform.cc" + "${CMAKE_CURRENT_LIST_DIR}/transform.hh" + "${CMAKE_CURRENT_LIST_DIR}/types.hh" + "${CMAKE_CURRENT_LIST_DIR}/velocity.cc" + "${CMAKE_CURRENT_LIST_DIR}/velocity.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel_registry.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel_registry.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel_storage.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel_storage.hh") +target_compile_features(shared PUBLIC cxx_std_20) +target_include_directories(shared PUBLIC "${DEPS_INCLUDE_DIR}") +target_include_directories(shared PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_include_directories(shared PRIVATE "${PROJECT_SOURCE_DIR}/src/game") +target_precompile_headers(shared PRIVATE "${CMAKE_CURRENT_LIST_DIR}/pch.hh") +target_link_libraries(shared PUBLIC core enet entt FNL miniz parson thread_pool) diff --git a/src/game/shared/chunk.cc b/src/game/shared/chunk.cc new file mode 100644 index 0000000..da798fa --- /dev/null +++ b/src/game/shared/chunk.cc @@ -0,0 +1,69 @@ +#include "shared/pch.hh" + +#include "shared/chunk.hh" + +#include "shared/coord.hh" + +Chunk::Chunk(entt::entity entity, Dimension* dimension) +{ + m_entity = entity; + m_dimension = dimension; + m_voxels.fill(NULL_VOXEL_ID); + m_biome = BIOME_VOID; +} + +voxel_id Chunk::get_voxel(const local_pos& lpos) const +{ + return get_voxel(coord::to_index(lpos)); +} + +voxel_id Chunk::get_voxel(const std::size_t index) const +{ + if(index >= CHUNK_VOLUME) { + return NULL_VOXEL_ID; + } else { + return m_voxels[index]; + } +} + +void Chunk::set_voxel(voxel_id voxel, const local_pos& lpos) +{ + set_voxel(voxel, coord::to_index(lpos)); +} + +void Chunk::set_voxel(voxel_id voxel, const std::size_t index) +{ + if(index < CHUNK_VOLUME) { + m_voxels[index] = voxel; + } +} + +const VoxelStorage& Chunk::get_voxels(void) const +{ + return m_voxels; +} + +void Chunk::set_voxels(const VoxelStorage& voxels) +{ + m_voxels = voxels; +} + +unsigned int Chunk::get_biome(void) const +{ + return m_biome; +} + +void Chunk::set_biome(unsigned int biome) +{ + m_biome = biome; +} + +entt::entity Chunk::get_entity(void) const +{ + return m_entity; +} + +Dimension* Chunk::get_dimension(void) const +{ + return m_dimension; +} diff --git a/src/game/shared/chunk.hh b/src/game/shared/chunk.hh new file mode 100644 index 0000000..67bedae --- /dev/null +++ b/src/game/shared/chunk.hh @@ -0,0 +1,39 @@ +#ifndef SHARED_CHUNK_HH +#define SHARED_CHUNK_HH 1 +#pragma once + +#include "shared/types.hh" +#include "shared/voxel_storage.hh" + +constexpr static unsigned int BIOME_VOID = 0U; + +class Dimension; + +class Chunk final { +public: + explicit Chunk(entt::entity entity, Dimension* dimension); + virtual ~Chunk(void) = default; + + voxel_id get_voxel(const local_pos& lpos) const; + voxel_id get_voxel(const std::size_t index) const; + + void set_voxel(voxel_id voxel, const local_pos& lpos); + void set_voxel(voxel_id voxel, const std::size_t index); + + const VoxelStorage& get_voxels(void) const; + void set_voxels(const VoxelStorage& voxels); + + unsigned int get_biome(void) const; + void set_biome(unsigned int biome); + + entt::entity get_entity(void) const; + Dimension* get_dimension(void) const; + +private: + entt::entity m_entity; + Dimension* m_dimension; + VoxelStorage m_voxels; + unsigned int m_biome; +}; + +#endif /* SHARED_CHUNK_HH */ diff --git a/src/game/shared/chunk_aabb.cc b/src/game/shared/chunk_aabb.cc new file mode 100644 index 0000000..5f23ea9 --- /dev/null +++ b/src/game/shared/chunk_aabb.cc @@ -0,0 +1,54 @@ +#include "shared/pch.hh" + +#include "shared/chunk_aabb.hh" + +void ChunkAABB::set_bounds(const chunk_pos& min, const chunk_pos& max) +{ + this->min = min; + this->max = max; +} + +void ChunkAABB::set_offset(const chunk_pos& base, const chunk_pos& size) +{ + this->min = base; + this->max = base + size; +} + +bool ChunkAABB::contains(const chunk_pos& point) const +{ + auto result = true; + result = result && (point.x >= min.x) && (point.x <= max.x); + result = result && (point.y >= min.y) && (point.y <= max.y); + result = result && (point.z >= min.z) && (point.z <= max.z); + return result; +} + +bool ChunkAABB::intersect(const ChunkAABB& other_box) const +{ + auto result = true; + result = result && (min.x < other_box.max.x) && (max.x > other_box.min.x); + result = result && (min.y < other_box.max.y) && (max.y > other_box.min.y); + result = result && (min.z < other_box.max.z) && (max.z > other_box.min.z); + return result; +} + +ChunkAABB ChunkAABB::combine_with(const ChunkAABB& other_box) const +{ + ChunkAABB result; + result.set_bounds(min, other_box.max); + return result; +} + +ChunkAABB ChunkAABB::multiply_with(const ChunkAABB& other_box) const +{ + ChunkAABB result; + result.set_bounds(other_box.min, max); + return result; +} + +ChunkAABB ChunkAABB::push(const chunk_pos& vector) const +{ + ChunkAABB result; + result.set_bounds(min + vector, max + vector); + return result; +} diff --git a/src/game/shared/chunk_aabb.hh b/src/game/shared/chunk_aabb.hh new file mode 100644 index 0000000..7a35dd4 --- /dev/null +++ b/src/game/shared/chunk_aabb.hh @@ -0,0 +1,32 @@ +#ifndef SHARED_CHUNK_AABB +#define SHARED_CHUNK_AABB 1 +#pragma once + +#include "core/macros.hh" + +#include "shared/types.hh" + +class ChunkAABB final { +public: + DECLARE_DEFAULT_CONSTRUCTOR(ChunkAABB); + virtual ~ChunkAABB(void) = default; + + void set_bounds(const chunk_pos& min, const chunk_pos& max); + void set_offset(const chunk_pos& base, const chunk_pos& size); + + const chunk_pos& get_min(void) const; + const chunk_pos& get_max(void) const; + + bool contains(const chunk_pos& point) const; + bool intersect(const ChunkAABB& other_box) const; + + ChunkAABB combine_with(const ChunkAABB& other_box) const; + ChunkAABB multiply_with(const ChunkAABB& other_box) const; + ChunkAABB push(const chunk_pos& vector) const; + +public: + chunk_pos min; + chunk_pos max; +}; + +#endif /* SHARED_CHUNK_AABB */ diff --git a/src/game/shared/collision.cc b/src/game/shared/collision.cc new file mode 100644 index 0000000..dd51dd5 --- /dev/null +++ b/src/game/shared/collision.cc @@ -0,0 +1,169 @@ +#include "shared/pch.hh" + +#include "shared/collision.hh" + +#include "core/constexpr.hh" + +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/globals.hh" +#include "shared/gravity.hh" +#include "shared/grounded.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" +#include "shared/voxel_registry.hh" + +static int vgrid_collide(const Dimension* dimension, int d, CollisionComponent& collision, TransformComponent& transform, + VelocityComponent& velocity, voxel_surface& touch_surface) +{ + const auto move = globals::fixed_frametime * velocity.value[d]; + const auto move_sign = vx::sign<int>(move); + + const auto& ref_aabb = collision.aabb; + const auto current_aabb = ref_aabb.push(transform.local); + + auto next_aabb = AABB(current_aabb); + next_aabb.min[d] += move; + next_aabb.max[d] += move; + + local_pos lpos_min; + lpos_min.x = vx::floor<local_pos::value_type>(next_aabb.min.x); + lpos_min.y = vx::floor<local_pos::value_type>(next_aabb.min.y); + lpos_min.z = vx::floor<local_pos::value_type>(next_aabb.min.z); + + local_pos lpos_max; + lpos_max.x = vx::ceil<local_pos::value_type>(next_aabb.max.x); + lpos_max.y = vx::ceil<local_pos::value_type>(next_aabb.max.y); + lpos_max.z = vx::ceil<local_pos::value_type>(next_aabb.max.z); + + // Other axes + const int u = (d + 1) % 3; + const int v = (d + 2) % 3; + + local_pos::value_type ddir; + local_pos::value_type dmin; + local_pos::value_type dmax; + + if(move < 0.0f) { + ddir = local_pos::value_type(+1); + dmin = lpos_min[d]; + dmax = lpos_max[d]; + } else { + ddir = local_pos::value_type(-1); + dmin = lpos_max[d]; + dmax = lpos_min[d]; + } + + voxel_touch latch_touch = voxel_touch::NOTHING; + glm::fvec3 latch_values = glm::fvec3(0.0f, 0.0f, 0.0f); + voxel_surface latch_surface = voxel_surface::UNKNOWN; + AABB latch_vbox; + + for(auto i = dmin; i != dmax; i += ddir) { + for(auto j = lpos_min[u]; j < lpos_max[u]; ++j) + for(auto k = lpos_min[v]; k < lpos_max[v]; ++k) { + local_pos lpos; + lpos[d] = i; + lpos[u] = j; + lpos[v] = k; + + const auto vpos = coord::to_voxel(transform.chunk, lpos); + const auto info = voxel_registry::find(dimension->get_voxel(vpos)); + + if(info == nullptr) { + // Don't collide with something + // that we assume to be nothing + continue; + } + + AABB vbox; + vbox.min = glm::fvec3(lpos); + vbox.max = glm::fvec3(lpos) + 1.0f; + + if(!next_aabb.intersect(vbox)) { + // No intersection between the voxel + // and the entity's collision hull + continue; + } + + if(info->touch_type == voxel_touch::SOLID) { + // Solid touch type makes a collision + // response whenever it is encountered + velocity.value[d] = 0.0f; + touch_surface = info->surface; + return move_sign; + } + + // In case of other touch types, they + // are latched and the last ever touch + // type is then responded to + if(info->touch_type != voxel_touch::NOTHING) { + latch_touch = info->touch_type; + latch_values = info->touch_values; + latch_surface = info->surface; + latch_vbox = vbox; + continue; + } + } + } + + if(latch_touch != voxel_touch::NOTHING) { + if(latch_touch == voxel_touch::BOUNCE) { + const auto move_distance = vx::abs(current_aabb.min[d] - next_aabb.min[d]); + const auto threshold = 2.0f * globals::fixed_frametime; + + if(move_distance > threshold) { + velocity.value[d] *= -latch_values[d]; + } else { + velocity.value[d] = 0.0f; + } + + touch_surface = latch_surface; + + return move_sign; + } + + if(latch_touch == voxel_touch::SINK) { + velocity.value[d] *= latch_values[d]; + touch_surface = latch_surface; + return move_sign; + } + } + + return 0; +} + +void CollisionComponent::fixed_update(Dimension* dimension) +{ + // FIXME: this isn't particularly accurate considering + // some voxels might be passable and some other voxels + // might apply some slowing factor; what I might do in the + // future is to add a specific value to the voxel registry + // entries that would specify the amount of force we apply + // to prevent player movement inside a specific voxel, plus + // we shouldn't treat all voxels as full cubes if we want + // to support slabs, stairs and non-full liquid voxels in the future + + auto group = dimension->entities.group<CollisionComponent>(entt::get<TransformComponent, VelocityComponent>); + + for(auto [entity, collision, transform, velocity] : group.each()) { + auto surface = voxel_surface::UNKNOWN; + auto vertical_move = vgrid_collide(dimension, 1, collision, transform, velocity, surface); + + if(dimension->entities.any_of<GravityComponent>(entity)) { + if(vertical_move == vx::sign<int>(dimension->get_gravity())) { + dimension->entities.emplace_or_replace<GroundedComponent>(entity, GroundedComponent { surface }); + } else { + dimension->entities.remove<GroundedComponent>(entity); + } + } else { + // The entity cannot be grounded because the component + // setup of said entity should not let it comprehend the + // concept of resting on the ground (it flies around) + dimension->entities.remove<GroundedComponent>(entity); + } + + vgrid_collide(dimension, 0, collision, transform, velocity, surface); + vgrid_collide(dimension, 2, collision, transform, velocity, surface); + } +} diff --git a/src/game/shared/collision.hh b/src/game/shared/collision.hh new file mode 100644 index 0000000..641a803 --- /dev/null +++ b/src/game/shared/collision.hh @@ -0,0 +1,19 @@ +#ifndef SHARED_COLLISION_HH +#define SHARED_COLLISION_HH 1 +#pragma once + +#include "core/aabb.hh" + +class Dimension; + +struct CollisionComponent final { + AABB aabb; + +public: + // NOTE: CollisionComponent::fixed_update must be called + // before TransformComponent::fixed_update and VelocityComponent::fixed_update + // because both transform and velocity may be updated internally + static void fixed_update(Dimension* dimension); +}; + +#endif /* SHARED_COLLISION_HH */ diff --git a/src/game/shared/const.hh b/src/game/shared/const.hh new file mode 100644 index 0000000..4639c4b --- /dev/null +++ b/src/game/shared/const.hh @@ -0,0 +1,47 @@ +#ifndef SHARED_CONST_HH +#define SHARED_CONST_HH 1 +#pragma once + +#include "core/constexpr.hh" + +constexpr static unsigned int CHUNK_SIZE = 16; +constexpr static unsigned int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE; +constexpr static unsigned int CHUNK_VOLUME = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; +constexpr static unsigned int CHUNK_BITSHIFT = vx::log2(CHUNK_SIZE); + +template<typename T> +constexpr static glm::vec<3, T> DIR_NORTH = glm::vec<3, T>(0, 0, +1); +template<typename T> +constexpr static glm::vec<3, T> DIR_SOUTH = glm::vec<3, T>(0, 0, -1); +template<typename T> +constexpr static glm::vec<3, T> DIR_EAST = glm::vec<3, T>(-1, 0, 0); +template<typename T> +constexpr static glm::vec<3, T> DIR_WEST = glm::vec<3, T>(+1, 0, 0); +template<typename T> +constexpr static glm::vec<3, T> DIR_DOWN = glm::vec<3, T>(0, -1, 0); +template<typename T> +constexpr static glm::vec<3, T> DIR_UP = glm::vec<3, T>(0, +1, 0); + +template<typename T> +constexpr static glm::vec<3, T> DIR_FORWARD = glm::vec<3, T>(0, 0, +1); +template<typename T> +constexpr static glm::vec<3, T> DIR_BACK = glm::vec<3, T>(0, 0, -1); +template<typename T> +constexpr static glm::vec<3, T> DIR_LEFT = glm::vec<3, T>(-1, 0, 0); +template<typename T> +constexpr static glm::vec<3, T> DIR_RIGHT = glm::vec<3, T>(+1, 0, 0); + +template<typename T> +constexpr static glm::vec<3, T> UNIT_X = glm::vec<3, T>(1, 0, 0); +template<typename T> +constexpr static glm::vec<3, T> UNIT_Y = glm::vec<3, T>(0, 1, 0); +template<typename T> +constexpr static glm::vec<3, T> UNIT_Z = glm::vec<3, T>(0, 0, 1); + +template<typename T> +constexpr static glm::vec<2, T> ZERO_VEC2 = glm::vec<2, T>(0, 0); + +template<typename T> +constexpr static glm::vec<3, T> ZERO_VEC3 = glm::vec<3, T>(0, 0, 0); + +#endif /* SHARED_CONST_HH */ diff --git a/src/game/shared/coord.hh b/src/game/shared/coord.hh new file mode 100644 index 0000000..f1a2e70 --- /dev/null +++ b/src/game/shared/coord.hh @@ -0,0 +1,149 @@ +#ifndef SHARED_COORD_HH +#define SHARED_COORD_HH 1 +#pragma once + +#include "shared/const.hh" +#include "shared/types.hh" + +namespace coord +{ +constexpr chunk_pos to_chunk(const voxel_pos& vpos); +} // namespace coord + +namespace coord +{ +constexpr local_pos to_local(const voxel_pos& vpos); +constexpr local_pos to_local(const glm::fvec3& fvec); +constexpr local_pos to_local(std::size_t index); +} // namespace coord + +namespace coord +{ +constexpr voxel_pos to_voxel(const chunk_pos& cpos, const local_pos& lpos); +constexpr voxel_pos to_voxel(const chunk_pos& cpos, const glm::fvec3& fvec); +} // namespace coord + +namespace coord +{ +constexpr std::size_t to_index(const local_pos& lpos); +} // namespace coord + +namespace coord +{ +constexpr glm::fvec3 to_relative(const chunk_pos& pivot_cpos, const chunk_pos& cpos, const glm::fvec3& fvec); +constexpr glm::fvec3 to_relative(const chunk_pos& pivot_cpos, const glm::fvec3& pivot_fvec, const chunk_pos& cpos); +constexpr glm::fvec3 to_relative(const chunk_pos& pivot_cpos, const glm::fvec3& pivot_fvec, const chunk_pos& cpos, const glm::fvec3& fvec); +} // namespace coord + +namespace coord +{ +constexpr glm::fvec3 to_fvec3(const chunk_pos& cpos); +constexpr glm::fvec3 to_fvec3(const chunk_pos& cpos, const glm::fvec3& fpos); +} // namespace coord + +inline constexpr chunk_pos coord::to_chunk(const voxel_pos& vpos) +{ + return chunk_pos { + static_cast<chunk_pos::value_type>(vpos.x >> CHUNK_BITSHIFT), + static_cast<chunk_pos::value_type>(vpos.y >> CHUNK_BITSHIFT), + static_cast<chunk_pos::value_type>(vpos.z >> CHUNK_BITSHIFT), + }; +} + +inline constexpr local_pos coord::to_local(const voxel_pos& vpos) +{ + return local_pos { + static_cast<local_pos::value_type>(vx::mod_signed<voxel_pos::value_type>(vpos.x, CHUNK_SIZE)), + static_cast<local_pos::value_type>(vx::mod_signed<voxel_pos::value_type>(vpos.y, CHUNK_SIZE)), + static_cast<local_pos::value_type>(vx::mod_signed<voxel_pos::value_type>(vpos.z, CHUNK_SIZE)), + }; +} + +inline constexpr local_pos coord::to_local(const glm::fvec3& fvec) +{ + return local_pos { + static_cast<local_pos::value_type>(fvec.x), + static_cast<local_pos::value_type>(fvec.y), + static_cast<local_pos::value_type>(fvec.z), + }; +} + +inline constexpr local_pos coord::to_local(std::size_t index) +{ + return local_pos { + static_cast<local_pos::value_type>((index % CHUNK_SIZE)), + static_cast<local_pos::value_type>((index / CHUNK_SIZE) / CHUNK_SIZE), + static_cast<local_pos::value_type>((index / CHUNK_SIZE) % CHUNK_SIZE), + }; +} + +inline constexpr voxel_pos coord::to_voxel(const chunk_pos& cpos, const local_pos& lpos) +{ + return voxel_pos { + lpos.x + (static_cast<voxel_pos::value_type>(cpos.x) << CHUNK_BITSHIFT), + lpos.y + (static_cast<voxel_pos::value_type>(cpos.y) << CHUNK_BITSHIFT), + lpos.z + (static_cast<voxel_pos::value_type>(cpos.z) << CHUNK_BITSHIFT), + }; +} + +inline constexpr voxel_pos coord::to_voxel(const chunk_pos& cpos, const glm::fvec3& fvec) +{ + return voxel_pos { + static_cast<voxel_pos::value_type>(fvec.x) + (static_cast<voxel_pos::value_type>(cpos.x) << CHUNK_BITSHIFT), + static_cast<voxel_pos::value_type>(fvec.y) + (static_cast<voxel_pos::value_type>(cpos.y) << CHUNK_BITSHIFT), + static_cast<voxel_pos::value_type>(fvec.z) + (static_cast<voxel_pos::value_type>(cpos.z) << CHUNK_BITSHIFT), + }; +} + +inline constexpr std::size_t coord::to_index(const local_pos& lpos) +{ + return static_cast<std::size_t>((lpos.y * CHUNK_SIZE + lpos.z) * CHUNK_SIZE + lpos.x); +} + +inline constexpr glm::fvec3 coord::to_relative(const chunk_pos& pivot_cpos, const chunk_pos& cpos, const glm::fvec3& fvec) +{ + return glm::fvec3 { + static_cast<float>((cpos.x - pivot_cpos.x) << CHUNK_BITSHIFT) + fvec.x, + static_cast<float>((cpos.y - pivot_cpos.y) << CHUNK_BITSHIFT) + fvec.y, + static_cast<float>((cpos.z - pivot_cpos.z) << CHUNK_BITSHIFT) + fvec.z, + }; +} + +inline constexpr glm::fvec3 coord::to_relative(const chunk_pos& pivot_cpos, const glm::fvec3& pivot_fvec, const chunk_pos& cpos) +{ + return glm::fvec3 { + static_cast<float>((cpos.x - pivot_cpos.x) << CHUNK_BITSHIFT) - pivot_fvec.x, + static_cast<float>((cpos.y - pivot_cpos.y) << CHUNK_BITSHIFT) - pivot_fvec.y, + static_cast<float>((cpos.z - pivot_cpos.z) << CHUNK_BITSHIFT) - pivot_fvec.z, + }; +} + +inline constexpr glm::fvec3 coord::to_relative( + const chunk_pos& pivot_cpos, const glm::fvec3& pivot_fvec, const chunk_pos& cpos, const glm::fvec3& fvec) +{ + return glm::fvec3 { + static_cast<float>((cpos.x - pivot_cpos.x) << CHUNK_BITSHIFT) + (fvec.x - pivot_fvec.x), + static_cast<float>((cpos.y - pivot_cpos.y) << CHUNK_BITSHIFT) + (fvec.y - pivot_fvec.y), + static_cast<float>((cpos.z - pivot_cpos.z) << CHUNK_BITSHIFT) + (fvec.z - pivot_fvec.z), + }; +} + +inline constexpr glm::fvec3 coord::to_fvec3(const chunk_pos& cpos) +{ + return glm::fvec3 { + static_cast<float>(cpos.x << CHUNK_BITSHIFT), + static_cast<float>(cpos.y << CHUNK_BITSHIFT), + static_cast<float>(cpos.z << CHUNK_BITSHIFT), + }; +} + +inline constexpr glm::fvec3 coord::to_fvec3(const chunk_pos& cpos, const glm::fvec3& fpos) +{ + return glm::fvec3 { + fpos.x + static_cast<float>(cpos.x << CHUNK_BITSHIFT), + fpos.y + static_cast<float>(cpos.y << CHUNK_BITSHIFT), + fpos.z + static_cast<float>(cpos.z << CHUNK_BITSHIFT), + }; +} + +#endif /* SHARED_COORD_HH */ diff --git a/src/game/shared/dimension.cc b/src/game/shared/dimension.cc new file mode 100644 index 0000000..a919dc4 --- /dev/null +++ b/src/game/shared/dimension.cc @@ -0,0 +1,169 @@ +#include "shared/pch.hh" + +#include "shared/dimension.hh" + +#include "shared/chunk.hh" +#include "shared/coord.hh" +#include "shared/globals.hh" + +Dimension::Dimension(const char* name, float gravity) +{ + m_name = name; + m_gravity = gravity; +} + +Dimension::~Dimension(void) +{ + for(const auto it : m_chunkmap) + delete it.second; + entities.clear(); + chunks.clear(); +} + +const char* Dimension::get_name(void) const +{ + return m_name.c_str(); +} + +float Dimension::get_gravity(void) const +{ + return m_gravity; +} + +Chunk* Dimension::create_chunk(const chunk_pos& cpos) +{ + auto it = m_chunkmap.find(cpos); + + if(it != m_chunkmap.cend()) { + // Chunk already exists + return it->second; + } + + auto entity = chunks.create(); + auto chunk = new Chunk(entity, this); + + auto& component = chunks.emplace<ChunkComponent>(entity); + component.chunk = chunk; + component.cpos = cpos; + + ChunkCreateEvent event; + event.dimension = this; + event.chunk = chunk; + event.cpos = cpos; + + globals::dispatcher.trigger(event); + + return m_chunkmap.insert_or_assign(cpos, std::move(chunk)).first->second; +} + +Chunk* Dimension::find_chunk(entt::entity entity) const +{ + if(chunks.valid(entity)) { + return chunks.get<ChunkComponent>(entity).chunk; + } else { + return nullptr; + } +} + +Chunk* Dimension::find_chunk(const chunk_pos& cpos) const +{ + auto it = m_chunkmap.find(cpos); + + if(it != m_chunkmap.cend()) { + return it->second; + } else { + return nullptr; + } +} + +void Dimension::remove_chunk(entt::entity entity) +{ + if(chunks.valid(entity)) { + auto& component = chunks.get<ChunkComponent>(entity); + m_chunkmap.erase(component.cpos); + chunks.destroy(entity); + } +} + +void Dimension::remove_chunk(const chunk_pos& cpos) +{ + auto it = m_chunkmap.find(cpos); + + if(it != m_chunkmap.cend()) { + chunks.destroy(it->second->get_entity()); + m_chunkmap.erase(it); + } +} + +void Dimension::remove_chunk(Chunk* chunk) +{ + if(chunk) { + const auto& component = chunks.get<ChunkComponent>(chunk->get_entity()); + m_chunkmap.erase(component.cpos); + chunks.destroy(chunk->get_entity()); + } +} + +voxel_id Dimension::get_voxel(const voxel_pos& vpos) const +{ + auto cpos = coord::to_chunk(vpos); + auto lpos = coord::to_local(vpos); + + if(auto chunk = find_chunk(cpos)) { + return chunk->get_voxel(lpos); + } else { + return NULL_VOXEL_ID; + } +} + +voxel_id Dimension::get_voxel(const chunk_pos& cpos, const local_pos& lpos) const +{ + // This allows accessing get_voxel with negative + // local coordinates that usually would result in an + // out-of-range values; this is useful for per-voxel update logic + return get_voxel(coord::to_voxel(cpos, lpos)); +} + +bool Dimension::set_voxel(voxel_id voxel, const voxel_pos& vpos) +{ + auto cpos = coord::to_chunk(vpos); + auto lpos = coord::to_local(vpos); + + if(auto chunk = find_chunk(cpos)) { + chunk->set_voxel(voxel, lpos); + + VoxelSetEvent event; + event.dimension = this; + event.cpos = cpos; + event.lpos = lpos; + event.voxel = voxel; + event.chunk = chunk; + + globals::dispatcher.trigger(event); + + return true; + } + + return false; +} + +bool Dimension::set_voxel(voxel_id voxel, const chunk_pos& cpos, const local_pos& lpos) +{ + // This allows accessing set_voxel with negative + // local coordinates that usually would result in an + // out-of-range values; this is useful for per-voxel update logic + return set_voxel(voxel, coord::to_voxel(cpos, lpos)); +} + +void Dimension::init(Config& config) +{ +} + +void Dimension::init_late(std::uint64_t global_seed) +{ +} + +bool Dimension::generate(const chunk_pos& cpos, VoxelStorage& voxels) +{ + return false; +} diff --git a/src/game/shared/dimension.hh b/src/game/shared/dimension.hh new file mode 100644 index 0000000..c609a14 --- /dev/null +++ b/src/game/shared/dimension.hh @@ -0,0 +1,85 @@ +#ifndef SHARED_DIMENSION_HH +#define SHARED_DIMENSION_HH 1 +#pragma once + +#include "shared/const.hh" +#include "shared/types.hh" + +class Chunk; +class Config; +class VoxelStorage; + +using dimension_entropy_map = std::array<std::uint64_t, CHUNK_AREA>; +using dimension_height_map = std::array<voxel_pos::value_type, CHUNK_AREA>; + +class Dimension { +public: + explicit Dimension(const char* name, float gravity); + virtual ~Dimension(void); + + const char* get_name(void) const; + float get_gravity(void) const; + +public: + Chunk* create_chunk(const chunk_pos& cpos); + Chunk* find_chunk(entt::entity entity) const; + Chunk* find_chunk(const chunk_pos& cpos) const; + + void remove_chunk(entt::entity entity); + void remove_chunk(const chunk_pos& cpos); + void remove_chunk(Chunk* chunk); + +public: + voxel_id get_voxel(const voxel_pos& vpos) const; + voxel_id get_voxel(const chunk_pos& cpos, const local_pos& lpos) const; + + bool set_voxel(voxel_id voxel, const voxel_pos& vpos); + bool set_voxel(voxel_id voxel, const chunk_pos& cpos, const local_pos& lpos); + +public: + virtual void init(Config& config); + virtual void init_late(std::uint64_t global_seed); + virtual bool generate(const chunk_pos& cpos, VoxelStorage& voxels); + +public: + entt::registry chunks; + entt::registry entities; + +private: + std::string m_name; + emhash8::HashMap<chunk_pos, Chunk*> m_chunkmap; + float m_gravity; +}; + +struct ChunkComponent final { + chunk_pos cpos; + Chunk* chunk; +}; + +struct ChunkCreateEvent final { + Dimension* dimension; + chunk_pos cpos; + Chunk* chunk; +}; + +struct ChunkDestroyEvent final { + Dimension* dimension; + chunk_pos cpos; + Chunk* chunk; +}; + +struct ChunkUpdateEvent final { + Dimension* dimension; + chunk_pos cpos; + Chunk* chunk; +}; + +struct VoxelSetEvent final { + Dimension* dimension; + chunk_pos cpos; + local_pos lpos; + voxel_id voxel; + Chunk* chunk; +}; + +#endif /* SHARED_DIMENSION_HH */ diff --git a/src/game/shared/factory.cc b/src/game/shared/factory.cc new file mode 100644 index 0000000..ad65928 --- /dev/null +++ b/src/game/shared/factory.cc @@ -0,0 +1,35 @@ +#include "shared/pch.hh" + +#include "shared/factory.hh" + +#include "shared/collision.hh" +#include "shared/dimension.hh" +#include "shared/globals.hh" +#include "shared/gravity.hh" +#include "shared/head.hh" +#include "shared/player.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" + +void shared_factory::create_player(Dimension* dimension, entt::entity entity) +{ + spdlog::debug("factory[{}]: assigning player components to {}", dimension->get_name(), static_cast<std::uint64_t>(entity)); + + auto& collision = dimension->entities.emplace_or_replace<CollisionComponent>(entity); + collision.aabb.min = glm::fvec3(-0.4f, -1.6f, -0.4f); + collision.aabb.max = glm::fvec3(+0.4f, +0.2f, +0.4f); + + auto& head = dimension->entities.emplace_or_replace<HeadComponent>(entity); + head.angles = glm::fvec3(0.0f, 0.0f, 0.0f); + head.offset = glm::fvec3(0.0f, 0.0f, 0.0f); + + dimension->entities.emplace_or_replace<PlayerComponent>(entity); + + auto& transform = dimension->entities.emplace_or_replace<TransformComponent>(entity); + transform.chunk = chunk_pos(0, 2, 0); + transform.local = glm::fvec3(0.0f, 0.0f, 0.0f); + transform.angles = glm::fvec3(0.0f, 0.0f, 0.0f); + + auto& velocity = dimension->entities.emplace_or_replace<VelocityComponent>(entity); + velocity.value = glm::fvec3(0.0f, 0.0f, 0.0f); +} diff --git a/src/game/shared/factory.hh b/src/game/shared/factory.hh new file mode 100644 index 0000000..c3449dd --- /dev/null +++ b/src/game/shared/factory.hh @@ -0,0 +1,12 @@ +#ifndef SHARED_FACTORY +#define SHARED_FACTORY 1 +#pragma once + +class Dimension; + +namespace shared_factory +{ +void create_player(Dimension* dimension, entt::entity entity); +} // namespace shared_factory + +#endif /* SHARED_FACTORY */ diff --git a/src/game/shared/feature.cc b/src/game/shared/feature.cc new file mode 100644 index 0000000..eb6cceb --- /dev/null +++ b/src/game/shared/feature.cc @@ -0,0 +1,52 @@ +#include "shared/pch.hh" + +#include "shared/feature.hh" + +#include "shared/chunk.hh" +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/voxel_storage.hh" + +void Feature::place(const voxel_pos& vpos, Dimension* dimension) const +{ + for(const auto [rpos, voxel, overwrite] : (*this)) { + auto it_vpos = vpos + rpos; + auto it_cpos = coord::to_chunk(it_vpos); + + if(auto chunk = dimension->create_chunk(it_cpos)) { + auto it_lpos = coord::to_local(it_vpos); + auto it_index = coord::to_index(it_lpos); + + if(chunk->get_voxel(it_index) && !overwrite) { + // There is something in the way + // and the called intentionally requested + // we do not force feature to overwrite voxels + continue; + } + + chunk->set_voxel(voxel, it_index); + } + } +} + +void Feature::place(const voxel_pos& vpos, const chunk_pos& cpos, VoxelStorage& voxels) const +{ + for(const auto [rpos, voxel, overwrite] : (*this)) { + auto it_vpos = vpos + rpos; + auto it_cpos = coord::to_chunk(it_vpos); + + if(it_cpos == cpos) { + auto it_lpos = coord::to_local(it_vpos); + auto it_index = coord::to_index(it_lpos); + + if(voxels[it_index] && !overwrite) { + // There is something in the way + // and the called intentionally requested + // we do not force feature to overwrite voxels + continue; + } + + voxels[it_index] = voxel; + } + } +} diff --git a/src/game/shared/feature.hh b/src/game/shared/feature.hh new file mode 100644 index 0000000..04f38e8 --- /dev/null +++ b/src/game/shared/feature.hh @@ -0,0 +1,22 @@ +#ifndef SHARED_FEATURE_HH +#define SHARED_FEATURE_HH 1 +#pragma once + +#include "core/macros.hh" + +#include "shared/types.hh" + +class Dimension; +class VoxelStorage; + +class Feature final : public std::vector<std::tuple<voxel_pos, voxel_id, bool>> { +public: + DECLARE_DEFAULT_CONSTRUCTOR(Feature); + virtual ~Feature(void) = default; + +public: + void place(const voxel_pos& vpos, Dimension* dimension) const; + void place(const voxel_pos& vpos, const chunk_pos& cpos, VoxelStorage& voxels) const; +}; + +#endif /* SHARED_FEATURE_HH */ diff --git a/src/game/shared/game.cc b/src/game/shared/game.cc new file mode 100644 index 0000000..73de8b5 --- /dev/null +++ b/src/game/shared/game.cc @@ -0,0 +1,121 @@ +#include "shared/pch.hh" + +#include "shared/game.hh" + +#include "core/cmdline.hh" + +static std::filesystem::path get_gamepath(void) +{ + if(auto gamepath = cmdline::get("gamepath")) { + // Allow users and third-party launchers to override + // content location. Perhaps this would work to allow + // for a Minecraft-like versioning approach? + return std::filesystem::path(gamepath); + } + + return std::filesystem::current_path() / "data"; +} + +static std::filesystem::path get_userpath(void) +{ + if(auto userpath = cmdline::get("userpath")) { + // Allow users and third-party launchers to override + // user data location. Perhaps this would work to allow + // for a Minecraft-like versioning approach? + return std::filesystem::path(userpath); + } + + if(auto win_appdata = std::getenv("APPDATA")) { + // On pre-seven Windows systems it's just AppData + // On post-seven Windows systems it's AppData/Roaming + return std::filesystem::path(win_appdata) / "voxelius"; + } + + if(auto xdg_home = std::getenv("XDG_DATA_HOME")) { + // Systems with an active X11/Wayland session + // theoretically should have this defined, and + // it can be different from ${HOME} (I think). + return std::filesystem::path(xdg_home) / ".voxelius"; + } + + if(auto unix_home = std::getenv("HOME")) { + // Any spherical UNIX/UNIX-like system in vacuum + // has this defined for every single user process. + return std::filesystem::path(unix_home) / ".voxelius"; + } + + // Give up and save stuff into CWD + return std::filesystem::current_path(); +} + +void shared_game::init(int argc, char** argv) +{ + auto logger = spdlog::default_logger(); + auto& logger_sinks = logger->sinks(); + + logger_sinks.clear(); + logger_sinks.push_back(std::make_shared<spdlog::sinks::stderr_color_sink_mt>()); + logger_sinks.push_back(std::make_shared<spdlog::sinks::basic_file_sink_mt>("voxelius.log", false)); + +#if defined(NDEBUG) + constexpr auto default_loglevel = spdlog::level::info; +#else + constexpr auto default_loglevel = spdlog::level::trace; +#endif + + if(cmdline::contains("quiet")) { + logger->set_level(spdlog::level::warn); + } else if(cmdline::contains("verbose")) { + logger->set_level(spdlog::level::trace); + } else { + logger->set_level(default_loglevel); + } + + logger->set_pattern("%H:%M:%S.%e %^[%L]%$ %v"); + logger->flush(); + + if(!PHYSFS_init(argv[0])) { + spdlog::critical("physfs: init failed: {}", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + std::terminate(); + } + + auto gamepath = get_gamepath(); + auto userpath = get_userpath(); + + spdlog::info("shared_game: set gamepath to {}", gamepath.string()); + spdlog::info("shared_game: set userpath to {}", userpath.string()); + + std::error_code ignore_error = {}; + std::filesystem::create_directories(gamepath, ignore_error); + std::filesystem::create_directories(userpath, ignore_error); + + if(!PHYSFS_mount(gamepath.string().c_str(), nullptr, false)) { + spdlog::critical("physfs: mount {} failed: {}", gamepath.string(), PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + std::terminate(); + } + + if(!PHYSFS_mount(userpath.string().c_str(), nullptr, false)) { + spdlog::critical("physfs: mount {} failed: {}", userpath.string(), PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + std::terminate(); + } + + if(!PHYSFS_setWriteDir(userpath.string().c_str())) { + spdlog::critical("physfs: setwritedir {} failed: {}", userpath.string(), PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + std::terminate(); + } + + if(enet_initialize()) { + spdlog::critical("enet: init failed"); + std::terminate(); + } +} + +void shared_game::deinit(void) +{ + enet_deinitialize(); + + if(!PHYSFS_deinit()) { + spdlog::critical("physfs: deinit failed: {}", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + std::terminate(); + } +} diff --git a/src/game/shared/game.hh b/src/game/shared/game.hh new file mode 100644 index 0000000..59c5327 --- /dev/null +++ b/src/game/shared/game.hh @@ -0,0 +1,11 @@ +#ifndef SHARED_GAME +#define SHARED_GAME 1 +#pragma once + +namespace shared_game +{ +void init(int argc, char** argv); +void deinit(void); +} // namespace shared_game + +#endif /* SHARED_GAME */ diff --git a/src/game/shared/game_items.cc b/src/game/shared/game_items.cc new file mode 100644 index 0000000..95fa6fc --- /dev/null +++ b/src/game/shared/game_items.cc @@ -0,0 +1,61 @@ +#include "shared/pch.hh" + +#include "shared/game_items.hh" + +#include "shared/game_voxels.hh" +#include "shared/item_registry.hh" + +item_id game_items::stone = NULL_ITEM_ID; +item_id game_items::cobblestone = NULL_ITEM_ID; +item_id game_items::dirt = NULL_ITEM_ID; +item_id game_items::grass = NULL_ITEM_ID; +item_id game_items::oak_leaves = NULL_ITEM_ID; +item_id game_items::oak_planks = NULL_ITEM_ID; +item_id game_items::oak_log = NULL_ITEM_ID; +item_id game_items::glass = NULL_ITEM_ID; +item_id game_items::slime = NULL_ITEM_ID; +item_id game_items::mud = NULL_ITEM_ID; + +void game_items::populate(void) +{ + // Stone; a hardened slate rock + game_items::stone = + item_registry::construct("stone").set_texture("textures/item/stone.png").set_place_voxel(game_voxels::stone).build(); + + // Cobblestone; a bunch of small stones + game_items::cobblestone = + item_registry::construct("cobblestone") + .set_texture("textures/item/cobblestone.png") + .set_place_voxel(game_voxels::cobblestone) + .build(); + + // Dirt; it's very dirty + game_items::dirt = item_registry::construct("dirt").set_texture("textures/item/dirt.png").set_place_voxel(game_voxels::dirt).build(); + + // Grass; literally just grassy dirt + game_items::grass = + item_registry::construct("grass").set_texture("textures/item/grass.png").set_place_voxel(game_voxels::grass).build(); + + // Oak leaves; they're bushy! + game_items::oak_leaves = + item_registry::construct("oak_leaves").set_texture("textures/item/oak_leaves.png").set_place_voxel(game_voxels::oak_leaves).build(); + + // Oak planks; watch for splinters! + game_items::oak_planks = + item_registry::construct("oak_planks").set_texture("textures/item/oak_planks.png").set_place_voxel(game_voxels::oak_planks).build(); + + // Oak log; a big wad of wood + game_items::oak_log = + item_registry::construct("oak_log").set_texture("textures/item/oak_log.png").set_place_voxel(game_voxels::oak_log).build(); + + // Glass; used for windowing + game_items::glass = + item_registry::construct("glass").set_texture("textures/item/glass.png").set_place_voxel(game_voxels::glass).build(); + + // Slime; it's bouncy! + game_items::slime = + item_registry::construct("slime").set_texture("textures/item/slime.png").set_place_voxel(game_voxels::slime).build(); + + // Mud; you sink in it! + game_items::mud = item_registry::construct("mud").set_texture("textures/item/mud.png").build(); +} diff --git a/src/game/shared/game_items.hh b/src/game/shared/game_items.hh new file mode 100644 index 0000000..4e4eb81 --- /dev/null +++ b/src/game/shared/game_items.hh @@ -0,0 +1,26 @@ +#ifndef SHARED_GAME_ITEMS +#define SHARED_GAME_ITEMS 1 +#pragma once + +#include "shared/types.hh" + +namespace game_items +{ +extern item_id stone; +extern item_id cobblestone; +extern item_id dirt; +extern item_id grass; +extern item_id oak_leaves; +extern item_id oak_planks; +extern item_id oak_log; +extern item_id glass; +extern item_id slime; +extern item_id mud; +} // namespace game_items + +namespace game_items +{ +void populate(void); +} // namespace game_items + +#endif /* SHARED_GAME_ITEMS */ diff --git a/src/game/shared/game_voxels.cc b/src/game/shared/game_voxels.cc new file mode 100644 index 0000000..0059335 --- /dev/null +++ b/src/game/shared/game_voxels.cc @@ -0,0 +1,122 @@ +#include "shared/pch.hh" + +#include "shared/game_voxels.hh" + +#include "shared/voxel_registry.hh" + +voxel_id game_voxels::cobblestone = NULL_VOXEL_ID; +voxel_id game_voxels::dirt = NULL_VOXEL_ID; +voxel_id game_voxels::grass = NULL_VOXEL_ID; +voxel_id game_voxels::stone = NULL_VOXEL_ID; +voxel_id game_voxels::vtest = NULL_VOXEL_ID; +voxel_id game_voxels::vtest_ck = NULL_VOXEL_ID; +voxel_id game_voxels::oak_leaves = NULL_VOXEL_ID; +voxel_id game_voxels::oak_planks = NULL_VOXEL_ID; +voxel_id game_voxels::oak_log = NULL_VOXEL_ID; +voxel_id game_voxels::glass = NULL_VOXEL_ID; +voxel_id game_voxels::slime = NULL_VOXEL_ID; +voxel_id game_voxels::mud = NULL_VOXEL_ID; + +void game_voxels::populate(void) +{ + // Stone; the backbone of the generated world + game_voxels::stone = + voxel_registry::construct("stone", voxel_type::CUBE, false, false) + .add_texture_default("textures/voxel/stone_01.png") + .add_texture_default("textures/voxel/stone_02.png") + .add_texture_default("textures/voxel/stone_03.png") + .add_texture_default("textures/voxel/stone_04.png") + .set_surface(voxel_surface::STONE) + .build(); + + // Cobblestone; should drop when a stone is broken, might also be present in surface features + game_voxels::cobblestone = + voxel_registry::construct("cobblestone", voxel_type::CUBE, false, false) + .add_texture_default("textures/voxel/cobblestone_01.png") + .add_texture_default("textures/voxel/cobblestone_02.png") + .set_surface(voxel_surface::STONE) + .build(); + + // Dirt with a grass layer on top; the top layer of plains biome + game_voxels::grass = + voxel_registry::construct("grass", voxel_type::CUBE, false, false) + .add_texture_default("textures/voxel/grass_side_01.png") + .add_texture_default("textures/voxel/grass_side_02.png") + .add_texture(voxel_face::CUBE_BOTTOM, "textures/voxel/dirt_01.png") + .add_texture(voxel_face::CUBE_BOTTOM, "textures/voxel/dirt_02.png") + .add_texture(voxel_face::CUBE_BOTTOM, "textures/voxel/dirt_03.png") + .add_texture(voxel_face::CUBE_BOTTOM, "textures/voxel/dirt_04.png") + .add_texture(voxel_face::CUBE_TOP, "textures/voxel/grass_01.png") + .add_texture(voxel_face::CUBE_TOP, "textures/voxel/grass_02.png") + .set_surface(voxel_surface::GRASS) + .build(); + + // Dirt; the under-surface layer of some biomes + game_voxels::dirt = + voxel_registry::construct("dirt", voxel_type::CUBE, false, false) + .add_texture_default("textures/voxel/dirt_01.png") + .add_texture_default("textures/voxel/dirt_02.png") + .add_texture_default("textures/voxel/dirt_03.png") + .add_texture_default("textures/voxel/dirt_04.png") + .set_surface(voxel_surface::DIRT) + .build(); + + // VTest; a test voxel to ensure animations work + game_voxels::vtest = + voxel_registry::construct("vtest", voxel_type::CUBE, true, false) + .add_texture_default("textures/voxel/vtest_F1.png") + .add_texture_default("textures/voxel/vtest_F2.png") + .add_texture_default("textures/voxel/vtest_F3.png") + .add_texture_default("textures/voxel/vtest_F4.png") + .build(); + + // VTest-CK; a pure blue chromakey I used to make the game's logo + game_voxels::vtest_ck = + voxel_registry::construct("vtest_ck", voxel_type::CUBE, false, false).add_texture_default("textures/voxel/chromakey.png").build(); + + // Oak leaves; greenery. TODO: add trees as surface features + game_voxels::oak_leaves = + voxel_registry::construct("oak_leaves", voxel_type::CUBE, false, false) + .add_texture_default("textures/voxel/oak_leaves.png") + .set_surface(voxel_surface::GRASS) + .build(); + + // Oak planks; the thing that comes out of oak logs + game_voxels::oak_planks = + voxel_registry::construct("oak_planks", voxel_type::CUBE, false, false) + .add_texture_default("textures/voxel/oak_planks_01.png") + .add_texture_default("textures/voxel/oak_planks_02.png") + .set_surface(voxel_surface::WOOD) + .build(); + + // Oak logs; greenery. TODO: add trees as surface features + game_voxels::oak_log = + voxel_registry::construct("oak_log", voxel_type::CUBE, false, false) + .add_texture_default("textures/voxel/oak_wood_01.png") + .add_texture_default("textures/voxel/oak_wood_02.png") + .add_texture(voxel_face::CUBE_BOTTOM, "textures/voxel/oak_wood_top.png") + .add_texture(voxel_face::CUBE_TOP, "textures/voxel/oak_wood_top.png") + .set_surface(voxel_surface::WOOD) + .build(); + + // Glass; blend rendering test + game_voxels::glass = voxel_registry::construct("glass", voxel_type::CUBE, false, true) + .add_texture_default("textures/voxel/glass_01.png") + .set_surface(voxel_surface::GLASS) + .build(); + + // Slime; it's bouncy! + game_voxels::slime = voxel_registry::construct("slime", voxel_type::CUBE, false, true) + .set_touch(voxel_touch::BOUNCE, glm::fvec3(0.00f, 0.60f, 0.00f)) + .add_texture_default("textures/voxel/slime_01.png") + .build(); + + // Mud; you sink in it + game_voxels::mud = + voxel_registry::construct("mud", voxel_type::CUBE, false, false) + .set_touch(voxel_touch::SINK, glm::fvec3(0.50f, 0.75f, 0.50f)) + .add_texture_default("textures/voxel/mud_01.png") + .add_texture_default("textures/voxel/mud_02.png") + .set_surface(voxel_surface::DIRT) + .build(); +} diff --git a/src/game/shared/game_voxels.hh b/src/game/shared/game_voxels.hh new file mode 100644 index 0000000..1b344bf --- /dev/null +++ b/src/game/shared/game_voxels.hh @@ -0,0 +1,28 @@ +#ifndef SHARED_GAME_VOXELS +#define SHARED_GAME_VOXELS 1 +#pragma once + +#include "shared/types.hh" + +namespace game_voxels +{ +extern voxel_id cobblestone; +extern voxel_id dirt; +extern voxel_id grass; +extern voxel_id stone; +extern voxel_id vtest; +extern voxel_id vtest_ck; +extern voxel_id oak_leaves; +extern voxel_id oak_planks; +extern voxel_id oak_log; +extern voxel_id glass; +extern voxel_id slime; +extern voxel_id mud; +} // namespace game_voxels + +namespace game_voxels +{ +void populate(void); +} // namespace game_voxels + +#endif /* SHARED_GAME_VOXELS */ diff --git a/src/game/shared/globals.cc b/src/game/shared/globals.cc new file mode 100644 index 0000000..8552214 --- /dev/null +++ b/src/game/shared/globals.cc @@ -0,0 +1,12 @@ +#include "shared/pch.hh" + +#include "shared/globals.hh" + +entt::dispatcher globals::dispatcher; + +float globals::fixed_frametime; +float globals::fixed_frametime_avg; +std::uint64_t globals::fixed_frametime_us; +std::size_t globals::fixed_framecount; + +std::uint64_t globals::curtime; diff --git a/src/game/shared/globals.hh b/src/game/shared/globals.hh new file mode 100644 index 0000000..216d341 --- /dev/null +++ b/src/game/shared/globals.hh @@ -0,0 +1,23 @@ +#ifndef SHARED_GLOBALS_HH +#define SHARED_GLOBALS_HH 1 +#pragma once + +namespace globals +{ +extern entt::dispatcher dispatcher; +} // namespace globals + +namespace globals +{ +extern float fixed_frametime; +extern float fixed_frametime_avg; +extern std::uint64_t fixed_frametime_us; +extern std::size_t fixed_framecount; +} // namespace globals + +namespace globals +{ +extern std::uint64_t curtime; +} // namespace globals + +#endif /* SHARED_GLOBALS_HH */ diff --git a/src/game/shared/gravity.cc b/src/game/shared/gravity.cc new file mode 100644 index 0000000..068f658 --- /dev/null +++ b/src/game/shared/gravity.cc @@ -0,0 +1,18 @@ +#include "shared/pch.hh" + +#include "shared/gravity.hh" + +#include "shared/dimension.hh" +#include "shared/globals.hh" +#include "shared/stasis.hh" +#include "shared/velocity.hh" + +void GravityComponent::fixed_update(Dimension* dimension) +{ + auto fixed_acceleration = globals::fixed_frametime * dimension->get_gravity(); + auto group = dimension->entities.group<GravityComponent>(entt::get<VelocityComponent>, entt::exclude<StasisComponent>); + + for(auto [entity, velocity] : group.each()) { + velocity.value.y += fixed_acceleration; + } +} diff --git a/src/game/shared/gravity.hh b/src/game/shared/gravity.hh new file mode 100644 index 0000000..53f51b7 --- /dev/null +++ b/src/game/shared/gravity.hh @@ -0,0 +1,12 @@ +#ifndef SHARED_GRAVITY_HH +#define SHARED_GRAVITY_HH 1 +#pragma once + +class Dimension; + +struct GravityComponent final { +public: + static void fixed_update(Dimension* dimension); +}; + +#endif /* SHARED_GRAVITY_HH */ diff --git a/src/game/shared/grounded.hh b/src/game/shared/grounded.hh new file mode 100644 index 0000000..869cca0 --- /dev/null +++ b/src/game/shared/grounded.hh @@ -0,0 +1,13 @@ +#ifndef SHARED_GROUNDED +#define SHARED_GROUNDED 1 +#pragma once + +#include "shared/voxel_registry.hh" + +// Assigned to entities which are grounded +// according to the collision and gravity system +struct GroundedComponent final { + voxel_surface surface; +}; + +#endif /* SHARED_GROUNDED */ diff --git a/src/game/shared/head.hh b/src/game/shared/head.hh new file mode 100644 index 0000000..57dd445 --- /dev/null +++ b/src/game/shared/head.hh @@ -0,0 +1,14 @@ +#ifndef SHARED_HEAD_HH +#define SHARED_HEAD_HH 1 +#pragma once + +struct HeadComponent { + glm::fvec3 angles; + glm::fvec3 offset; +}; + +// Client-side only - interpolated and previous head +struct HeadComponentIntr final : public HeadComponent {}; +struct HeadComponentPrev final : public HeadComponent {}; + +#endif /* SHARED_HEAD_HH */ diff --git a/src/game/shared/item_registry.cc b/src/game/shared/item_registry.cc new file mode 100644 index 0000000..1263201 --- /dev/null +++ b/src/game/shared/item_registry.cc @@ -0,0 +1,103 @@ +#include "shared/pch.hh" + +#include "shared/item_registry.hh" + +#include "core/crc64.hh" + +#include "shared/voxel_registry.hh" + +std::unordered_map<std::string, ItemInfoBuilder> item_registry::builders = {}; +std::unordered_map<std::string, item_id> item_registry::names = {}; +std::vector<std::shared_ptr<ItemInfo>> item_registry::items = {}; + +ItemInfoBuilder::ItemInfoBuilder(const char* name) +{ + prototype.name = name; + prototype.texture = std::string(); + prototype.place_voxel = NULL_VOXEL_ID; + prototype.cached_texture = nullptr; +} + +ItemInfoBuilder& ItemInfoBuilder::set_texture(const char* texture) +{ + prototype.texture = texture; + prototype.cached_texture = nullptr; + return *this; +} + +ItemInfoBuilder& ItemInfoBuilder::set_place_voxel(voxel_id place_voxel) +{ + prototype.place_voxel = place_voxel; + return *this; +} + +item_id ItemInfoBuilder::build(void) const +{ + const auto it = item_registry::names.find(prototype.name); + + if(it != item_registry::names.cend()) { + spdlog::warn("item_registry: cannot build {}: name already present", prototype.name); + return it->second; + } + + auto new_info = std::make_shared<ItemInfo>(); + new_info->name = prototype.name; + new_info->texture = prototype.texture; + new_info->place_voxel = prototype.place_voxel; + new_info->cached_texture = nullptr; + + item_registry::items.push_back(new_info); + item_registry::names.insert_or_assign(prototype.name, static_cast<item_id>(item_registry::items.size())); + + return static_cast<item_id>(item_registry::items.size()); +} + +ItemInfoBuilder& item_registry::construct(const char* name) +{ + const auto it = item_registry::builders.find(name); + + if(it != item_registry::builders.cend()) { + return it->second; + } else { + return item_registry::builders.emplace(name, ItemInfoBuilder(name)).first->second; + } +} + +ItemInfo* item_registry::find(const char* name) +{ + const auto it = item_registry::names.find(name); + + if(it != item_registry::names.cend()) { + return item_registry::find(it->second); + } else { + return nullptr; + } +} + +ItemInfo* item_registry::find(const item_id item) +{ + if((item != NULL_ITEM_ID) && (item <= item_registry::items.size())) { + return item_registry::items[item - 1].get(); + } else { + return nullptr; + } +} + +void item_registry::purge(void) +{ + item_registry::builders.clear(); + item_registry::names.clear(); + item_registry::items.clear(); +} + +std::uint64_t item_registry::calcualte_checksum(void) +{ + std::uint64_t result = 0; + + for(const auto& info : item_registry::items) { + result = crc64::get(info->name, result); + result += static_cast<std::uint64_t>(info->place_voxel); + } + + return result; +} diff --git a/src/game/shared/item_registry.hh b/src/game/shared/item_registry.hh new file mode 100644 index 0000000..17cff9f --- /dev/null +++ b/src/game/shared/item_registry.hh @@ -0,0 +1,62 @@ +#ifndef SHARED_ITEM_REGISTRY_HH +#define SHARED_ITEM_REGISTRY_HH 1 +#pragma once + +#include "core/resource.hh" + +#include "shared/types.hh" + +// This resource is only defined client-side and +// resource_ptr<TextureGUI> should remain set to null +// anywhere else in the shared and server code +struct TextureGUI; + +struct ItemInfo final { + std::string name; + std::string texture; + voxel_id place_voxel; + + resource_ptr<TextureGUI> cached_texture; // Client-side only +}; + +class ItemInfoBuilder final { +public: + explicit ItemInfoBuilder(const char* name); + virtual ~ItemInfoBuilder(void) = default; + +public: + ItemInfoBuilder& set_texture(const char* texture); + ItemInfoBuilder& set_place_voxel(voxel_id place_voxel); + +public: + item_id build(void) const; + +private: + ItemInfo prototype; +}; + +namespace item_registry +{ +extern std::unordered_map<std::string, ItemInfoBuilder> builders; +extern std::unordered_map<std::string, item_id> names; +extern std::vector<std::shared_ptr<ItemInfo>> items; +} // namespace item_registry + +namespace item_registry +{ +ItemInfoBuilder& construct(const char* name); +ItemInfo* find(const char* name); +ItemInfo* find(const item_id item); +} // namespace item_registry + +namespace item_registry +{ +void purge(void); +} // namespace item_registry + +namespace item_registry +{ +std::uint64_t calcualte_checksum(void); +} // namespace item_registry + +#endif /* SHARED_ITEM_REGISTRY_HH */ diff --git a/src/game/shared/pch.hh b/src/game/shared/pch.hh new file mode 100644 index 0000000..1776f9b --- /dev/null +++ b/src/game/shared/pch.hh @@ -0,0 +1,25 @@ +#ifndef SHARED_PCH_HH +#define SHARED_PCH_HH 1 +#pragma once + +#include <core/pch.hh> + +#include <csignal> + +#include <BS_thread_pool.hpp> + +#include <enet/enet.h> + +#include <entt/entity/registry.hpp> +#include <entt/signal/dispatcher.hpp> + +#include <fastnoiselite.h> + +#include <miniz.h> + +#include <parson.h> + +#include <spdlog/sinks/basic_file_sink.h> +#include <spdlog/sinks/stdout_color_sinks.h> + +#endif /* SHARED_PCH_HH */ diff --git a/src/game/shared/player.hh b/src/game/shared/player.hh new file mode 100644 index 0000000..a01b4f2 --- /dev/null +++ b/src/game/shared/player.hh @@ -0,0 +1,7 @@ +#ifndef SHARED_PLAYER_HH +#define SHARED_PLAYER_HH 1 +#pragma once + +struct PlayerComponent final {}; + +#endif /* SHARED_PLAYER_HH */ diff --git a/src/game/shared/protocol.cc b/src/game/shared/protocol.cc new file mode 100644 index 0000000..0e57fe4 --- /dev/null +++ b/src/game/shared/protocol.cc @@ -0,0 +1,491 @@ +#include "shared/pch.hh" + +#include "shared/protocol.hh" + +#include "core/buffer.hh" +#include "core/floathacks.hh" + +#include "shared/chunk.hh" +#include "shared/dimension.hh" +#include "shared/globals.hh" +#include "shared/head.hh" +#include "shared/player.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" + +static ReadBuffer read_buffer; +static WriteBuffer write_buffer; + +ENetPacket* protocol::encode(const protocol::StatusRequest& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::StatusRequest::ID); + write_buffer.write_UI32(packet.version); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::StatusResponse& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::StatusResponse::ID); + write_buffer.write_UI32(packet.version); + write_buffer.write_UI16(packet.max_players); + write_buffer.write_UI16(packet.num_players); + write_buffer.write_string(packet.motd); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::LoginRequest& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::LoginRequest::ID); + write_buffer.write_UI32(packet.version); + write_buffer.write_UI64(packet.voxel_registry_checksum); + write_buffer.write_UI64(packet.item_registry_checksum); + write_buffer.write_UI64(packet.password_hash); + write_buffer.write_string(packet.username.substr(0, protocol::MAX_USERNAME)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::LoginResponse& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::LoginResponse::ID); + write_buffer.write_UI16(packet.client_index); + write_buffer.write_UI64(packet.client_identity); + write_buffer.write_UI16(packet.server_tickrate); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::Disconnect& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::Disconnect::ID); + write_buffer.write_string(packet.reason); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::ChunkVoxels& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::ChunkVoxels::ID); + write_buffer.write_I32(packet.chunk.x); + write_buffer.write_I32(packet.chunk.y); + write_buffer.write_I32(packet.chunk.z); + packet.voxels.serialize(write_buffer); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::EntityTransform& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::EntityTransform::ID); + write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity)); + write_buffer.write_I32(packet.chunk.x); + write_buffer.write_I32(packet.chunk.y); + write_buffer.write_I32(packet.chunk.z); + write_buffer.write_FP32(packet.local.x); + write_buffer.write_FP32(packet.local.y); + write_buffer.write_FP32(packet.local.z); + write_buffer.write_FP32(packet.angles.x); + write_buffer.write_FP32(packet.angles.y); + write_buffer.write_FP32(packet.angles.z); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::EntityHead& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::EntityHead::ID); + write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity)); + write_buffer.write_FP32(packet.angles.x); + write_buffer.write_FP32(packet.angles.y); + write_buffer.write_FP32(packet.angles.z); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::EntityVelocity& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::EntityVelocity::ID); + write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity)); + write_buffer.write_FP32(packet.value.x); + write_buffer.write_FP32(packet.value.y); + write_buffer.write_FP32(packet.value.z); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::SpawnPlayer& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::SpawnPlayer::ID); + write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::ChatMessage& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::ChatMessage::ID); + write_buffer.write_UI16(packet.type); + write_buffer.write_string(packet.sender.substr(0, protocol::MAX_USERNAME)); + write_buffer.write_string(packet.message.substr(0, protocol::MAX_CHAT)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::SetVoxel& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::SetVoxel::ID); + write_buffer.write_I64(packet.vpos.x); + write_buffer.write_I64(packet.vpos.y); + write_buffer.write_I64(packet.vpos.z); + write_buffer.write_UI16(packet.voxel); + write_buffer.write_UI16(packet.flags); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::RemoveEntity& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::RemoveEntity::ID); + write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::EntityPlayer& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::EntityPlayer::ID); + write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::ScoreboardUpdate& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::ScoreboardUpdate::ID); + write_buffer.write_UI16(static_cast<std::uint16_t>(packet.names.size())); + for(const std::string& username : packet.names) + write_buffer.write_string(username.substr(0, protocol::MAX_USERNAME)); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::RequestChunk& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::RequestChunk::ID); + write_buffer.write_I32(packet.cpos.x); + write_buffer.write_I32(packet.cpos.y); + write_buffer.write_I32(packet.cpos.z); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::GenericSound& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::GenericSound::ID); + write_buffer.write_string(packet.sound.substr(0, protocol::MAX_SOUNDNAME)); + write_buffer.write_UI8(packet.looping); + write_buffer.write_FP32(packet.pitch); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::EntitySound& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::EntitySound::ID); + write_buffer.write_UI64(static_cast<std::uint64_t>(packet.entity)); + write_buffer.write_string(packet.sound.substr(0, protocol::MAX_SOUNDNAME)); + write_buffer.write_UI8(packet.looping); + write_buffer.write_FP32(packet.pitch); + return write_buffer.to_packet(flags); +} + +ENetPacket* protocol::encode(const protocol::DimensionInfo& packet, enet_uint32 flags) +{ + write_buffer.reset(); + write_buffer.write_UI16(protocol::DimensionInfo::ID); + write_buffer.write_string(packet.name); + write_buffer.write_FP32(packet.gravity); + return write_buffer.to_packet(flags); +} + +void protocol::broadcast(ENetHost* host, ENetPacket* packet) +{ + if(packet) { + enet_host_broadcast(host, protocol::CHANNEL, packet); + } +} + +void protocol::broadcast(ENetHost* host, ENetPacket* packet, ENetPeer* except) +{ + if(packet) { + for(unsigned int i = 0U; i < host->peerCount; ++i) { + if(host->peers[i].state == ENET_PEER_STATE_CONNECTED) { + if(&host->peers[i] != except) { + enet_peer_send(&host->peers[i], protocol::CHANNEL, packet); + } + } + } + } +} + +void protocol::send(ENetPeer* peer, ENetPacket* packet) +{ + if(packet) { + enet_peer_send(peer, protocol::CHANNEL, packet); + } +} + +void protocol::decode(entt::dispatcher& dispatcher, const ENetPacket* packet, ENetPeer* peer) +{ + read_buffer.reset(packet); + + protocol::StatusRequest status_request; + protocol::StatusResponse status_response; + protocol::LoginRequest login_request; + protocol::LoginResponse login_response; + protocol::Disconnect disconnect; + protocol::ChunkVoxels chunk_voxels; + protocol::EntityTransform entity_transform; + protocol::EntityHead entity_head; + protocol::EntityVelocity entity_velocity; + protocol::SpawnPlayer spawn_player; + protocol::ChatMessage chat_message; + protocol::SetVoxel set_voxel; + protocol::RemoveEntity remove_entity; + protocol::EntityPlayer entity_player; + protocol::ScoreboardUpdate scoreboard_update; + protocol::RequestChunk request_chunk; + protocol::GenericSound generic_sound; + protocol::EntitySound entity_sound; + protocol::DimensionInfo dimension_info; + + auto id = read_buffer.read_UI16(); + + switch(id) { + case protocol::StatusRequest::ID: + status_request.peer = peer; + status_request.version = read_buffer.read_UI32(); + dispatcher.trigger(status_request); + break; + case protocol::StatusResponse::ID: + status_response.peer = peer; + status_response.version = read_buffer.read_UI32(); + status_response.max_players = read_buffer.read_UI16(); + status_response.num_players = read_buffer.read_UI16(); + status_response.motd = read_buffer.read_string(); + dispatcher.trigger(status_response); + break; + case protocol::LoginRequest::ID: + login_request.peer = peer; + login_request.version = read_buffer.read_UI32(); + login_request.voxel_registry_checksum = read_buffer.read_UI64(); + login_request.item_registry_checksum = read_buffer.read_UI64(); + login_request.password_hash = read_buffer.read_UI64(); + login_request.username = read_buffer.read_string(); + dispatcher.trigger(login_request); + break; + case protocol::LoginResponse::ID: + login_response.peer = peer; + login_response.client_index = read_buffer.read_UI16(); + login_response.client_identity = read_buffer.read_UI64(); + login_response.server_tickrate = read_buffer.read_UI16(); + dispatcher.trigger(login_response); + break; + case protocol::Disconnect::ID: + disconnect.peer = peer; + disconnect.reason = read_buffer.read_string(); + dispatcher.trigger(disconnect); + break; + case protocol::ChunkVoxels::ID: + chunk_voxels.peer = peer; + chunk_voxels.chunk.x = read_buffer.read_I32(); + chunk_voxels.chunk.y = read_buffer.read_I32(); + chunk_voxels.chunk.z = read_buffer.read_I32(); + chunk_voxels.voxels.deserialize(read_buffer); + dispatcher.trigger(chunk_voxels); + break; + case protocol::EntityTransform::ID: + entity_transform.peer = peer; + entity_transform.entity = static_cast<entt::entity>(read_buffer.read_UI64()); + entity_transform.chunk.x = read_buffer.read_I32(); + entity_transform.chunk.y = read_buffer.read_I32(); + entity_transform.chunk.z = read_buffer.read_I32(); + entity_transform.local.x = read_buffer.read_FP32(); + entity_transform.local.y = read_buffer.read_FP32(); + entity_transform.local.z = read_buffer.read_FP32(); + entity_transform.angles.x = read_buffer.read_FP32(); + entity_transform.angles.y = read_buffer.read_FP32(); + entity_transform.angles.z = read_buffer.read_FP32(); + dispatcher.trigger(entity_transform); + break; + case protocol::EntityHead::ID: + entity_head.peer = peer; + entity_head.entity = static_cast<entt::entity>(read_buffer.read_UI64()); + entity_head.angles[0] = read_buffer.read_FP32(); + entity_head.angles[1] = read_buffer.read_FP32(); + entity_head.angles[2] = read_buffer.read_FP32(); + dispatcher.trigger(entity_head); + break; + case protocol::EntityVelocity::ID: + entity_velocity.peer = peer; + entity_velocity.entity = static_cast<entt::entity>(read_buffer.read_UI64()); + entity_velocity.value.x = read_buffer.read_FP32(); + entity_velocity.value.y = read_buffer.read_FP32(); + entity_velocity.value.z = read_buffer.read_FP32(); + dispatcher.trigger(entity_velocity); + break; + case protocol::SpawnPlayer::ID: + spawn_player.peer = peer; + spawn_player.entity = static_cast<entt::entity>(read_buffer.read_UI64()); + dispatcher.trigger(spawn_player); + break; + case protocol::ChatMessage::ID: + chat_message.peer = peer; + chat_message.type = read_buffer.read_UI16(); + chat_message.sender = read_buffer.read_string(); + chat_message.message = read_buffer.read_string(); + dispatcher.trigger(chat_message); + break; + case protocol::SetVoxel::ID: + set_voxel.peer = peer; + set_voxel.vpos.x = read_buffer.read_I64(); + set_voxel.vpos.y = read_buffer.read_I64(); + set_voxel.vpos.z = read_buffer.read_I64(); + set_voxel.voxel = read_buffer.read_UI16(); + set_voxel.flags = read_buffer.read_UI16(); + dispatcher.trigger(set_voxel); + break; + case protocol::RemoveEntity::ID: + remove_entity.peer = peer; + remove_entity.entity = static_cast<entt::entity>(read_buffer.read_UI64()); + dispatcher.trigger(remove_entity); + break; + case protocol::EntityPlayer::ID: + entity_player.peer = peer; + entity_player.entity = static_cast<entt::entity>(read_buffer.read_UI64()); + dispatcher.trigger(entity_player); + break; + case protocol::ScoreboardUpdate::ID: + scoreboard_update.peer = peer; + scoreboard_update.names.resize(read_buffer.read_UI16()); + for(std::size_t i = 0; i < scoreboard_update.names.size(); ++i) + scoreboard_update.names[i] = read_buffer.read_string(); + dispatcher.trigger(scoreboard_update); + break; + case protocol::RequestChunk::ID: + request_chunk.peer = peer; + request_chunk.cpos.x = read_buffer.read_UI32(); + request_chunk.cpos.y = read_buffer.read_UI32(); + request_chunk.cpos.z = read_buffer.read_UI32(); + dispatcher.trigger(request_chunk); + break; + case protocol::GenericSound::ID: + generic_sound.peer = peer; + generic_sound.sound = read_buffer.read_string(); + generic_sound.looping = read_buffer.read_UI8(); + generic_sound.pitch = read_buffer.read_FP32(); + dispatcher.trigger(generic_sound); + break; + case protocol::EntitySound::ID: + entity_sound.peer = peer; + entity_sound.entity = static_cast<entt::entity>(read_buffer.read_UI64()); + entity_sound.sound = read_buffer.read_string(); + entity_sound.looping = read_buffer.read_UI8(); + entity_sound.pitch = read_buffer.read_FP32(); + dispatcher.trigger(entity_sound); + break; + case protocol::DimensionInfo::ID: + dimension_info.peer = peer; + dimension_info.name = read_buffer.read_string(); + dimension_info.gravity = read_buffer.read_FP32(); + dispatcher.trigger(dimension_info); + break; + } +} + +ENetPacket* protocol::utils::make_disconnect(const char* reason, enet_uint32 flags) +{ + protocol::Disconnect packet; + packet.reason = std::string(reason); + return protocol::encode(packet, flags); +} + +ENetPacket* protocol::utils::make_chat_message(const char* message, enet_uint32 flags) +{ + protocol::ChatMessage packet; + packet.type = protocol::ChatMessage::TEXT_MESSAGE; + packet.message = std::string(message); + return protocol::encode(packet, flags); +} + +ENetPacket* protocol::utils::make_chunk_voxels(Dimension* dimension, entt::entity entity, enet_uint32 flags) +{ + if(auto component = dimension->chunks.try_get<ChunkComponent>(entity)) { + protocol::ChunkVoxels packet; + packet.chunk = component->cpos; + packet.voxels = component->chunk->get_voxels(); + return protocol::encode(packet, flags); + } + + return nullptr; +} + +ENetPacket* protocol::utils::make_entity_head(Dimension* dimension, entt::entity entity, enet_uint32 flags) +{ + if(auto component = dimension->entities.try_get<HeadComponent>(entity)) { + protocol::EntityHead packet; + packet.entity = entity; + packet.angles = component->angles; + return protocol::encode(packet, flags); + } + + return nullptr; +} + +ENetPacket* protocol::utils::make_entity_transform(Dimension* dimension, entt::entity entity, enet_uint32 flags) +{ + if(auto component = dimension->entities.try_get<TransformComponent>(entity)) { + protocol::EntityTransform packet; + packet.entity = entity; + packet.chunk = component->chunk; + packet.local = component->local; + packet.angles = component->angles; + return protocol::encode(packet, flags); + } + + return nullptr; +} + +ENetPacket* protocol::utils::make_entity_velocity(Dimension* dimension, entt::entity entity, enet_uint32 flags) +{ + if(auto component = dimension->entities.try_get<VelocityComponent>(entity)) { + protocol::EntityVelocity packet; + packet.entity = entity; + packet.value = component->value; + return protocol::encode(packet, flags); + } + + return nullptr; +} + +ENetPacket* protocol::utils::make_entity_player(Dimension* dimension, entt::entity entity, enet_uint32 flags) +{ + if(dimension->entities.any_of<PlayerComponent>(entity)) { + protocol::EntityPlayer packet; + packet.entity = entity; + return protocol::encode(packet, flags); + } + + return nullptr; +} + +ENetPacket* protocol::utils::make_dimension_info(const Dimension* dimension) +{ + protocol::DimensionInfo packet; + packet.name = dimension->get_name(); + packet.gravity = dimension->get_gravity(); + return protocol::encode(packet, ENET_PACKET_FLAG_RELIABLE); +} diff --git a/src/game/shared/protocol.hh b/src/game/shared/protocol.hh new file mode 100644 index 0000000..5b25628 --- /dev/null +++ b/src/game/shared/protocol.hh @@ -0,0 +1,213 @@ +#ifndef SHARED_PROTOCOL_HH +#define SHARED_PROTOCOL_HH 1 +#pragma once + +#include "shared/chunk.hh" + +class Dimension; + +namespace protocol +{ +constexpr static std::size_t MAX_CHAT = 16384; +constexpr static std::size_t MAX_USERNAME = 64; +constexpr static std::size_t MAX_SOUNDNAME = 1024; +constexpr static std::uint16_t TICKRATE = 60; +constexpr static std::uint16_t PORT = 43103; +constexpr static std::uint32_t VERSION = 15; +constexpr static std::uint8_t CHANNEL = 0; +} // namespace protocol + +namespace protocol +{ +template<std::uint16_t packet_id> +struct Base { + constexpr static std::uint16_t ID = packet_id; + virtual ~Base(void) = default; + ENetPeer* peer { nullptr }; +}; +} // namespace protocol + +namespace protocol +{ +struct StatusRequest; +struct StatusResponse; +struct LoginRequest; +struct LoginResponse; +struct Disconnect; +struct ChunkVoxels; +struct EntityTransform; +struct EntityHead; +struct EntityVelocity; +struct SpawnPlayer; +struct ChatMessage; +struct SetVoxel; +struct RemoveEntity; +struct EntityPlayer; +struct ScoreboardUpdate; +struct RequestChunk; +struct GenericSound; +struct EntitySound; +struct DimensionInfo; +} // namespace protocol + +namespace protocol +{ +ENetPacket* encode(const StatusRequest& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const StatusResponse& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const LoginRequest& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const LoginResponse& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const Disconnect& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const ChunkVoxels& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const EntityTransform& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const EntityHead& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const EntityVelocity& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const SpawnPlayer& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const ChatMessage& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const SetVoxel& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const RemoveEntity& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const EntityPlayer& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const ScoreboardUpdate& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const RequestChunk& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const GenericSound& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const EntitySound& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* encode(const DimensionInfo& packet, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +} // namespace protocol + +namespace protocol +{ +void broadcast(ENetHost* host, ENetPacket* packet); +void broadcast(ENetHost* host, ENetPacket* packet, ENetPeer* except); +void send(ENetPeer* peer, ENetPacket* packet); +} // namespace protocol + +namespace protocol +{ +void decode(entt::dispatcher& dispatcher, const ENetPacket* packet, ENetPeer* peer); +} // namespace protocol + +namespace protocol::utils +{ +ENetPacket* make_disconnect(const char* reason, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* make_chat_message(const char* message, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +} // namespace protocol::utils + +namespace protocol::utils +{ +ENetPacket* make_chunk_voxels(Dimension* dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +} // namespace protocol::utils + +namespace protocol::utils +{ +ENetPacket* make_entity_head(Dimension* dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* make_entity_transform(Dimension* dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* make_entity_velocity(Dimension* dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* make_entity_player(Dimension* dimension, entt::entity entity, enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE); +ENetPacket* make_dimension_info(const Dimension* dimension); +} // namespace protocol::utils + +struct protocol::StatusRequest final : public protocol::Base<0x0000> { + std::uint32_t version; +}; + +struct protocol::StatusResponse final : public protocol::Base<0x0001> { + std::uint32_t version; + std::uint16_t max_players; + std::uint16_t num_players; + std::string motd; +}; + +struct protocol::LoginRequest final : public protocol::Base<0x0002> { + std::uint32_t version; + std::uint64_t voxel_registry_checksum; + std::uint64_t item_registry_checksum; + std::uint64_t password_hash; + std::string username; +}; + +struct protocol::LoginResponse final : public protocol::Base<0x0003> { + std::uint16_t client_index; + std::uint64_t client_identity; + std::uint16_t server_tickrate; +}; + +struct protocol::Disconnect final : public protocol::Base<0x0004> { + std::string reason; +}; + +struct protocol::ChunkVoxels final : public protocol::Base<0x0005> { + chunk_pos chunk; + VoxelStorage voxels; +}; + +struct protocol::EntityTransform final : public protocol::Base<0x0006> { + entt::entity entity; + chunk_pos chunk; + glm::fvec3 local; + glm::fvec3 angles; +}; + +struct protocol::EntityHead final : public protocol::Base<0x0007> { + entt::entity entity; + glm::fvec3 angles; +}; + +struct protocol::EntityVelocity final : public protocol::Base<0x0008> { + entt::entity entity; + glm::fvec3 value; +}; + +struct protocol::SpawnPlayer final : public protocol::Base<0x0009> { + entt::entity entity; +}; + +struct protocol::ChatMessage final : public protocol::Base<0x000A> { + constexpr static std::uint16_t TEXT_MESSAGE = 0x0000; + constexpr static std::uint16_t PLAYER_JOIN = 0x0001; + constexpr static std::uint16_t PLAYER_LEAVE = 0x0002; + + std::uint16_t type; + std::string sender; + std::string message; +}; + +struct protocol::SetVoxel final : public protocol::Base<0x000B> { + voxel_pos vpos; + voxel_id voxel; + std::uint16_t flags; +}; + +struct protocol::RemoveEntity final : public protocol::Base<0x000C> { + entt::entity entity; +}; + +struct protocol::EntityPlayer final : public protocol::Base<0x000D> { + entt::entity entity; +}; + +struct protocol::ScoreboardUpdate final : public protocol::Base<0x000E> { + std::vector<std::string> names; +}; + +struct protocol::RequestChunk final : public protocol::Base<0x000F> { + chunk_pos cpos; +}; + +struct protocol::GenericSound final : public protocol::Base<0x0010> { + std::string sound; + bool looping; + float pitch; +}; + +struct protocol::EntitySound final : public protocol::Base<0x0011> { + entt::entity entity; + std::string sound; + bool looping; + float pitch; +}; + +struct protocol::DimensionInfo final : public protocol::Base<0x0012> { + std::string name; + float gravity; +}; + +#endif /* SHARED_PROTOCOL_HH */ diff --git a/src/game/shared/ray_dda.cc b/src/game/shared/ray_dda.cc new file mode 100644 index 0000000..75d4386 --- /dev/null +++ b/src/game/shared/ray_dda.cc @@ -0,0 +1,96 @@ +#include "shared/pch.hh" + +#include "shared/ray_dda.hh" + +#include "shared/coord.hh" +#include "shared/dimension.hh" + +RayDDA::RayDDA(const Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction) +{ + reset(dimension, start_chunk, start_fpos, direction); +} + +RayDDA::RayDDA(const Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction) +{ + reset(dimension, start_chunk, start_fpos, direction); +} + +void RayDDA::reset(const Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction) +{ + this->dimension = dimension; + this->start_chunk = start_chunk; + this->start_fpos = start_fpos; + this->direction = direction; + + this->delta_dist.x = direction.x ? vx::abs(1.0f / direction.x) : std::numeric_limits<float>::max(); + this->delta_dist.y = direction.y ? vx::abs(1.0f / direction.y) : std::numeric_limits<float>::max(); + this->delta_dist.z = direction.z ? vx::abs(1.0f / direction.z) : std::numeric_limits<float>::max(); + + this->distance = 0.0f; + this->vpos = coord::to_voxel(start_chunk, start_fpos); + this->vnormal = voxel_pos(0, 0, 0); + + // Need this for initial direction calculations + auto lpos = coord::to_local(start_fpos); + + if(direction.x < 0.0f) { + this->side_dist.x = this->delta_dist.x * (start_fpos.x - lpos.x); + this->vstep.x = voxel_pos::value_type(-1); + } else { + this->side_dist.x = this->delta_dist.x * (lpos.x + 1.0f - start_fpos.x); + this->vstep.x = voxel_pos::value_type(+1); + } + + if(direction.y < 0.0f) { + this->side_dist.y = this->delta_dist.y * (start_fpos.y - lpos.y); + this->vstep.y = voxel_pos::value_type(-1); + } else { + this->side_dist.y = this->delta_dist.y * (lpos.y + 1.0f - start_fpos.y); + this->vstep.y = voxel_pos::value_type(+1); + } + + if(direction.z < 0.0f) { + this->side_dist.z = this->delta_dist.z * (start_fpos.z - lpos.z); + this->vstep.z = voxel_pos::value_type(-1); + } else { + this->side_dist.z = this->delta_dist.z * (lpos.z + 1.0f - start_fpos.z); + this->vstep.z = voxel_pos::value_type(+1); + } +} + +void RayDDA::reset(const Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction) +{ + reset(&dimension, start_chunk, start_fpos, direction); +} + +voxel_id RayDDA::step(void) +{ + if(side_dist.x < side_dist.z) { + if(side_dist.x < side_dist.y) { + vnormal = voxel_pos(-vstep.x, 0, 0); + distance = side_dist.x; + side_dist.x += delta_dist.x; + vpos.x += vstep.x; + } else { + vnormal = voxel_pos(0, -vstep.y, 0); + distance = side_dist.y; + side_dist.y += delta_dist.y; + vpos.y += vstep.y; + } + } else { + if(side_dist.z < side_dist.y) { + vnormal = voxel_pos(0, 0, -vstep.z); + distance = side_dist.z; + side_dist.z += delta_dist.z; + vpos.z += vstep.z; + } else { + vnormal = voxel_pos(0, -vstep.y, 0); + distance = side_dist.y; + side_dist.y += delta_dist.y; + vpos.y += vstep.y; + } + } + + // This is slower than I want it to be + return dimension->get_voxel(vpos); +} diff --git a/src/game/shared/ray_dda.hh b/src/game/shared/ray_dda.hh new file mode 100644 index 0000000..91eb462 --- /dev/null +++ b/src/game/shared/ray_dda.hh @@ -0,0 +1,37 @@ +#ifndef SHARED_RAY_DDA +#define SHARED_RAY_DDA 1 +#pragma once + +#include "core/macros.hh" + +#include "shared/types.hh" + +class Dimension; + +class RayDDA final { +public: + DECLARE_DEFAULT_CONSTRUCTOR(RayDDA); + explicit RayDDA(const Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction); + explicit RayDDA(const Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction); + + void reset(const Dimension* dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction); + void reset(const Dimension& dimension, const chunk_pos& start_chunk, const glm::fvec3& start_fpos, const glm::fvec3& direction); + + voxel_id step(void); + +public: + const Dimension* dimension; + chunk_pos start_chunk; + glm::fvec3 start_fpos; + glm::fvec3 direction; + + glm::fvec3 delta_dist; + glm::fvec3 side_dist; + voxel_pos vstep; + + double distance; + voxel_pos vnormal; + voxel_pos vpos; +}; + +#endif /* SHARED_RAY_DDA */ diff --git a/src/game/shared/splash.cc b/src/game/shared/splash.cc new file mode 100644 index 0000000..0cd5f50 --- /dev/null +++ b/src/game/shared/splash.cc @@ -0,0 +1,64 @@ +#include "shared/pch.hh" + +#include "shared/splash.hh" + +constexpr static const char* SPLASHES_FILENAME_CLIENT = "misc/splashes_client.txt"; +constexpr static const char* SPLASHES_FILENAME_SERVER = "misc/splashes_server.txt"; +constexpr static std::size_t SPLASH_SERVER_MAX_LENGTH = 32; + +static std::mt19937_64 splash_random; +static std::vector<std::string> splash_lines; + +static std::string sanitize_line(const std::string& line) +{ + std::string result; + + for(auto chr : line) { + if(chr != '\r' && chr != '\n') { + result.push_back(chr); + } + } + + return result; +} + +static void splash_init_filename(const char* filename) +{ + if(auto file = PHYSFS_openRead(filename)) { + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + std::string line; + std::istringstream stream(source); + + while(std::getline(stream, line)) + splash_lines.push_back(sanitize_line(line)); + splash_random.seed(std::random_device()()); + } else { + splash_lines.push_back(std::format("{}: {}", filename, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()))); + splash_random.seed(std::random_device()()); + } +} + +void splash::init_client(void) +{ + splash_init_filename(SPLASHES_FILENAME_CLIENT); +} + +void splash::init_server(void) +{ + splash_init_filename(SPLASHES_FILENAME_SERVER); + + // Server browser GUI should be able to display + // these splash messages without text clipping over + for(int i = 0; i < splash_lines.size(); i++) { + splash_lines[i] = splash_lines[i].substr(0, SPLASH_SERVER_MAX_LENGTH); + } +} + +const char* splash::get(void) +{ + std::uniform_int_distribution<std::size_t> dist(0, splash_lines.size() - 1); + return splash_lines.at(dist(splash_random)).c_str(); +} diff --git a/src/game/shared/splash.hh b/src/game/shared/splash.hh new file mode 100644 index 0000000..9510d35 --- /dev/null +++ b/src/game/shared/splash.hh @@ -0,0 +1,12 @@ +#ifndef SHARED_SPLASH_HH +#define SHARED_SPLASH_HH 1 +#pragma once + +namespace splash +{ +void init_client(void); +void init_server(void); +const char* get(void); +} // namespace splash + +#endif /* SHARED_SPLASH_HH */ diff --git a/src/game/shared/stasis.cc b/src/game/shared/stasis.cc new file mode 100644 index 0000000..462871d --- /dev/null +++ b/src/game/shared/stasis.cc @@ -0,0 +1,19 @@ +#include "shared/pch.hh" + +#include "shared/stasis.hh" + +#include "shared/dimension.hh" +#include "shared/transform.hh" + +void StasisComponent::fixed_update(Dimension* dimension) +{ + auto view = dimension->entities.view<TransformComponent>(); + + for(auto [entity, transform] : view.each()) { + if(dimension->find_chunk(transform.chunk)) { + dimension->entities.remove<StasisComponent>(entity); + } else { + dimension->entities.emplace_or_replace<StasisComponent>(entity); + } + } +} diff --git a/src/game/shared/stasis.hh b/src/game/shared/stasis.hh new file mode 100644 index 0000000..bd06d4e --- /dev/null +++ b/src/game/shared/stasis.hh @@ -0,0 +1,14 @@ +#ifndef SHARED_STASIS_HH +#define SHARED_STASIS_HH 1 +#pragma once + +class Dimension; + +// Attached to entities with transform values +// out of bounds in a specific dimension +struct StasisComponent final { +public: + static void fixed_update(Dimension* dimension); +}; + +#endif /* SHARED_STASIS_HH */ diff --git a/src/game/shared/threading.cc b/src/game/shared/threading.cc new file mode 100644 index 0000000..9e46d17 --- /dev/null +++ b/src/game/shared/threading.cc @@ -0,0 +1,111 @@ +#include "shared/pch.hh" + +#include "shared/threading.hh" + +#include "core/cmdline.hh" +#include "core/constexpr.hh" + +constexpr static const char* DEFAULT_POOL_SIZE_ARG = "4"; + +static BS::light_thread_pool* thread_pool; +static std::deque<Task*> task_deque; + +static void task_process(Task* task) +{ + task->set_status(task_status::PROCESSING); + task->process(); + + if(task->get_status() == task_status::PROCESSING) { + // If the task status is still PROCESSING + // it can be deduced it hasn't been cancelled + task->set_status(task_status::COMPLETED); + } +} + +task_status Task::get_status(void) const +{ + return m_status; +} + +void Task::set_status(task_status status) +{ + m_status = status; +} + +void threading::init(void) +{ + auto argument = cmdline::get("threads", DEFAULT_POOL_SIZE_ARG); + auto num_concurrent_threads = std::thread::hardware_concurrency(); + unsigned int thread_pool_size; + + if(num_concurrent_threads && !std::strcmp(argument, "max")) { + // Use the maximum available number of concurrent + // hardware threads provided by the implementation + thread_pool_size = num_concurrent_threads; + } else { + if(num_concurrent_threads) { + thread_pool_size = vx::clamp<unsigned int>(std::strtoul(argument, nullptr, 10), 1U, num_concurrent_threads); + } else { + thread_pool_size = vx::max<unsigned int>(std::strtoul(argument, nullptr, 10), 1U); + } + } + + spdlog::info("threading: using {} threads for pooling tasks", thread_pool_size); + + thread_pool = new BS::light_thread_pool(thread_pool_size); + + task_deque.clear(); +} + +void threading::deinit(void) +{ + for(auto task : task_deque) { + auto status = task->get_status(); + if((status != task_status::CANCELLED) || (status != task_status::COMPLETED)) { + task->set_status(task_status::CANCELLED); + } + } + + thread_pool->purge(); + thread_pool->wait(); + + for(auto task : task_deque) + delete task; + task_deque.clear(); + + delete thread_pool; +} + +void threading::update(void) +{ + auto task_iter = task_deque.cbegin(); + + while(task_iter != task_deque.cend()) { + auto task_ptr = *task_iter; + auto status = task_ptr->get_status(); + + if(status == task_status::CANCELLED) { + delete task_ptr; + task_iter = task_deque.erase(task_iter); + continue; + } + + if(status == task_status::COMPLETED) { + task_ptr->finalize(); + delete task_ptr; + task_iter = task_deque.erase(task_iter); + continue; + } + + task_iter = std::next(task_iter); + } +} + +void threading::detail::submit_new(Task* task) +{ + task->set_status(task_status::ENQUEUED); + + static_cast<void>(thread_pool->submit_task(std::bind(&task_process, task))); + + task_deque.push_back(task); +} diff --git a/src/game/shared/threading.hh b/src/game/shared/threading.hh new file mode 100644 index 0000000..bce4811 --- /dev/null +++ b/src/game/shared/threading.hh @@ -0,0 +1,50 @@ +#ifndef SHARED_THREADING_HH +#define SHARED_THREADING_HH 1 +#pragma once + +enum class task_status : unsigned int { + ENQUEUED = 0x0000U, + PROCESSING = 0x0001U, + COMPLETED = 0x0002U, + CANCELLED = 0x0004U, +}; + +class Task { +public: + virtual ~Task(void) = default; + virtual void process(void) = 0; + virtual void finalize(void) = 0; + + task_status get_status(void) const; + void set_status(task_status status); + +protected: + std::atomic<task_status> m_status; + std::future<void> m_future; +}; + +namespace threading +{ +void init(void); +void deinit(void); +void update(void); +} // namespace threading + +namespace threading::detail +{ +void submit_new(Task* task); +} // namespace threading::detail + +namespace threading +{ +template<typename T, typename... AT> +void submit(AT&&... args); +} // namespace threading + +template<typename T, typename... AT> +inline void threading::submit(AT&&... args) +{ + threading::detail::submit_new(new T(args...)); +} + +#endif /* SHARED_THREADING_HH */ diff --git a/src/game/shared/transform.cc b/src/game/shared/transform.cc new file mode 100644 index 0000000..f4b661c --- /dev/null +++ b/src/game/shared/transform.cc @@ -0,0 +1,32 @@ +#include "shared/pch.hh" + +#include "shared/transform.hh" + +#include "shared/const.hh" +#include "shared/dimension.hh" + +constexpr inline static void update_component(unsigned int dim, TransformComponent& component) +{ + if(component.local[dim] >= CHUNK_SIZE) { + component.local[dim] -= CHUNK_SIZE; + component.chunk[dim] += 1; + return; + } + + if(component.local[dim] < 0.0f) { + component.local[dim] += CHUNK_SIZE; + component.chunk[dim] -= 1; + return; + } +} + +void TransformComponent::fixed_update(Dimension* dimension) +{ + auto view = dimension->entities.view<TransformComponent>(); + + for(auto [entity, transform] : view.each()) { + update_component(0U, transform); + update_component(1U, transform); + update_component(2U, transform); + } +} diff --git a/src/game/shared/transform.hh b/src/game/shared/transform.hh new file mode 100644 index 0000000..f9c5f47 --- /dev/null +++ b/src/game/shared/transform.hh @@ -0,0 +1,25 @@ +#ifndef SHARED_TRANSFORM_HH +#define SHARED_TRANSFORM_HH 1 +#pragma once + +#include "shared/types.hh" + +class Dimension; + +struct TransformComponent { + chunk_pos chunk; + glm::fvec3 local; + glm::fvec3 angles; + +public: + // Updates TransformComponent values so that + // the local translation field is always within + // local coodrinates; [floating-point precision] + static void fixed_update(Dimension* dimension); +}; + +// Client-side only - interpolated and previous transform +struct TransformComponentIntr final : public TransformComponent {}; +struct TransformComponentPrev final : public TransformComponent {}; + +#endif /* SHARED_TRANSFORM_HH */ diff --git a/src/game/shared/types.hh b/src/game/shared/types.hh new file mode 100644 index 0000000..85fbd19 --- /dev/null +++ b/src/game/shared/types.hh @@ -0,0 +1,44 @@ +#ifndef SHARED_TYPES_HH +#define SHARED_TYPES_HH 1 +#pragma once + +using item_id = std::uint32_t; +constexpr static item_id NULL_ITEM_ID = UINT32_C(0x00000000); +constexpr static item_id MAX_ITEM_ID = UINT32_C(0xFFFFFFFF); + +using voxel_id = std::uint16_t; +constexpr static voxel_id NULL_VOXEL_ID = UINT16_C(0x0000); +constexpr static voxel_id MAX_VOXEL_ID = UINT16_C(0xFFFF); + +using chunk_pos = glm::vec<3, std::int32_t>; +using local_pos = glm::vec<3, std::int16_t>; +using voxel_pos = glm::vec<3, std::int64_t>; + +using chunk_pos_xz = glm::vec<2, chunk_pos::value_type>; +using local_pos_xz = glm::vec<2, local_pos::value_type>; +using voxel_pos_xz = glm::vec<2, local_pos::value_type>; + +template<> +struct std::hash<chunk_pos> final { + constexpr inline std::size_t operator()(const chunk_pos& cpos) const + { + std::size_t value = 0; + value ^= cpos.x * 73856093; + value ^= cpos.y * 19349663; + value ^= cpos.z * 83492791; + return value; + } +}; + +template<> +struct std::hash<chunk_pos_xz> final { + constexpr inline std::size_t operator()(const chunk_pos_xz& cwpos) const + { + std::size_t value = 0; + value ^= cwpos.x * 73856093; + value ^= cwpos.y * 19349663; + return value; + } +}; + +#endif /* SHARED_TYPES_HH */ diff --git a/src/game/shared/velocity.cc b/src/game/shared/velocity.cc new file mode 100644 index 0000000..329dc91 --- /dev/null +++ b/src/game/shared/velocity.cc @@ -0,0 +1,17 @@ +#include "shared/pch.hh" + +#include "shared/velocity.hh" + +#include "shared/dimension.hh" +#include "shared/globals.hh" +#include "shared/stasis.hh" +#include "shared/transform.hh" + +void VelocityComponent::fixed_update(Dimension* dimension) +{ + auto group = dimension->entities.group<VelocityComponent>(entt::get<TransformComponent>, entt::exclude<StasisComponent>); + + for(auto [entity, velocity, transform] : group.each()) { + transform.local += velocity.value * globals::fixed_frametime; + } +} diff --git a/src/game/shared/velocity.hh b/src/game/shared/velocity.hh new file mode 100644 index 0000000..45a2858 --- /dev/null +++ b/src/game/shared/velocity.hh @@ -0,0 +1,17 @@ +#ifndef SHARED_VELOCITY_HH +#define SHARED_VELOCITY_HH 1 +#pragma once + +class Dimension; + +struct VelocityComponent final { + glm::fvec3 value; + +public: + // Updates entities TransformComponent values + // according to velocities multiplied by fixed_frametime. + // NOTE: This system was previously called inertial + static void fixed_update(Dimension* dimension); +}; + +#endif /* SHARED_VELOCITY_HH */ diff --git a/src/game/shared/voxel_registry.cc b/src/game/shared/voxel_registry.cc new file mode 100644 index 0000000..cb3c724 --- /dev/null +++ b/src/game/shared/voxel_registry.cc @@ -0,0 +1,192 @@ +#include "shared/pch.hh" + +#include "shared/voxel_registry.hh" + +#include "core/crc64.hh" + +std::unordered_map<std::string, VoxelInfoBuilder> voxel_registry::builders = {}; +std::unordered_map<std::string, voxel_id> voxel_registry::names = {}; +std::vector<std::shared_ptr<VoxelInfo>> voxel_registry::voxels = {}; + +VoxelInfoBuilder::VoxelInfoBuilder(const char* name, voxel_type type, bool animated, bool blending) +{ + prototype.name = name; + prototype.type = type; + prototype.animated = animated; + prototype.blending = blending; + + switch(type) { + case voxel_type::CUBE: + prototype.textures.resize(static_cast<std::size_t>(voxel_face::CUBE__NR)); + break; + case voxel_type::CROSS: + prototype.textures.resize(static_cast<std::size_t>(voxel_face::CROSS__NR)); + break; + case voxel_type::MODEL: + // Custom models should use a different texture + // resource management that is not a voxel atlas + // TODO: actually implement custom models lol + prototype.textures.resize(0); + break; + default: + // Something really bad should happen if we end up here. + // The outside code would static_cast an int to VoxelType + // and possibly fuck a lot of things up to cause this + spdlog::critical("voxel_registry: {}: unknown voxel type {}", name, static_cast<int>(type)); + std::terminate(); + } + + // Physics properties + prototype.touch_type = voxel_touch::SOLID; + prototype.touch_values = glm::fvec3(0.0f, 0.0f, 0.0f); + prototype.surface = voxel_surface::DEFAULT; + + // Things set in future by item_def + prototype.item_pick = NULL_ITEM_ID; +} + +VoxelInfoBuilder& VoxelInfoBuilder::add_texture_default(const char* texture) +{ + default_texture.paths.push_back(texture); + return *this; +} + +VoxelInfoBuilder& VoxelInfoBuilder::add_texture(voxel_face face, const char* texture) +{ + const auto index = static_cast<std::size_t>(face); + prototype.textures[index].paths.push_back(texture); + return *this; +} + +VoxelInfoBuilder& VoxelInfoBuilder::set_touch(voxel_touch type, const glm::fvec3& values) +{ + prototype.touch_type = type; + prototype.touch_values = values; + return *this; +} + +VoxelInfoBuilder& VoxelInfoBuilder::set_surface(voxel_surface surface) +{ + prototype.surface = surface; + return *this; +} + +voxel_id VoxelInfoBuilder::build(void) const +{ + const auto it = voxel_registry::names.find(prototype.name); + + if(it != voxel_registry::names.cend()) { + spdlog::warn("voxel_registry: cannot build {}: name already present", prototype.name); + return it->second; + } + + std::size_t state_count; + + switch(prototype.type) { + case voxel_type::CUBE: + case voxel_type::CROSS: + case voxel_type::MODEL: + state_count = 1; + break; + default: + // Something really bad should happen if we end up here. + // The outside code would static_cast an int to VoxelType + // and possibly fuck a lot of things up to cause this + spdlog::critical("voxel_registry: {}: unknown voxel type {}", prototype.name, static_cast<int>(prototype.type)); + std::terminate(); + } + + if((voxel_registry::voxels.size() + state_count) >= MAX_VOXEL_ID) { + spdlog::critical("voxel_registry: voxel registry overflow"); + std::terminate(); + } + + auto new_info = std::make_shared<VoxelInfo>(); + new_info->name = prototype.name; + new_info->type = prototype.type; + new_info->animated = prototype.animated; + new_info->blending = prototype.blending; + + new_info->textures.resize(prototype.textures.size()); + + for(std::size_t i = 0; i < prototype.textures.size(); ++i) { + if(prototype.textures[i].paths.empty()) { + new_info->textures[i].paths = default_texture.paths; + new_info->textures[i].cached_offset = SIZE_MAX; + new_info->textures[i].cached_plane = SIZE_MAX; + } else { + new_info->textures[i].paths = prototype.textures[i].paths; + new_info->textures[i].cached_offset = SIZE_MAX; + new_info->textures[i].cached_plane = SIZE_MAX; + } + } + + // Physics properties + new_info->touch_type = prototype.touch_type; + new_info->touch_values = prototype.touch_values; + new_info->surface = prototype.surface; + + // Things set in future by item_def + new_info->item_pick = prototype.item_pick; + + // Base voxel identifier offset + new_info->base_voxel = voxel_registry::voxels.size() + 1; + + for(std::size_t i = 0; i < state_count; ++i) + voxel_registry::voxels.push_back(new_info); + voxel_registry::names.insert_or_assign(new_info->name, new_info->base_voxel); + + return new_info->base_voxel; +} + +VoxelInfoBuilder& voxel_registry::construct(const char* name, voxel_type type, bool animated, bool blending) +{ + const auto it = voxel_registry::builders.find(name); + + if(it != voxel_registry::builders.cend()) { + return it->second; + } else { + return voxel_registry::builders.emplace(name, VoxelInfoBuilder(name, type, animated, blending)).first->second; + } +} + +VoxelInfo* voxel_registry::find(const char* name) +{ + const auto it = voxel_registry::names.find(name); + + if(it != voxel_registry::names.cend()) { + return voxel_registry::find(it->second); + } else { + return nullptr; + } +} + +VoxelInfo* voxel_registry::find(const voxel_id voxel) +{ + if((voxel != NULL_VOXEL_ID) && (voxel <= voxel_registry::voxels.size())) { + return voxel_registry::voxels[voxel - 1].get(); + } else { + return nullptr; + } +} + +void voxel_registry::purge(void) +{ + voxel_registry::builders.clear(); + voxel_registry::names.clear(); + voxel_registry::voxels.clear(); +} + +std::uint64_t voxel_registry::calcualte_checksum(void) +{ + std::uint64_t result = 0; + + for(const std::shared_ptr<VoxelInfo>& info : voxel_registry::voxels) { + result = crc64::get(info->name, result); + result += static_cast<std::uint64_t>(info->type); + result += static_cast<std::uint64_t>(info->base_voxel); + result += info->blending ? 256 : 1; + } + + return result; +} diff --git a/src/game/shared/voxel_registry.hh b/src/game/shared/voxel_registry.hh new file mode 100644 index 0000000..5f7963f --- /dev/null +++ b/src/game/shared/voxel_registry.hh @@ -0,0 +1,144 @@ +#ifndef SHARED_VOXEL_REGISTRY_HH +#define SHARED_VOXEL_REGISTRY_HH 1 +#pragma once + +#include "shared/types.hh" + +enum class voxel_face : unsigned short { + CUBE_NORTH = 0x0000, + CUBE_SOUTH = 0x0001, + CUBE_EAST = 0x0002, + CUBE_WEST = 0x0003, + CUBE_TOP = 0x0004, + CUBE_BOTTOM = 0x0005, + CUBE__NR = 0x0006, + + CROSS_NESW = 0x0000, + CROSS_NWSE = 0x0001, + CROSS__NR = 0x0002, +}; + +enum class voxel_type : unsigned short { + CUBE = 0x0000, + CROSS = 0x0001, // TODO + MODEL = 0x0002, // TODO +}; + +enum class voxel_facing : unsigned short { + NORTH = 0x0000, + SOUTH = 0x0001, + EAST = 0x0002, + WEST = 0x0003, + UP = 0x0004, + DOWN = 0x0005, + NESW = 0x0006, + NWSE = 0x0007, +}; + +enum class voxel_touch : unsigned short { + SOLID = 0x0000, // The entity is stopped in its tracks + BOUNCE = 0x0001, // The entity bounces back with some energy loss + SINK = 0x0002, // The entity phases/sinks through the voxel + NOTHING = 0xFFFF, +}; + +enum class voxel_surface : unsigned short { + DEFAULT = 0x0000, + STONE = 0x0001, + DIRT = 0x0002, + GLASS = 0x0003, + GRASS = 0x0004, + GRAVEL = 0x0005, + METAL = 0x0006, + SAND = 0x0007, + WOOD = 0x0008, + SLOSH = 0x0009, + COUNT = 0x000A, + UNKNOWN = 0xFFFF, +}; + +using voxel_vis = unsigned short; +constexpr static voxel_vis VIS_NORTH = 1 << static_cast<unsigned int>(voxel_facing::NORTH); +constexpr static voxel_vis VIS_SOUTH = 1 << static_cast<unsigned int>(voxel_facing::SOUTH); +constexpr static voxel_vis VIS_EAST = 1 << static_cast<unsigned int>(voxel_facing::EAST); +constexpr static voxel_vis VIS_WEST = 1 << static_cast<unsigned int>(voxel_facing::WEST); +constexpr static voxel_vis VIS_UP = 1 << static_cast<unsigned int>(voxel_facing::UP); +constexpr static voxel_vis VIS_DOWN = 1 << static_cast<unsigned int>(voxel_facing::DOWN); + +struct VoxelTexture final { + std::vector<std::string> paths; + std::size_t cached_offset; // client-side only + std::size_t cached_plane; // client-side only +}; + +struct VoxelInfo final { + std::string name; + voxel_type type; + bool animated; + bool blending; + + std::vector<VoxelTexture> textures; + + // Physics properties go here + // TODO: player_move friction modifiers + // that would make the voxel very sticky or + // very slippery to walk on + voxel_touch touch_type; + glm::fvec3 touch_values; + voxel_surface surface; + + // Some voxel types might occupy multiple voxel_id + // values that reference to the exact same VoxelInfo + // structure; the actual numeric state is figured out by + // subtracting base_voxel from the checking voxel_id + voxel_id base_voxel; + + // These will be set by item_registry + // and by default set to NULL_ITEM_ID + item_id item_pick; +}; + +class VoxelInfoBuilder final { +public: + explicit VoxelInfoBuilder(const char* name, voxel_type type, bool animated, bool blending); + virtual ~VoxelInfoBuilder(void) = default; + +public: + VoxelInfoBuilder& add_texture_default(const char* texture); + VoxelInfoBuilder& add_texture(voxel_face face, const char* texture); + VoxelInfoBuilder& set_touch(voxel_touch type, const glm::fvec3& values); + VoxelInfoBuilder& set_surface(voxel_surface surface); + +public: + voxel_id build(void) const; + +private: + VoxelTexture default_texture; + VoxelInfo prototype; +}; + +namespace voxel_registry +{ +extern std::unordered_map<std::string, VoxelInfoBuilder> builders; +extern std::unordered_map<std::string, voxel_id> names; +extern std::vector<std::shared_ptr<VoxelInfo>> voxels; +} // namespace voxel_registry + +namespace voxel_registry +{ +VoxelInfoBuilder& construct(const char* name, voxel_type type, bool animated, bool blending); +VoxelInfo* find(const char* name); +VoxelInfo* find(const voxel_id voxel); +} // namespace voxel_registry + +namespace voxel_registry +{ +void purge(void); +} // namespace voxel_registry + +namespace voxel_registry +{ +std::uint64_t calcualte_checksum(void); +} // namespace voxel_registry + +#endif /* SHARED_VOXEL_REGISTRY_HH */ diff --git a/src/game/shared/voxel_storage.cc b/src/game/shared/voxel_storage.cc new file mode 100644 index 0000000..f2c4d42 --- /dev/null +++ b/src/game/shared/voxel_storage.cc @@ -0,0 +1,46 @@ +#include "shared/pch.hh" + +#include "shared/voxel_storage.hh" + +#include "core/buffer.hh" + +void VoxelStorage::serialize(WriteBuffer& buffer) const +{ + auto bound = mz_compressBound(sizeof(VoxelStorage)); + auto zdata = std::vector<unsigned char>(bound); + + VoxelStorage net_storage; + + for(std::size_t i = 0; i < CHUNK_VOLUME; ++i) { + // Convert voxel indices into network byte order; + // We're going to compress them but we still want + // the order to be consistent across all the platforms + net_storage[i] = ENET_HOST_TO_NET_16(at(i)); + } + + mz_compress(zdata.data(), &bound, reinterpret_cast<unsigned char*>(net_storage.data()), sizeof(VoxelStorage)); + + buffer.write_UI64(bound); + + // Write all the compressed data into the buffer + for(std::size_t i = 0; i < bound; buffer.write_UI8(zdata[i++])) + ; +} + +void VoxelStorage::deserialize(ReadBuffer& buffer) +{ + auto size = static_cast<mz_ulong>(sizeof(VoxelStorage)); + auto bound = static_cast<mz_ulong>(buffer.read_UI64()); + auto zdata = std::vector<unsigned char>(bound); + + // Read all the compressed data from the buffer + for(std::size_t i = 0; i < bound; zdata[i++] = buffer.read_UI8()) + ; + + mz_uncompress(reinterpret_cast<unsigned char*>(data()), &size, zdata.data(), bound); + + for(std::size_t i = 0; i < CHUNK_VOLUME; ++i) { + // Convert voxel indices back into the host byte order + at(i) = ENET_NET_TO_HOST_16(at(i)); + } +} diff --git a/src/game/shared/voxel_storage.hh b/src/game/shared/voxel_storage.hh new file mode 100644 index 0000000..a31cf3e --- /dev/null +++ b/src/game/shared/voxel_storage.hh @@ -0,0 +1,18 @@ +#ifndef SHARED_VOXEL_STORAGE_HH +#define SHARED_VOXEL_STORAGE_HH 1 +#pragma once + +#include "shared/const.hh" +#include "shared/types.hh" + +class ReadBuffer; +class WriteBuffer; + +class VoxelStorage final : public std::array<voxel_id, CHUNK_VOLUME> { +public: + using std::array<voxel_id, CHUNK_VOLUME>::array; + void serialize(WriteBuffer& buffer) const; + void deserialize(ReadBuffer& buffer); +}; + +#endif /* SHARED_VOXEL_STORAGE_HH */ |
