feat(chat): implement client-server chat messaging

Add protobuf definition for ChatMsg, new packet ID CHAT_MSG (4001), and
handle sending/receiving chat messages on both client and server side.
Wire up UI components (ChatBox, TextField) to allow message input and
display, including a message queue in ClientWorld for thread-safe
processing. Also add default initialization for ChatMessage::time.
This commit is contained in:
2026-07-18 20:29:14 +08:00
parent f4103e254e
commit 75f71a537f
19 changed files with 126 additions and 17 deletions

View File

@@ -6,6 +6,6 @@ namespace Cubed {
struct ChatMessage { struct ChatMessage {
std::string player; std::string player;
std::string text; std::string text;
uint64_t time; uint64_t time = 0;
}; };
} // namespace Cubed } // namespace Cubed

View File

@@ -2,6 +2,7 @@
#include "Cubed/audio/audio_engine.hpp" #include "Cubed/audio/audio_engine.hpp"
#include "Cubed/config.hpp" #include "Cubed/config.hpp"
#include "Cubed/gameplay/block.hpp" #include "Cubed/gameplay/block.hpp"
#include "Cubed/gameplay/chat_message.hpp"
#include "Cubed/gameplay/chunk_pos.hpp" #include "Cubed/gameplay/chunk_pos.hpp"
#include "Cubed/gameplay/client_chunk.hpp" #include "Cubed/gameplay/client_chunk.hpp"
#include "Cubed/gameplay/client_player.hpp" #include "Cubed/gameplay/client_player.hpp"
@@ -102,6 +103,10 @@ public:
Config& get_config(); Config& get_config();
WorldScene& world_scene(); WorldScene& world_scene();
void set_direct_exit(); void set_direct_exit();
void receive_chat_message(ChatMsg& msg);
void send_chat_message(ChatMessage& message);
template <typename Fn> template <typename Fn>
void register_ticktimer(std::string_view id, TickType threshold, Fn&& f) { void register_ticktimer(std::string_view id, TickType threshold, Fn&& f) {
m_ticktimers.emplace( m_ticktimers.emplace(
@@ -147,6 +152,7 @@ private:
tbb::concurrent_queue<std::unique_ptr<ClientChunk>> m_pending_upload_queue; tbb::concurrent_queue<std::unique_ptr<ClientChunk>> m_pending_upload_queue;
tbb::concurrent_queue<ChunkPos> m_dirty_chunk_queue; tbb::concurrent_queue<ChunkPos> m_dirty_chunk_queue;
tbb::concurrent_queue<PendingSound> m_pending_sound; tbb::concurrent_queue<PendingSound> m_pending_sound;
tbb::concurrent_queue<ChatMessage> m_message_queue;
std::deque<ChunkPos> m_dirty_queue; std::deque<ChunkPos> m_dirty_queue;
std::vector<const ChunkRenderSnapshot*> m_render_snapshots; std::vector<const ChunkRenderSnapshot*> m_render_snapshots;

View File

@@ -61,6 +61,7 @@ enum class PacketEnum : uint16_t {
BLOCK_CHANGE_RSP = 3004, BLOCK_CHANGE_RSP = 3004,
S2C_CLEAR_ALL_CHUNKS = 3005, S2C_CLEAR_ALL_CHUNKS = 3005,
UPDATE_TIME = 3006, UPDATE_TIME = 3006,
CHAT_MSG = 4001,
PING = 9001, PING = 9001,
PONG = 9002 PONG = 9002
@@ -121,6 +122,9 @@ template <> constexpr uint16_t get_packet_id<Pong>() {
template <> constexpr uint16_t get_packet_id<PlayerWaterSound>() { template <> constexpr uint16_t get_packet_id<PlayerWaterSound>() {
return std::to_underlying(PacketEnum::PLAYER_WATER_SOUND); return std::to_underlying(PacketEnum::PLAYER_WATER_SOUND);
} }
template <> constexpr uint16_t get_packet_id<ChatMsg>() {
return std::to_underlying(PacketEnum::CHAT_MSG);
}
template <typename T> template <typename T>
requires std::derived_from<T, google::protobuf::Message> requires std::derived_from<T, google::protobuf::Message>

View File

@@ -80,6 +80,8 @@ public:
void handle_chunk_req(int task_id, const std::string& uuid, ChunkPos pos); void handle_chunk_req(int task_id, const std::string& uuid, ChunkPos pos);
void handle_block_change(const BlockChangeReq& req); void handle_block_change(const BlockChangeReq& req);
void handle_chat_message(ChatMsg& msg);
int chunk_size() const; int chunk_size() const;
template <typename Fn> template <typename Fn>
void register_timer(std::string_view id, TickType threshold, Fn&& f) { void register_timer(std::string_view id, TickType threshold, Fn&& f) {
@@ -189,5 +191,7 @@ private:
std::atomic<std::shared_ptr<PriorityThreadPool>>& thread_pool, std::atomic<std::shared_ptr<PriorityThreadPool>>& thread_pool,
int threads); int threads);
void send_server_stop(); void send_server_stop();
void boardcast_message(const std::string& name, const std::string& message);
}; };
} // namespace Cubed } // namespace Cubed

View File

@@ -35,6 +35,8 @@ public:
void set_error(std::string_view error); void set_error(std::string_view error);
void set_mouse(bool pause); void set_mouse(bool pause);
void set_chatting(bool chatting); void set_chatting(bool chatting);
// Not thread safe
void handle_chat_message(ChatMessage& message);
private: private:
bool handle_mouse_move_event(const MouseMoveEvent& e) override; bool handle_mouse_move_event(const MouseMoveEvent& e) override;

View File

@@ -20,6 +20,8 @@ public:
ChatBox& set_width(float width); ChatBox& set_width(float width);
ChatBox& set_show_lines(int lines); ChatBox& set_show_lines(int lines);
ChatBox& set_spacing(float spacing); ChatBox& set_spacing(float spacing);
ChatBox& clear_input();
std::string& get_input_text();
float width() const override; float width() const override;
float height() const override; float height() const override;
void set_d_image(TextureManager& m); void set_d_image(TextureManager& m);

View File

@@ -28,13 +28,14 @@ public:
TextField& set_auto_scale(bool auto_scale); TextField& set_auto_scale(bool auto_scale);
TextField& set_app(App* app); TextField& set_app(App* app);
TextField& set_typing(bool typing); TextField& set_typing(bool typing);
TextField& clear_input();
bool handle_mouse_move_event(const MouseMoveEvent& e) override; bool handle_mouse_move_event(const MouseMoveEvent& e) override;
bool handle_mouse_button_event(const MouseButtonEvent& e) override; bool handle_mouse_button_event(const MouseButtonEvent& e) override;
bool handle_text_input_event(const TextInputEvent& e) override; bool handle_text_input_event(const TextInputEvent& e) override;
bool handle_key_event(const KeyEvent& e) override; bool handle_key_event(const KeyEvent& e) override;
bool handle_window_resize_event(const WindowResizeEvent& e) override; bool handle_window_resize_event(const WindowResizeEvent& e) override;
const std::string& input_text() const; const std::string& input_text() const;
std::string& input_text();
template <typename F> TextField& set_on_finish(F&& f) { template <typename F> TextField& set_on_finish(F&& f) {
m_on_finished = std::forward<F>(f); m_on_finished = std::forward<F>(f);
return *this; return *this;

View File

@@ -21,6 +21,8 @@ public:
void set_chatting(bool chantting); void set_chatting(bool chantting);
void add_chat_message(ChatMessage& message);
private: private:
bool handle_key_event(const KeyEvent& e) override; bool handle_key_event(const KeyEvent& e) override;
WorldScene& m_scene; WorldScene& m_scene;

View File

@@ -6,6 +6,7 @@
#include "Cubed/gameplay/packet.hpp" #include "Cubed/gameplay/packet.hpp"
#include "Cubed/scene/world_scene.hpp" #include "Cubed/scene/world_scene.hpp"
#include "Cubed/tools/math_tools.hpp" #include "Cubed/tools/math_tools.hpp"
#include "Cubed/tools/time_tools.hpp"
#include <absl/container/inlined_vector.h> #include <absl/container/inlined_vector.h>
#include <numbers> #include <numbers>
@@ -750,6 +751,17 @@ void ClientWorld::request_exit() {
} }
} }
void ClientWorld::receive_chat_message(ChatMsg& msg) {
m_message_queue.emplace(msg.name(), msg.msg(), Tools::get_time_ticks());
}
void ClientWorld::send_chat_message(ChatMessage& message) {
Arena arena;
auto msg = Arena::Create<ChatMsg>(&arena);
msg->set_name(message.player);
msg->set_msg(message.text);
m_client->send(make_packet(*msg));
}
void ClientWorld::update(float delta_time) { void ClientWorld::update(float delta_time) {
m_player.update(delta_time); m_player.update(delta_time);
@@ -916,6 +928,10 @@ void ClientWorld::update(float delta_time) {
for (auto& [pos, timer] : m_timers) { for (auto& [pos, timer] : m_timers) {
timer.update(delta_time); timer.update(delta_time);
} }
ChatMessage message;
while (m_message_queue.try_pop(message)) {
m_world_scene.handle_chat_message(message);
}
} }
bool ClientWorld::handle_event(const Event& e) { bool ClientWorld::handle_event(const Event& e) {

View File

@@ -135,6 +135,12 @@ asio::awaitable<void> NetworkClient::read_loop() {
m_world.receive_player_water_sound(*rsp); m_world.receive_player_water_sound(*rsp);
} }
} break; } break;
case std::to_underlying(PacketEnum::CHAT_MSG): {
auto* msg = Arena::Create<ChatMsg>(&arena);
if (decode_packet(*msg, body_data, header)) {
m_world.receive_chat_message(*msg);
}
} break;
} }
} }
} catch (const asio::system_error& e) { } catch (const asio::system_error& e) {

View File

@@ -753,6 +753,16 @@ void ServerWorld::handle_chunk_req(int task_id, const std::string& uuid,
[task_id, uuid, pos, this]() { send_chunk(task_id, uuid, pos); }); [task_id, uuid, pos, this]() { send_chunk(task_id, uuid, pos); });
} }
void ServerWorld::handle_chat_message(ChatMsg& msg) {
std::string name = msg.name();
std::string message = msg.msg();
auto pool = m_net_thread_pool.load();
pool->enqueue([this, player = std::move(name), m = std::move(message)]() {
boardcast_message(player, m);
});
}
void ServerWorld::handle_block_change(const BlockChangeReq& req) { void ServerWorld::handle_block_change(const BlockChangeReq& req) {
float x = std::floor(req.pos().x()); float x = std::floor(req.pos().x());
float y = std::floor(req.pos().y()); float y = std::floor(req.pos().y());
@@ -861,6 +871,28 @@ void ServerWorld::send_server_stop() {
Logger::info("Send Server Mesaage Success"); Logger::info("Send Server Mesaage Success");
} }
void ServerWorld::boardcast_message(const std::string& name,
const std::string& message) {
std::vector<std::shared_ptr<Session>> m_session;
{
std::shared_lock lock(m_player_mutex);
for (auto& [_, p] : m_players) {
m_session.emplace_back(p.get_session());
}
}
Arena arena;
auto msg = Arena::Create<ChatMsg>(&arena);
msg->set_msg(message);
msg->set_name(name);
for (auto& s : m_session) {
s->send(make_packet(*msg));
}
}
int ServerWorld::chunk_load_style() const { int ServerWorld::chunk_load_style() const {
return std::to_underlying(m_chunk_load_style.load()); return std::to_underlying(m_chunk_load_style.load());
} }

View File

@@ -99,6 +99,12 @@ asio::awaitable<void> Session::read_loop() {
m_server_world.sync_player_water_sound(*req); m_server_world.sync_player_water_sound(*req);
} }
} }
if (cmd_id == std::to_underlying(PacketEnum::CHAT_MSG)) {
auto* msg = Arena::Create<ChatMsg>(&arena);
if (decode_packet(*msg, body_data, header)) {
m_server_world.handle_chat_message(*msg);
}
}
} }
} catch (const asio::system_error& e) { } catch (const asio::system_error& e) {
auto ec = e.code(); auto ec = e.code();

View File

@@ -0,0 +1,6 @@
syntax = "proto3";
message ChatMsg {
string name = 1;
string msg = 2;
}

View File

@@ -9,4 +9,5 @@ import "system/ping.proto";
import "system/pong.proto"; import "system/pong.proto";
import "world/chunk_data.proto"; import "world/chunk_data.proto";
import "world/block_change.proto"; import "world/block_change.proto";
import "world/time.proto"; import "world/time.proto";
import "chat/chat.proto";

View File

@@ -334,6 +334,11 @@ void WorldScene::set_chatting(bool chatting) {
Logger::info("World Scene Chatting {}", chatting); Logger::info("World Scene Chatting {}", chatting);
} }
// Not thread safe
void WorldScene::handle_chat_message(ChatMessage& message) {
m_hud_ui.add_chat_message(message);
}
void WorldScene::set_error(std::string_view error) { void WorldScene::set_error(std::string_view error) {
Logger::error("WorldScene Error Set {}", error); Logger::error("WorldScene Error Set {}", error);
m_error_ui.set_error(error); m_error_ui.set_error(error);

View File

@@ -52,6 +52,12 @@ ChatBox& ChatBox::set_spacing(float spacing) {
m_spacing = spacing; m_spacing = spacing;
return *this; return *this;
} }
ChatBox& ChatBox::clear_input() {
m_text_field->clear_input();
return *this;
}
std::string& ChatBox::get_input_text() { return m_text_field->input_text(); }
float ChatBox::width() const { return m_width * m_scale; } float ChatBox::width() const { return m_width * m_scale; }
float ChatBox::height() const { float ChatBox::height() const {
// Height is dynamically calculated, no scaling needed // Height is dynamically calculated, no scaling needed

View File

@@ -79,18 +79,15 @@ void TextField::on_update(float dt) {
} }
void TextField::update_show_text() { void TextField::update_show_text() {
if (m_input_text.empty()) {
if (!m_typing) {
m_foreground->set_text(m_show_text);
m_foreground->set_color(Color::GRAY);
} else {
m_foreground->set_text(" ");
}
if (!m_typing) {
m_foreground->set_text(m_show_text);
m_foreground->set_color(Color::GRAY);
} else { } else {
m_foreground->set_text(m_input_text); m_foreground->set_text(m_input_text);
m_foreground->set_color(Color::WHITE); m_foreground->set_color(Color::WHITE);
} }
update_text_scale(); update_text_scale();
update_input_area(); update_input_area();
m_cursor->set_offset({13.0f + m_foreground->width(), 0.0f}); m_cursor->set_offset({13.0f + m_foreground->width(), 0.0f});
@@ -162,6 +159,12 @@ TextField& TextField::set_typing(bool typing) {
return *this; return *this;
} }
TextField& TextField::clear_input() {
m_input_text.clear();
update_show_text();
return *this;
}
void TextField::start_typing() { void TextField::start_typing() {
if (m_app) { if (m_app) {
m_app->start_text_input(); m_app->start_text_input();
@@ -178,7 +181,7 @@ void TextField::stop_typing() {
} }
const std::string& TextField::input_text() const { return m_input_text; } const std::string& TextField::input_text() const { return m_input_text; }
std::string& TextField::input_text() { return m_input_text; }
bool TextField::handle_mouse_move_event(const MouseMoveEvent& e) { bool TextField::handle_mouse_move_event(const MouseMoveEvent& e) {
auto p = pos(); auto p = pos();
if (e.xpos >= p.x && e.xpos <= p.x + width() && e.ypos >= p.y && if (e.xpos >= p.x && e.xpos <= p.x + width() && e.ypos >= p.y &&

View File

@@ -1,8 +1,5 @@
#include "Cubed/ui/ui_vertex_data.hpp" #include "Cubed/ui/ui_vertex_data.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp"
namespace Cubed { namespace Cubed {
UIVertexData::UIVertexData() {} UIVertexData::UIVertexData() {}
UIVertexData::~UIVertexData() { UIVertexData::~UIVertexData() {
@@ -12,8 +9,7 @@ UIVertexData::~UIVertexData() {
void UIVertexData::upload() { void UIVertexData::upload() {
if (m_sum == 0) { if (m_sum == 0) {
Logger::error("You need update_sum first"); return;
ASSERT(false);
} }
if (m_vertices.size() == 0) { if (m_vertices.size() == 0) {
return; return;

View File

@@ -2,6 +2,7 @@
#include "Cubed/app.hpp" #include "Cubed/app.hpp"
#include "Cubed/debug_collector.hpp" #include "Cubed/debug_collector.hpp"
#include "Cubed/gameplay/client_world.hpp"
#include "Cubed/render/renderer.hpp" #include "Cubed/render/renderer.hpp"
#include "Cubed/scene/scene_manager.hpp" #include "Cubed/scene/scene_manager.hpp"
#include "Cubed/scene/world_scene.hpp" #include "Cubed/scene/world_scene.hpp"
@@ -38,7 +39,12 @@ void WorldUIManager::init() {
chat_box.set_text_scale(0.6f); chat_box.set_text_scale(0.6f);
chat_box.set_d_image(m_scene.scene_manager().app().texture_manager()); chat_box.set_d_image(m_scene.scene_manager().app().texture_manager());
chat_box.set_app(&m_scene.scene_manager().app()); chat_box.set_app(&m_scene.scene_manager().app());
chat_box.set_on_finish([]() {}); chat_box.set_on_finish([this, &chat_box]() {
ChatMessage message{m_scene.client_world().get_player().get_name(),
std::move(chat_box.get_input_text()), 0};
chat_box.clear_input();
m_scene.client_world().send_chat_message(message);
});
m_chat_box = &chat_box; m_chat_box = &chat_box;
} }
void WorldUIManager::render(Renderer& renderer) { void WorldUIManager::render(Renderer& renderer) {
@@ -56,6 +62,11 @@ void WorldUIManager::set_chatting(bool chantting) {
m_chat_box->set_typing(chantting); m_chat_box->set_typing(chantting);
} }
void WorldUIManager::add_chat_message(ChatMessage& message) {
m_chat_box->add_message(message);
}
bool WorldUIManager::handle_key_event(const KeyEvent& e) { bool WorldUIManager::handle_key_event(const KeyEvent& e) {
return UIManager::handle_key_event(e); return UIManager::handle_key_event(e);