#include "Cubed/gameplay/client_world.hpp" #include "Cubed/config.hpp" #include "Cubed/gameplay/game_time.hpp" #include "Cubed/gameplay/packet.hpp" using namespace std::chrono; using namespace std::chrono_literals; using namespace google::protobuf; namespace Cubed { namespace { struct ChunkRenderData { std::array*, 4> neighbor_block; ClientChunk* chunk; }; } // namespace ClientWorld::ClientWorld() : m_player(*this) {} ClientWorld::~ClientWorld() { stop_client_thread(); stop_thread_pool(); { std::lock_guard lock(m_chunks_mutex); m_chunks.clear(); } { std::lock_guard lk(m_delete_vbo_mutex); for (auto x : m_pending_delete_vbo) { glDeleteBuffers(1, &x); } m_pending_delete_vbo.clear(); } { std::lock_guard lk(m_delete_vao_mutex); for (auto x : m_pending_delete_vao) { glDeleteVertexArrays(1, &x); } m_pending_delete_vao.clear(); } m_timers.clear(); } const std::optional& ClientWorld::get_look_block_pos(const std::string& name) const { return m_player.get_look_block_pos(); } ClientPlayer& ClientWorld::get_player() { return m_player; } int ClientWorld::get_block(const glm::ivec3& block_pos) const { auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z); std::shared_lock lk(m_chunks_mutex); auto it = m_chunks.find(ChunkPos{chunk_x, chunk_z}); if (it == m_chunks.end()) { return 0; } const auto& chunk_blocks = it->second.get_chunk_blocks(); auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z}); if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y || z >= CHUNK_SIZE) { return 0; } return chunk_blocks[ClientChunk::index(x, y, z)]; } bool ClientWorld::is_solid(const glm::ivec3& block_pos) const { auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z); std::shared_lock lk(m_chunks_mutex); auto it = m_chunks.find(ChunkPos{chunk_x, chunk_z}); if (it == m_chunks.end()) { return false; } const auto& chunk_blocks = it->second.get_chunk_blocks(); auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z}); if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y || z >= CHUNK_SIZE) { return false; } auto id = chunk_blocks[ClientChunk::index(x, y, z)]; if (BlockManager::is_gas(id) || BlockManager::is_liquid(id)) { return false; } else { return true; } } bool ClientWorld::can_pass_block(const glm::ivec3& block_pos) const { auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z); std::shared_lock lk(m_chunks_mutex); auto it = m_chunks.find(ChunkPos{chunk_x, chunk_z}); if (it == m_chunks.end()) { return true; } const auto& chunk_blocks = it->second.get_chunk_blocks(); auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z}); if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y || z >= CHUNK_SIZE) { return true; } auto id = chunk_blocks[ClientChunk::index(x, y, z)]; return BlockManager::is_passable(id); } BlockType ClientWorld::get_block_tpye(const glm::ivec3& block_pos) const { auto [chunk_x, chunk_z] = get_chunk_pos(block_pos.x, block_pos.z); std::shared_lock lk(m_chunks_mutex); auto it = m_chunks.find(ChunkPos{chunk_x, chunk_z}); if (it == m_chunks.end()) { // Logger::error("Can't Find Block {} {} {}", block_pos.x, block_pos.y, // block_pos.z); return 0; } const auto& chunk_blocks = it->second.get_chunk_blocks(); auto [x, y, z] = ClientChunk::world_to_block(block_pos, {chunk_x, chunk_z}); if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y || z >= CHUNK_SIZE) { // Logger::error("Can't Find Block {} {} {}", block_pos.x, block_pos.y, // block_pos.z); return 0; } return chunk_blocks[ClientChunk::index(x, y, z)]; } void ClientWorld::set_block(const glm::ivec3& block_pos, unsigned id) { int world_x, world_y, world_z; world_x = block_pos.x; world_y = block_pos.y; world_z = block_pos.z; auto [chunk_x, chunk_z] = get_chunk_pos(world_x, world_z); std::lock_guard lk(m_chunks_mutex); auto it = m_chunks.find(ChunkPos{chunk_x, chunk_z}); if (it == m_chunks.end()) { return; } auto [x, y, z] = ClientChunk::world_to_block(world_x, world_y, world_z, chunk_x, chunk_z); if (x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y || z >= CHUNK_SIZE) { return; } it->second.set_chunk_block(ClientChunk::index(x, y, z), id); static const glm::ivec3 NEIGHBOR_DIRS[] = { {1, 0, 0}, {-1, 0, 0}, {0, 0, -1}, {0, 0, 1}}; for (const auto& dir : NEIGHBOR_DIRS) { glm::ivec3 neighbor = block_pos + dir; auto [cx, cz] = get_chunk_pos(neighbor.x, neighbor.z); auto it = m_chunks.find({cx, cz}); if (it != m_chunks.end()) { it->second.mark_dirty(); } } } void ClientWorld::push_delete_vbo(GLuint vbo) { std::lock_guard lk(m_delete_vbo_mutex); m_pending_delete_vbo.push_back(vbo); } void ClientWorld::push_delete_vao(GLuint vao) { std::lock_guard lk(m_delete_vao_mutex); m_pending_delete_vao.push_back(vao); } void ClientWorld::report_block_change(const glm::ivec3& pos, unsigned id) const { Arena arena; auto* req = Arena::Create(&arena); req->set_uuid(m_player.get_uuid()); req->set_block(id); auto* p = req->mutable_pos(); p->set_x(pos.x); p->set_y(pos.y); p->set_z(pos.z); m_client->send(make_packet(*req)); } void ClientWorld::receive_block_change(const BlockChangeRsp& rsp) { glm::vec3 pos{rsp.pos().x(), rsp.pos().y(), rsp.pos().z()}; set_block(pos, rsp.block()); } void ClientWorld::receive_time(const UpdateTime& rsp) { m_game_ticks = rsp.game_tick(); m_day_tick = rsp.day_tick(); } void ClientWorld::receive_other_player(const PlayerInfoRsp& rsp) { { std::lock_guard lock(m_other_players_mutex); m_other_players[rsp.uuid()].name = rsp.name(); m_other_players[rsp.uuid()].pos = glm::vec3{rsp.pos().x(), rsp.pos().y(), rsp.pos().z()}; // Logger::info("Player {} pos Update", rsp.name()); } } void ClientWorld::receive_player_logout(const LogoutRsp& rsp) { { std::lock_guard lock(m_other_players_mutex); int sum = m_other_players.erase(rsp.uuid()); if (sum == 0) { Logger::warn("Player {} not find", rsp.uuid()); } else { Logger::info("Player {} erase", rsp.uuid()); } } } void ClientWorld::init(std::string_view player_name, std::shared_ptr client) { m_chunks.reserve(MAX_DISTANCE * MAX_DISTANCE * 4); m_player.init(player_name); m_client = client; // timer register_timer("player_pos", 1, [this]() { report_player_pos(); }); LoginReq req; req.set_name(m_player.get_name()); while (!client->is_connected()) { if (client->is_connect_error()) { throw std::runtime_error("Can't connect to the server"); } std::this_thread::sleep_for(milliseconds(200)); } start_thread_pool(); // request login Logger::info("Send Login Request"); m_client->send(make_packet(req)); } void ClientWorld::start_client_thread(std::string_view uuid) { if (m_game_running) { Logger::error("Game Already Running"); return; } // response m_player.set_uuid(uuid); m_client_thread = std::jthread([this](std::stop_token token) { m_game_running = true; client_run(token); }); request_chunk(); } void ClientWorld::stop_client_thread() { m_client_thread.request_stop(); if (m_client_thread.joinable()) { m_client_thread.join(); } m_game_running = false; } void ClientWorld::start_thread_pool() { int max_threads = std::thread::hardware_concurrency(); int threads = std::min(max_threads, 4); change_pool_threads(threads); } void ClientWorld::stop_thread_pool() { auto pool_ptr = m_thread_pool.load(); if (pool_ptr) { pool_ptr->stop(); } m_thread_pool.store(nullptr); Logger::info("Thread Pool Stopped"); } void ClientWorld::change_pool_threads(int threads) { int m_max_threads = std::thread::hardware_concurrency(); if (m_max_threads < 1) { Logger::warn("Can't Get Max Support Threads, Set Max Threads to 4"); m_max_threads = 1; } int used_thread = std::clamp(threads, 1, m_max_threads); Logger::info("Create New Thread Pool Use {} Threads", used_thread); m_thread_pool.store(std::make_shared(used_thread)); } void ClientWorld::hot_reload() { auto& config = Config::get(); int dist = config.get("world.rendering_distance"); m_rendering_distance = dist <= MAX_DISTANCE ? dist : MAX_DISTANCE; } void ClientWorld::client_run(std::stop_token stoken) { Logger::info("Client Thread Started"); while (!stoken.stop_requested()) { auto t1 = system_clock::now(); for (auto& x : m_timers) { x.second.update(); } auto t2 = system_clock::now(); auto dt = duration_cast(t2 - t1); auto st = std::max(dt, milliseconds(DEFAULT_PER_TICK_TIME) - dt); std::this_thread::sleep_for(st); } } void ClientWorld::report_player_pos() { if (!m_client) { return; } Arena arena; auto* pos = Arena::Create(&arena); pos->set_uuid(m_player.get_uuid()); glm::vec3 player_pos = m_player.get_player_pos(); auto* v3 = pos->mutable_pos(); v3->set_x(player_pos.x); v3->set_y(player_pos.y); v3->set_z(player_pos.z); m_client->send(make_packet(*pos)); } void ClientWorld::request_chunk() { if (m_requesting_chunk.exchange(true)) { Logger::warn("It is requesting new chunk!"); return; } ChunkPosSet required_chunks; glm::vec3 player_pos = m_player.get_player_pos(); int x = std::floor(player_pos.x); int z = std::floor(player_pos.z); auto [chunk_x, chunk_z] = get_chunk_pos(x, z); int radius = m_rendering_distance; int r2 = radius * radius; required_chunks.reserve(radius * radius); for (int dx = -radius; dx <= radius; ++dx) { for (int dz = -radius; dz <= radius; ++dz) { if (dx * dx + dz * dz <= r2) { required_chunks.emplace(chunk_x + dx, chunk_z + dz); } } } ChunkPosVector need_send_pos; { std::lock_guard lk(m_chunks_mutex); for (auto it = m_chunks.begin(); it != m_chunks.end();) { if (required_chunks.find(it->first) == required_chunks.end()) { it = m_chunks.unsafe_erase(it); } else { ++it; } } for (auto pos : required_chunks) { auto it = m_chunks.find(pos); if (it == m_chunks.end()) { need_send_pos.emplace_back(pos); } } } if (need_send_pos.empty()) { return; } using enum ChunkLoadStyle; switch (m_chunk_load_style) { case RANDOM: break; case CENTER: { glm::vec3 player_pos = m_player.get_player_pos(); auto dist2 = [player_pos](ChunkPos chunk_pos) { ChunkPos player_chunk_pos = get_chunk_pos(player_pos.x, player_pos.z); float dx = player_chunk_pos.x - chunk_pos.x; float dz = player_chunk_pos.z - chunk_pos.z; return dx * dx + dz * dz; }; std::sort(need_send_pos.begin(), need_send_pos.end(), [&dist2](const auto& a, const auto& b) { return dist2(a) < dist2(b); }); } } auto uuid = m_player.get_uuid(); Arena arena; auto* req = Arena::Create(&arena); for (const auto& pos : need_send_pos) { req->set_uuid(uuid); auto* p = req->mutable_pos(); p->set_x(pos.x); p->set_z(pos.z); m_client->send(make_packet(*req)); } m_requesting_chunk = false; } void ClientWorld::receive_chunk(ChunkDataRsp data) { { std::lock_guard lock(m_chunks_mutex); ChunkPos pos{data.pos().x(), data.pos().z()}; if (m_chunks.find(pos) != m_chunks.end()) { Logger::warn("Chunk {} {} has already in client world", pos.x, pos.z); return; } } // vertex data will genrator in client thread pool instead of net thread; auto pool = m_thread_pool.load(); if (!pool) { Logger::error("Client Thread Pool is nullptr"); return; } pool->enqueue([this, data = std::move(data)]() { ClientChunk chunk{*this}; chunk.receive_chunk(data); { std::lock_guard lock(m_pending_upload_queue_mutex); m_pending_upload_queue.emplace_back(std::move(chunk)); } }); } void ClientWorld::exit() { Arena arena; auto* req = Arena::Create(&arena); req->set_uuid(m_player.get_uuid()); m_client->send(make_packet(*req)); } void ClientWorld::update(float delta_time) { m_player.update(delta_time); { std::lock_guard lk(m_delete_vbo_mutex); for (auto x : m_pending_delete_vbo) { glDeleteBuffers(1, &x); } m_pending_delete_vbo.clear(); } { std::lock_guard lk(m_delete_vao_mutex); for (auto x : m_pending_delete_vao) { glDeleteVertexArrays(1, &x); } m_pending_delete_vao.clear(); } std::vector new_chunks; { std::lock_guard lock(m_pending_upload_queue_mutex); for (auto& c : m_pending_upload_queue) { // Logger::info("{} {}", c.get_chunk_pos().x, c.get_chunk_pos().z); new_chunks.emplace_back(std::move(c)); } m_pending_upload_queue.clear(); } for (auto& c : new_chunks) { c.upload_to_gpu(); } { std::lock_guard lock(m_chunks_mutex); for (auto& c : new_chunks) { m_chunks.emplace(c.get_chunk_pos(), std::move(c)); } m_render_snapshots.clear(); for (auto& [pos, chunk] : m_chunks) { if (chunk.is_dirty()) { // the curial fator influence OptionalBlockVectorArray neighbor_block; for (int i = 0; i < 4; i++) { auto it = m_chunks.find(pos + CHUNK_DIR[i]); if (it != m_chunks.end()) { neighbor_block[i] = (it->second.get_chunk_blocks()); } else { neighbor_block[i] = std::nullopt; } } chunk.gen_vertex_data(neighbor_block); chunk.upload_to_gpu(); } if (!chunk.is_dirty()) { if (chunk.is_need_upload()) { chunk.upload_to_gpu(); } m_render_snapshots.push_back( {chunk.get_normal_vao(), chunk.get_normal_vertices_sum(), chunk.get_cross_vao(), chunk.get_cross_vertices_sum(), chunk.get_normal_discard_vao(), chunk.get_normal_discard_vertices_sum(), chunk.get_normal_blend_vao(), chunk.get_normal_blend_vertices_sum(), chunk.get_water_vao(), chunk.get_water_vertices_sum(), glm::vec3(static_cast(pos.x * CHUNK_SIZE) + static_cast(CHUNK_SIZE / 2), static_cast(WORLD_SIZE_Y / 2), static_cast(pos.z * CHUNK_SIZE) + static_cast(CHUNK_SIZE / 2)), glm::vec3(static_cast(CHUNK_SIZE / 2), static_cast(WORLD_SIZE_Y / 2), static_cast(CHUNK_SIZE / 2))}); } } } m_render_player_data.clear(); { std::lock_guard lock(m_other_players_mutex); for (auto& [uuid, player] : m_other_players) { m_render_player_data.emplace_back(player.name, player.pos); } } } glm::vec3 ClientWorld::sunlight_dir() const { float altitude = sin((m_day_tick - 6 * PER_HOUR) / static_cast(DAY_TIME / 2) * std::numbers::pi) * 90.0f; float t = static_cast(m_day_tick) / DAY_TIME; float azimuth = 90.0f - 360.0f * (t - 0.25f); float alt = glm::radians(altitude); float az = glm::radians(azimuth); glm::vec3 dir; dir.x = cos(alt) * sin(az); dir.y = sin(alt); dir.z = cos(alt) * cos(az); return glm::normalize(-dir); } int ClientWorld::rendering_distance() const { return m_rendering_distance.load(); } void ClientWorld::rendering_distance(int rendering_distance) { m_rendering_distance = rendering_distance; } const std::vector& ClientWorld::render_snapshots() const { return m_render_snapshots; }; const std::vector& ClientWorld::render_player_data() const { return m_render_player_data; } std::vector& ClientWorld::planes() { return m_planes; } } // namespace Cubed