4 Commits

Author SHA1 Message Date
6f39ff771d refactor(packet): serialize directly into packet buffer with ByteSizeLong 2026-06-23 22:00:19 +08:00
2953e8a233 feat(protocol): add packet serialization and login handling
Introduce packet header and ID mapping for protobuf messages.
Refactor session and server_world to use new packet wrapper.
Fix missing semicolons in proto files.
2026-06-23 21:58:35 +08:00
678fc02998 refactor(proto): restructure protobuf definitions and update build system 2026-06-23 21:10:40 +08:00
4303c1cd32 feat(network): integrate protobuf for player sync and session management
Add Protobuf dependency, define proto messages for player requests, positions, and chunk data. Refactor Session to use strand and async write. Implement player join/exit and position sync in ServerWorld.
2026-06-23 20:08:40 +08:00
20 changed files with 333 additions and 29 deletions

View File

@@ -19,6 +19,8 @@ if(MSVC)
endif()
find_package(OpenGL REQUIRED)
find_package(Protobuf REQUIRED)
if (UNIX AND NOT APPLE)
find_package(Freetype REQUIRED)
@@ -28,8 +30,7 @@ if (UNIX AND NOT APPLE)
find_package(glfw3 REQUIRED)
endif()
add_library(glad STATIC third_party/glad/src/glad.c
src/gameplay/network_client.cpp)
add_library(glad STATIC third_party/glad/src/glad.c)
target_include_directories(glad PUBLIC third_party/glad/include)
include(FetchContent)
@@ -99,7 +100,7 @@ FetchContent_MakeAvailable(tomlplusplus)
add_subdirectory(third_party/imgui)
set(INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)
add_executable(${PROJECT_NAME}
src/main.cpp
@@ -148,8 +149,17 @@ add_executable(${PROJECT_NAME}
src/gameplay/server_player.cpp
src/gameplay/client_player.cpp
src/gameplay/session.cpp
src/gameplay/network_client.cpp
)
file(GLOB_RECURSE PROTO_FILES
${CMAKE_CURRENT_SOURCE_DIR}/src/proto/*.proto
)
protobuf_generate(
TARGET ${PROJECT_NAME}
LANGUAGE cpp
PROTOS ${PROTO_FILES}
IMPORT_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/src/proto
)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Building with AddressSanitizer enabled for target: ${PROJECT_NAME}")
@@ -175,10 +185,13 @@ else()
)
endif()
target_include_directories(${PROJECT_NAME} PRIVATE ${INCLUDE_DIR})
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/include)
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/third_party/asio/include
)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/src)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_compile_definitions(${PROJECT_NAME} PRIVATE
ASIO_STANDALONE
ASIO_NO_DEPRECATED
@@ -194,6 +207,7 @@ target_link_libraries(${PROJECT_NAME}
tomlplusplus::tomlplusplus
imgui
tbb
protobuf::libprotobuf
)
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")

View File

@@ -1,4 +1,7 @@
#pragma once
#include "Cubed/gameplay/server_world.hpp"
#include "Cubed/gameplay/session.hpp"
#include <asio.hpp>
#include <thread>
namespace Cubed {
@@ -10,11 +13,14 @@ public:
void stop();
void run();
int port() const;
std::unordered_map<std::string, std::shared_ptr<Session>> m_session;
private:
asio::io_context m_io;
std::thread m_server;
int m_port = 25530;
std::atomic<bool> m_stopped{false};
ServerWorld m_world;
asio::awaitable<void> listen();
};
} // namespace Cubed

View File

@@ -0,0 +1,57 @@
#pragma once
#include "packet.pb.h"
#include <netinet/in.h>
#include <type_traits>
namespace Cubed {
constexpr int HEADER_LEN = 8;
using Packet = std::shared_ptr<std::vector<uint8_t>>;
template <typename> struct always_false : std::false_type {}; // NOLINT
template <typename T> constexpr uint16_t get_packet_id() {
using std::is_same_v;
using U = std::decay_t<T>;
if constexpr (is_same_v<U, LoginReq>) {
return 1001;
} else if constexpr (is_same_v<U, LoginRsp>) {
return 1002;
} else if constexpr (is_same_v<U, PlayerInfo>) {
return 2001;
} else if constexpr (is_same_v<U, PlayerPos>) {
return 2002;
} else if constexpr (is_same_v<U, ChunkData>) {
return 3001;
} else if constexpr (is_same_v<U, Ping>) {
return 9001;
} else if (is_same_v<U, Pong>) {
return 9002;
} else {
static_assert(always_false<U>::value, "Unkonw Type");
}
}
template <typename T> Packet make_packet(const T& msg) {
uint16_t cmd = get_packet_id<T>();
uint32_t body_len = static_cast<uint32_t>(msg.ByteSizeLong());
uint32_t total_len = HEADER_LEN + body_len;
auto packet = std::make_shared<std::vector<uint8_t>>(total_len);
uint32_t total_len_net = htonl(total_len);
uint16_t cmd_net = htons(cmd);
std::memcpy(packet->data(), &total_len_net, sizeof(total_len_net));
std::memcpy(packet->data() + 4, &cmd_net, sizeof(cmd_net));
if (!msg.SerializeToArray(packet->data() + HEADER_LEN,
static_cast<int>(body_len))) {
return {};
}
return packet;
}
} // namespace Cubed

View File

@@ -1,4 +1,17 @@
#pragma once
#include <glm/glm.hpp>
#include <string>
#include <string_view>
namespace Cubed {
class ServerPlayer {};
class ServerPlayer {
public:
explicit ServerPlayer(std::string_view);
const glm::vec3& get_pos() const;
const std::string& get_name() const;
void update_pos(float x, float y, float z);
private:
std::string m_name;
glm::vec3 m_pos{0.0f};
};
} // namespace Cubed

View File

@@ -10,15 +10,19 @@
#include <future>
#include <shared_mutex>
#include <tbb/concurrent_hash_map.h>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
namespace Cubed {
class Session;
class ServerWorld {
public:
ServerWorld();
~ServerWorld();
void player_join();
void player_join(const std::string& name);
void player_exit(const std::string& name);
void init_world();
void need_gen();
void update();
@@ -60,6 +64,11 @@ public:
void set_block(const glm::ivec3& block_pos, unsigned id);
void sync_player_pos(const std::string& name, float x, float y, float z);
void handle_player_login(const std::string& player_name,
std::shared_ptr<Session> session);
glm::vec3 get_player_pos(const std::string& name) const;
private:
enum class ChunkLoadStyle { RANDOM, CENTER };
struct PendingChunk {
@@ -68,7 +77,7 @@ private:
};
using ChunkHashMap =
std::unordered_map<ChunkPos, ServerChunk, ChunkPos::Hash>;
using PlayerHashMap = std::unordered_map<std::size_t, ServerPlayer>;
using PlayerHashMap = std::unordered_map<std::string, ServerPlayer>;
using PendingChunkHashMap =
std::unordered_map<ChunkPos, PendingChunk, ChunkPos::Hash>;
using ChunkPosSet = std::unordered_set<ChunkPos, ChunkPos::Hash>;
@@ -110,6 +119,11 @@ private:
std::atomic<ChunkLoadStyle> m_chunk_load_style{ChunkLoadStyle::RANDOM};
std::optional<std::string> m_request_gen_name = std::nullopt;
tbb::concurrent_hash_map<std::string, std::shared_ptr<Session>>
m_player_session;
void init_chunks();
void gen_chunks_internal();
@@ -120,7 +134,5 @@ private:
void submit_new_chunks();
void poll_finished_chunks();
void wait_all_chunk_tasks();
void sync_player_pos(glm::vec3& pos);
};
} // namespace Cubed

View File

@@ -1,33 +1,35 @@
#pragma once
#include "Cubed/gameplay/packet.hpp"
#include <asio.hpp>
#include <deque>
#include <memory>
#include <string>
namespace Cubed {
using asio::ip::tcp;
using asio::ip::tcp;
class ServerWorld;
class Session : public std::enable_shared_from_this<Session> {
public:
Session(tcp::socket socket);
Session(tcp::socket socket, ServerWorld& server_world);
~Session();
void start();
void send();
void send(Packet packet);
void close();
const std::string& uuid() const;
private:
static constexpr int HEADER_LEN = 8;
static constexpr uint32_t MAX_PACKET_SIZE = 4 * 1024 * 1024;
tcp::socket m_socket;
std::vector<char> m_read_buffer;
std::deque<std::vector<char>> m_write_queue;
std::mutex m_write_mutex;
std::deque<Packet> m_write_queue;
asio::strand<asio::io_context::executor_type> m_strand;
std::string m_uuid;
asio::awaitable<void> read();
asio::awaitable<void> write();
ServerWorld& m_server_world;
asio::awaitable<void> read_loop();
std::atomic<bool> m_closed{false};
void do_write();
};
} // namespace Cubed

View File

@@ -9,7 +9,17 @@ NetworkServer::NetworkServer(int port) : m_port(port) {}
NetworkServer::~NetworkServer() { stop(); }
void NetworkServer::stop() {
if (m_stopped.exchange(true)) {
return;
}
for (auto& [key, s] : m_session) {
if (s) {
s->close();
}
}
m_io.stop();
m_session.clear();
if (m_server.joinable()) {
m_server.join();
}
@@ -19,10 +29,17 @@ void NetworkServer::stop() {
asio::awaitable<void> NetworkServer::listen() {
tcp::acceptor acceptor(m_io, tcp::endpoint(tcp::v4(), m_port));
while (true) {
tcp::socket socket =
co_await acceptor.async_accept(asio::use_awaitable);
if (m_stopped) {
break;
}
std::shared_ptr<Session> s =
std::make_shared<Session>(std::move(socket), m_world);
s->start();
m_session.emplace(s->uuid(), s);
}
co_return;
}
void NetworkServer::run() {

View File

@@ -0,0 +1,10 @@
#include "Cubed/gameplay/server_player.hpp"
namespace Cubed {
ServerPlayer::ServerPlayer(std::string_view name) : m_name(name) {}
const glm::vec3& ServerPlayer::get_pos() const { return m_pos; }
const std::string& ServerPlayer::get_name() const { return m_name; }
void ServerPlayer::update_pos(float x, float y, float z) {
m_pos = glm::vec3{x, y, z};
}
} // namespace Cubed

View File

@@ -1,6 +1,8 @@
#include "Cubed/gameplay/server_world.hpp"
#include "Cubed/config.hpp"
#include "Cubed/gameplay/packet.hpp"
#include "Cubed/gameplay/session.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp"
@@ -83,12 +85,16 @@ void ServerWorld::gen_chunks_internal() {
submit_new_chunks();
m_chunk_gen_finished = true;
m_request_gen_name = std::nullopt;
}
void ServerWorld::compute_required_chunks(ChunkPosSet& required_chunks) {
glm::vec3 player_pos;
// sync_player_pos(player_pos);
ASSERT_MSG(false, "Player Pos");
if (m_request_gen_name == std::nullopt) {
player_pos = glm::vec3{0.0f};
} else {
player_pos = get_player_pos(m_request_gen_name.value());
}
int x = std::floor(player_pos.x);
int z = std::floor(player_pos.z);
auto [chunk_x, chunk_z] = get_chunk_pos(x, z);
@@ -147,7 +153,11 @@ void ServerWorld::submit_new_chunks() {
}
}
glm::vec3 player_pos;
sync_player_pos(player_pos);
if (m_request_gen_name == std::nullopt) {
player_pos = glm::vec3{0.0f};
} else {
player_pos = get_player_pos(m_request_gen_name.value());
}
auto dist2 = [player_pos](ChunkPos chunk_pos) {
ChunkPos player_chunk_pos =
get_chunk_pos(player_pos.x, player_pos.z);
@@ -339,6 +349,45 @@ void ServerWorld::rebuild_world() {
void ServerWorld::update() { poll_finished_chunks(); }
void ServerWorld::sync_player_pos(const std::string& name, float x, float y,
float z) {
auto it = m_players.find(name);
if (it == m_players.end()) {
Logger::warn("Player {} is not in this Server", it->first);
return;
}
it->second.update_pos(x, y, z);
}
void ServerWorld::handle_player_login(const std::string& name,
std::shared_ptr<Session> session) {
player_join(name);
m_player_session.emplace(name, session);
LoginRsp rsp;
rsp.set_success(true);
rsp.set_uuid(name);
session->send(make_packet(name));
}
void ServerWorld::player_join(const std::string& name) {
m_players.emplace(name, name);
}
void ServerWorld::player_exit(const std::string& name) {
auto it = m_players.find(name);
if (it == m_players.end()) {
Logger::error("Player {} isn't in Server", it->first);
}
m_players.erase(it);
}
glm::vec3 ServerWorld::get_player_pos(const std::string& name) const {
auto it = m_players.find(name);
if (it == m_players.end()) {
return glm::vec3{0.0f};
}
return it->second.get_pos();
}
int ServerWorld::rendering_distance() const {
return m_rendering_distance.load();
}

View File

@@ -1,26 +1,39 @@
#include "Cubed/gameplay/session.hpp"
#include "Cubed/gameplay/server_world.hpp"
#include "Cubed/tools/log.hpp"
#include "Cubed/tools/uuid.hpp"
using asio::ip::tcp;
namespace Cubed {
Session::Session(tcp::socket socket)
: m_socket(std::move(socket)), m_uuid(generate_uuid()) {}
Session::Session(tcp::socket socket, ServerWorld& server_world)
: m_socket(std::move(socket)), m_uuid(generate_uuid()),
m_server_world(server_world) {}
Session::~Session() {}
void Session::start() {
auto self = shared_from_this();
asio::co_spawn(
m_socket.get_executor(),
[self]() -> asio::awaitable<void> { co_await self->read(); },
m_strand,
[self]() -> asio::awaitable<void> { co_await self->read_loop(); },
asio::detached);
}
void Session::send(std::shared_ptr<std::vector<uint8_t>> packet) {
asio::post(m_strand, [self = shared_from_this(),
packet = std::move(packet)]() mutable {
bool idle = self->m_write_queue.empty();
self->m_write_queue.emplace_back(std::move(packet));
if (idle) {
self->do_write();
}
});
}
const std::string& Session::uuid() const { return m_uuid; }
asio::awaitable<void> Session::read() {
asio::awaitable<void> Session::read_loop() {
try {
while (true) {
std::array<char, HEADER_LEN> header;
@@ -47,13 +60,47 @@ asio::awaitable<void> Session::read() {
if (cmd_id == 1001) {
}
if (cmd_id == 1002) {
}
}
} catch (const asio::system_error& e) {
Logger::warn("Catch Asio Error {}", e.what());
close();
} catch (...) {
Logger::error("Unknow Error");
close();
}
co_return;
}
void Session::do_write() {
auto self = shared_from_this();
asio::async_write(
m_socket, asio::buffer(*(m_write_queue.front())),
asio::bind_executor(m_strand, [self](std::error_code ec, size_t) {
if (ec) {
Logger::warn("Write Ec {}", ec.message());
self->close();
return;
}
self->m_write_queue.pop_front();
if (!self->m_write_queue.empty()) {
self->do_write();
}
}));
}
void Session::close() {
if (m_closed.exchange(true)) {
return;
}
std::error_code ec;
m_socket.shutdown(tcp::socket::shutdown_both, ec);
m_socket.close(ec);
}
} // namespace Cubed

11
src/proto/auth/auth.proto Normal file
View File

@@ -0,0 +1,11 @@
syntax = "proto3";
message LoginReq {
string name = 1;
}
message LoginRsp {
bool success = 1;
string uuid = 2;
}

View File

@@ -0,0 +1,6 @@
syntax = "proto3";
message ChunkPos {
int32 x = 1;
int32 y = 2;
}

View File

@@ -0,0 +1,6 @@
syntax = "proto3";
message Error {
int32 code = 1;
string mes = 2;
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message PlayerInfo {
string name = 1;
}

View File

@@ -0,0 +1,7 @@
syntax = "proto3";
message Vec3 {
float x = 1;
float y = 2;
float z = 3;
}

11
src/proto/packet.proto Normal file
View File

@@ -0,0 +1,11 @@
syntax = "proto3";
import "common/chunk_pos.proto";
import "common/error.proto";
import "common/player_info.proto";
import "common/vector3.proto";
import "player/player.proto";
import "auth/auth.proto";
import "system/ping.proto";
import "system/pong.proto";
import "world/chunk_data.proto";

View File

@@ -0,0 +1,8 @@
syntax = "proto3";
import "common/vector3.proto";
message PlayerPos {
string uuid = 1;
Vec3 pos = 2;
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message Ping {
uint64 timestamp = 1;
}

View File

@@ -0,0 +1,5 @@
syntax = "proto3";
message Pong {
uint64 timestamp = 1;
}

View File

@@ -0,0 +1,13 @@
syntax = "proto3";
import "common/chunk_pos.proto";
message ChunkData {
ChunkPos pos = 1;
uint32 chunk_seed = 2;
repeated uint32 chunk_blocks = 3 [packed=true];
repeated uint32 neighbor_blocks_1 = 4 [packed=true];
repeated uint32 neighbor_blocks_2 = 5 [packed=true];
repeated uint32 neighbor_blocks_3 = 6 [packed=true];
repeated uint32 neighbor_blocks_4 = 7 [packed=true];
}