diff --git a/TODO.md b/TODO.md index 7fe65d1..5d0ec96 100644 --- a/TODO.md +++ b/TODO.md @@ -9,7 +9,8 @@ ## Hello other - [~] Multiplayer - - [ ] Chat + - [~] Chat + - Private messages - [~] Authentication - [x] Compression - [ ] Encryption @@ -64,6 +65,7 @@ - [ ] Slash screen - [ ] QUIC protocal + - [ ] Use in memory protocol (to replace server_handle) - [ ] Octree - [ ] Better Lod - [ ] VK diff --git a/src/client/Client.cpp b/src/client/Client.cpp index bc70e72..a1388e9 100644 --- a/src/client/Client.cpp +++ b/src/client/Client.cpp @@ -29,6 +29,11 @@ void Client::run(server_handle* const localHandle) { world->onTeleport = [&](voxel_pos pos) { state.position = player.position = pos; }; + world->onMessage = [&](const std::string &text) { + // MAYBE: rolling buffer + const auto yellow = std::make_optional(0xFF43F5F5); + state.console.lines.push_back(state::state::line{text, text[0] == '>' ? yellow : std::nullopt}); + }; do { window.startFrame(); @@ -116,6 +121,14 @@ void Client::run(server_handle* const localHandle) { if(actions && render::UI::Actions::FillMode) { pipeline->setFillMode(options.renderer.wireframe); } + if(actions && render::UI::Actions::Message) { + char *s = state.console.buffer.data(); + //Strtrim(s); + //if (s[0]) + // ExecCommand(s); + world->emit(world::action::Message(s)); + strcpy(s, ""); + } } { // Rendering ZoneScopedN("Render"); diff --git a/src/client/Window.cpp b/src/client/Window.cpp index 2bf72c3..0dbaa21 100644 --- a/src/client/Window.cpp +++ b/src/client/Window.cpp @@ -62,8 +62,6 @@ bool Window::create(const CreateInfo &opt) { } glfwMakeContextCurrent(ptr); - // Ensure we can capture the escape key being pressed below - glfwSetInputMode(ptr, GLFW_STICKY_KEYS, GL_TRUE); // Hide the mouse and enable unlimited mouvement glfwSetInputMode(ptr, GLFW_CURSOR, GLFW_CURSOR_DISABLED); diff --git a/src/client/config.hpp b/src/client/config.hpp index 4e4aa6c..9481985 100644 --- a/src/client/config.hpp +++ b/src/client/config.hpp @@ -82,6 +82,10 @@ public: editor.tool.material = config["editor"]["tool"]["material"].value_or(editor.tool.material); editor.tool.emptyAir = config["editor"]["tool"]["empty_air"].value_or(editor.tool.emptyAir); + console.visible = config["console"]["visible"].value_or(console.visible); + console.scroll = config["console"]["scroll"].value_or(console.scroll); + console.opacity = config["console"]["opacity"].value_or(console.opacity); + debugMenu.bar = config["debug_menu"]["bar"].value_or(debugMenu.bar); debugMenu.render = config["debug_menu"]["render"].value_or(debugMenu.render); debugMenu.world = config["debug_menu"]["world"].value_or(debugMenu.world); @@ -155,6 +159,11 @@ public: {"empty_air", editor.tool.emptyAir} })} })); + config.insert_or_assign("console", toml::table({ + {"visible", console.visible}, + {"scroll", console.scroll}, + {"opacity", console.opacity} + })); config.insert_or_assign("debug_menu", toml::table({ {"bar", debugMenu.bar}, {"render", debugMenu.render}, @@ -200,6 +209,12 @@ public: } tool; } editor; + struct { + bool visible = true; + bool scroll = true; + float opacity = .8f; + } console; + struct { bool visible = true; int corner = 3; diff --git a/src/client/render/UI.cpp b/src/client/render/UI.cpp index 18674f5..8982e5e 100644 --- a/src/client/render/UI.cpp +++ b/src/client/render/UI.cpp @@ -50,6 +50,7 @@ UI::Actions UI::draw(config::client::options &options, state::state &state, cons ImGui::EndMenu(); } ImGui::Checkbox("Editor", &options.editor.visible); + ImGui::Checkbox("Console", &options.console.visible); if(ImGui::MenuItem("Close")) options.debugMenu.bar = false; @@ -230,36 +231,94 @@ UI::Actions UI::draw(config::client::options &options, state::state &state, cons ImGui::End(); } - /*if (show_console) { - ImGui::SetNextWindowPos(ImVec2(UI_MARGIN, 500), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(200, 100), ImGuiCond_FirstUseEver); - ImGui::Begin("Console", &show_console, ImGuiWindowFlags_MenuBar); + if (options.console.visible) { + { + const auto SIZE = ImVec2(300, 200); + ImGui::SetNextWindowSize(SIZE, ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(UI_MARGIN, io.DisplaySize.y - SIZE.y - UI_MARGIN), ImGuiCond_FirstUseEver); + } + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, options.console.opacity); + ImGui::Begin("Console", &options.console.visible, ImGuiWindowFlags_MenuBar); ImGui::BeginMenuBar(); - ImGui::MenuItem("Auto-scrool", NULL, &console_scrool); + if(ImGui::BeginMenu("Options")) { + ImGui::Checkbox("Auto-scrool", &options.console.scroll); + ImGui::SliderFloat("Opacity", &options.console.opacity, 0.1, 1); + ImGui::EndMenu(); + } + if (ImGui::Button("Clear")) + state.console.lines.clear(); + const bool copy_to_clipboard = ImGui::Button("Copy"); ImGui::EndMenuBar(); - // TODO: text + // Reserve enough left-over height for 1 separator + 1 input text + const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); + ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), false, ImGuiWindowFlags_HorizontalScrollbar); + /*if (ImGui::BeginPopupContextWindow()) { + if (ImGui::Selectable("Clear")) ClearLog(); + ImGui::EndPopup(); + }*/ + + // Display every line as a separate entry so we can change their color or add custom widgets. + // If you only want raw text you can use ImGui::TextUnformatted(log.begin(), log.end()); + // NB- if you have thousands of entries this approach may be too inefficient and may require user-side clipping + // to only process visible items. The clipper will automatically measure the height of your first item and then + // "seek" to display only items in the visible area. + // To use the clipper we can replace your standard loop: + // for (int i = 0; i < Items.Size; i++) + // With: + // ImGuiListClipper clipper(Items.Size); + // while (clipper.Step()) + // for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + // - That your items are evenly spaced (same height) + // - That you have cheap random access to your elements (you can access them given their index, + // without processing all the ones before) + // You cannot this code as-is if a filter is active because it breaks the 'cheap random-access' property. + // We would need random-access on the post-filtered list. + // A typical application wanting coarse clipping and filtering may want to pre-compute an array of indices + // or offsets of items that passed the filtering test, recomputing this array when user changes the filter, + // and appending newly elements as they are inserted. This is left as a task to the user until we can manage + // to improve this example code! + // If your items are of variable height: + // - Split them into same height items would be simpler and facilitate random-seeking into your list. + // - Consider using manual call to IsRectVisible() and skipping extraneous decoration from your items. + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1)); // Tighten spacing + if (copy_to_clipboard) + ImGui::LogToClipboard(); + for (const auto& item: state.console.lines) { + /*if (!Filter.PassFilter(item)) + continue;*/ + + if (item.color.has_value()) + ImGui::PushStyleColor(ImGuiCol_Text, item.color.value()); + ImGui::TextUnformatted(item.text.c_str()); + if (item.color.has_value()) + ImGui::PopStyleColor(); + } + if (copy_to_clipboard) + ImGui::LogFinish(); + + if (options.console.scroll || ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) + ImGui::SetScrollHereY(1.0f); + + ImGui::PopStyleVar(); + ImGui::EndChild(); ImGui::Separator(); // Command-line - bool reclaim_focus = false; - // TODO: completion callback - if (ImGui::InputText("Input", console_buffer, IM_ARRAYSIZE(console_buffer), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory)) { - char *s = console_buffer; - //Strtrim(s); - //if (s[0]) - // ExecCommand(s); - strcpy(s, ""); - reclaim_focus = true; - } + const auto flags = ImGuiInputTextFlags_EnterReturnsTrue; + // | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory; + ImGui::GetColumnWidth(); + if (ImGui::InputText("", state.console.buffer.data(), state.console.buffer.size(), flags /*, TODO: callback, context*/) && strlen(state.console.buffer.data()) > 0) + actions |= Actions::Message; // Auto-focus on window apparition ImGui::SetItemDefaultFocus(); - if (reclaim_focus) + if (actions && Actions::Message) ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget ImGui::End(); - }*/ + ImGui::PopStyleVar(); + } if (options.overlay.visible) { if (options.overlay.corner != -1) { diff --git a/src/client/render/UI.hpp b/src/client/render/UI.hpp index bccba56..b0bfbcc 100644 --- a/src/client/render/UI.hpp +++ b/src/client/render/UI.hpp @@ -36,6 +36,7 @@ public: Camera = 1 << 6, Control = 1 << 7, FillMode = 1 << 8, + Message = 1 << 9, }; friend inline void operator|=(Actions& a, Actions b) { a = static_cast(static_cast(a) | static_cast(b)); diff --git a/src/client/state.hpp b/src/client/state.hpp index 7e9260d..71f7cce 100644 --- a/src/client/state.hpp +++ b/src/client/state.hpp @@ -12,7 +12,14 @@ struct state { contouring::Abstract* contouring; - std::array console_buffer; + struct line { + std::string text; + std::optional color; + }; + struct { + std::array buffer; + std::vector lines; + } console; }; /// Readonly metrics diff --git a/src/client/world/DistantUniverse.cpp b/src/client/world/DistantUniverse.cpp index a993364..911e207 100644 --- a/src/client/world/DistantUniverse.cpp +++ b/src/client/world/DistantUniverse.cpp @@ -102,6 +102,12 @@ void DistantUniverse::pullNetwork(voxel_pos pos) { break; } + case server_packet_type::MESSAGE: { + const auto ref = PacketReader(packet, true).remaning(); + onMessage(std::string(static_cast(ref.data()), ref.size())); + break; + } + case server_packet_type::AREAS: { auto reader = PacketReader(packet, true); while(!reader.isFull()) { @@ -210,6 +216,8 @@ void DistantUniverse::pullNetwork(voxel_pos pos) { void DistantUniverse::emit(const action::packet &action) { if(const auto move = std::get_if(&action)) { peer.send(net::client_packet_type::MOVE, move->pos, net::channel_type::NOTIFY); + } else if(const auto message = std::get_if(&action)) { + peer.send(net::client_packet_type::MESSAGE, message->text.data(), message->text.size(), net::channel_type::RELIABLE); } else if(const auto fillCube = std::get_if(&action)) { peer.send(net::client_packet_type::FILL_CUBE, *fillCube, net::channel_type::RELIABLE); } else { diff --git a/src/client/world/LocalUniverse.cpp b/src/client/world/LocalUniverse.cpp index d2a6667..4e15dbe 100644 --- a/src/client/world/LocalUniverse.cpp +++ b/src/client/world/LocalUniverse.cpp @@ -28,6 +28,10 @@ void LocalUniverse::update(voxel_pos pos, float) { onTeleport(pos); handle->teleport = std::nullopt; } + if (handle->message.has_value() && onMessage) { + onMessage(handle->message.value()); + handle->message = std::nullopt; + } const auto cur_chunk = glm::divide(pos); const auto chunkChange = cur_chunk != last_chunk; diff --git a/src/client/world/Universe.hpp b/src/client/world/Universe.hpp index 09d6bf3..25c83bc 100644 --- a/src/client/world/Universe.hpp +++ b/src/client/world/Universe.hpp @@ -26,6 +26,8 @@ namespace world::client { virtual void emit(const action::packet &) = 0; /// When server teleport player std::function onTeleport; + /// On chat message + std::function onMessage; /// Get current contouring worker contouring::Abstract* getContouring() const { diff --git a/src/core/net/data.hpp b/src/core/net/data.hpp index 1398d9d..91a14b1 100644 --- a/src/core/net/data.hpp +++ b/src/core/net/data.hpp @@ -50,6 +50,10 @@ enum class server_packet_type: enet_uint8 { /// Server capabilities /// ushort(loadDistance), MAYBE: more reliable CAPABILITIES = 25, + + /// Public chat message + /// char[] (not null terminated) reliable + MESSAGE = 29, }; enum class client_packet_type: enet_uint8 { /// Interact with voxels @@ -60,6 +64,10 @@ enum class client_packet_type: enet_uint8 { /// area_id, chunk_pos[] reliable MISSING_CHUNKS = 8, + /// Send public text message + /// char[] (not null terminated) reliable + MESSAGE = 9, + /// Position update /// voxel_pos notify MOVE = 16, diff --git a/src/core/server_handle.hpp b/src/core/server_handle.hpp index f036031..fedddad 100644 --- a/src/core/server_handle.hpp +++ b/src/core/server_handle.hpp @@ -11,4 +11,5 @@ struct server_handle { std::function emit; std::function raycast; std::optional teleport; + std::optional message; }; diff --git a/src/core/world/actions.hpp b/src/core/world/actions.hpp index 93d480e..ec24050 100644 --- a/src/core/world/actions.hpp +++ b/src/core/world/actions.hpp @@ -27,6 +27,11 @@ struct Move: part::Ping { const voxel_pos pos; }; +struct Message: part::Ping { + Message(const std::string& text): text(text) { } -using packet = std::variant; + const std::string text; +}; + +using packet = std::variant; } \ No newline at end of file diff --git a/src/server/world/SharedUniverse.cpp b/src/server/world/SharedUniverse.cpp index f4646d1..7cb7d45 100644 --- a/src/server/world/SharedUniverse.cpp +++ b/src/server/world/SharedUniverse.cpp @@ -17,6 +17,8 @@ SharedUniverse::SharedUniverse(const options &o, server_handle *const localHandl this->set(fill->pos, fill->val); } else if(const auto fillCube = std::get_if(&packet)) { this->setCube(fillCube->pos, fillCube->val, fillCube->radius); + } else if(const auto message = std::get_if(&packet)) { + this->broadcastMessage("Player" + std::to_string(id.index) + ": " + message->text); } else if(const auto move = std::get_if(&packet)) { if(!movePlayer(PLAYER_ENTITY_ID, move->pos)) { LOG_W("Bad move of player " << PLAYER_ENTITY_ID.index); @@ -44,6 +46,11 @@ void SharedUniverse::updateChunk(area_map::iterator &it, world::ChunkContainer:: } } +void SharedUniverse::broadcastMessage(const std::string& text) { + localHandle->message = text; + Universe::broadcastMessage(text); +} + std::shared_ptr SharedUniverse::createChunk(const chunk_pos &pos, const std::unique_ptr &rnd) const { return std::make_shared(pos, rnd); } diff --git a/src/server/world/SharedUniverse.hpp b/src/server/world/SharedUniverse.hpp index 26cd313..b8d7bd6 100644 --- a/src/server/world/SharedUniverse.hpp +++ b/src/server/world/SharedUniverse.hpp @@ -19,6 +19,8 @@ namespace world::server { void loadChunk(area_, chunk_pos, const world::ChunkContainer &) override; void updateChunk(area_map::iterator &, world::ChunkContainer::iterator &, chunk_pos, float deltaTime) override; + void broadcastMessage(const std::string&) override; + private: server_handle *const localHandle; }; diff --git a/src/server/world/Universe.cpp b/src/server/world/Universe.cpp index b54b8fb..9a0784f 100644 --- a/src/server/world/Universe.cpp +++ b/src/server/world/Universe.cpp @@ -389,12 +389,14 @@ void Universe::pullNetwork() { host.sendTo(peer, server_packet_type::TELEPORT, player->pos.as_voxel(), channel_type::RELIABLE); movedPlayers.insert(client->instanceId); } + broadcastMessage("> Player" + std::to_string(client->instanceId.index) + " has joined us"); broadcastAreas(); }, [&](peer_t *peer, disconnect_reason reason) { ZoneScopedN("Disconnect"); LOG_I("Client disconnect from " << peer->address << " with " << (enet_uint32)reason); if (const auto data = Server::GetPeerData(peer)) { + broadcastMessage("> Player" + std::to_string(data->instanceId.index) + " has left our universe"); entities.at(PLAYER_ENTITY_ID).instances.free(data->instanceId); delete data; } @@ -429,6 +431,12 @@ void Universe::pullNetwork() { } break; } + case client_packet_type::MESSAGE: { + const auto ref = PacketReader(packet).remaning(); + broadcastMessage("Player" + std::to_string(Server::GetPeerData(peer)->instanceId.index) + + ": " + std::string(static_cast(ref.data()), ref.size())); + break; + } case client_packet_type::MISSING_CHUNKS: { if (auto player = findEntity(PLAYER_ENTITY_ID,Server::GetPeerData(peer)->instanceId )) { const auto pos = player->pos.as_voxel(); @@ -457,7 +465,7 @@ void Universe::pullNetwork() { default: LOG_T("Bad packet from " << peer->address); break; - } + } }); } void Universe::broadcastAreas() { @@ -484,6 +492,9 @@ net::packet_t* Universe::serializeChunk(const robin_hood::pair, assert(packet.isFull()); return packet.get(); } +void Universe::broadcastMessage(const std::string& text) { + host.broadcast(net::server_packet_type::MESSAGE, text.data(), text.size(), net::channel_type::RELIABLE); +} void Universe::updateChunk(area_map::iterator &, world::ChunkContainer::iterator &, chunk_pos, float deltaTime) {} void Universe::loadChunk(area_, chunk_pos, const world::ChunkContainer &) {} diff --git a/src/server/world/Universe.hpp b/src/server/world/Universe.hpp index 1be2337..f6258ad 100644 --- a/src/server/world/Universe.hpp +++ b/src/server/world/Universe.hpp @@ -69,6 +69,7 @@ namespace world::server { void pullNetwork(); void broadcastAreas(); net::packet_t* serializeChunk(const robin_hood::pair, std::shared_ptr> &); + virtual void broadcastMessage(const std::string &); using area_map = robin_hood::unordered_map>;