#include "ZoneManager.hpp" #include "world/CharacterBody.hpp" #include "world/CharacterController.hpp" #include "world/Transform.hpp" #include "world/WorldEntity.hpp" #include #include namespace tw::net { ZoneManager::ZoneManager(im::AreaBounds zone_bounds) : m_world(std::make_unique()), m_physics_world(m_world.get()), m_spatial_backend(std::make_unique(zone_bounds.min.x, zone_bounds.max.x, zone_bounds.min.y, zone_bounds.max.y)), m_interest_system(std::make_unique>( m_world.get(), m_spatial_backend.get() )) {} im::AreaBounds ZoneManager::area() const { return { { (float)m_spatial_backend->world_min_x(), (float)m_spatial_backend->world_min_z() }, { (float)m_spatial_backend->world_max_x(), (float)m_spatial_backend->world_max_z() } }; } void ZoneManager::transfer_entity(EntityInfo&& info) { spdlog::info("Transfered entity to zone ({} {}) ({} {})", area().min.x, area().min.y, area().max.x, area().max.y); spawn_entity(std::move(info)); } entt::entity ZoneManager::spawn_entity(EntityInfo&& info) { entt::entity entity = m_world->registry().create(); m_world->registry().emplace(entity, info.name, (uint32_t)entity); m_world->registry().emplace(entity, Transform(info.position)); m_world->registry().emplace(entity, 20.0f); m_world->registry().emplace( entity, m_physics_world.create_character( new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)), info.position ) ); return entity; } void ZoneManager::add_client(im::InterestId interest_id, entt::entity entity) { m_client_entities[interest_id] = entity; m_entity_sessions[entity] = interest_id; m_interest_system->set_entity_interest(interest_id, entity); } entt::entity ZoneManager::client_entity(im::InterestId interest_id) const { auto it = m_client_entities.find(interest_id); return it != m_client_entities.end() ? it->second : entt::null; } void ZoneManager::on_player_move(SessionId session_id, mmo::PlayerMoveMessage&& message) { ZoneScoped; entt::entity entity = client_entity(session_id); if (entity == entt::null) return; m_session_input_frame[session_id] = message.frame_idx(); auto* controller = m_world->registry().try_get(entity); if (controller) { controller->set_input( message.frame_idx(), glm::vec3(message.input().x(), message.input().y(), message.input().z()) ); } else { spdlog::error("Player {} has no character controller", (uint32_t)entity); } } void ZoneManager::register_neighbor_zone(ZoneProxy* neighbor) { im::InterestId id = m_next_neighbor_id++; m_neighbors.push_back({ neighbor, id }); // Expand bounds so entities approaching the border appear in the interest // system before they cross. Transfer still uses the true (unexpanded) bounds. auto b = neighbor->area(); im::AreaBounds expanded { { b.min.x - kNeighborBorderOverlap, b.min.y - kNeighborBorderOverlap }, { b.max.x + kNeighborBorderOverlap, b.max.y + kNeighborBorderOverlap } }; m_interest_system->set_area_interest(id, expanded); } const im::Interest* ZoneManager::get_interest(im::InterestId id) const { return m_interest_system->get_interest(id); } uint32_t ZoneManager::acked_input_frame(im::InterestId interest_id) const { auto it = m_session_acked_frame.find(interest_id); return it != m_session_acked_frame.end() ? it->second : 0; } void ZoneManager::check_neighbor_transfers() { ZoneScopedN("ZoneManager::check_neighbor_transfers"); for (const auto& [proxy, interest_id] : m_neighbors) { const im::Interest* interest = m_interest_system->get_interest(interest_id); if (!interest || interest->interest().empty()) continue; // Only transfer entities that have actually crossed into the neighbour's // true area (not just the expanded interest margin). auto true_bounds = proxy->area(); std::vector to_transfer; for (entt::entity entity : interest->interest()) { const Transform* transform = m_world->registry().try_get(entity); if (!transform) continue; glm::vec3 pos = transform->position(); if (pos.x >= true_bounds.min.x && pos.x <= true_bounds.max.x && pos.z >= true_bounds.min.y && pos.z <= true_bounds.max.y) { to_transfer.push_back(entity); } } for (entt::entity entity : to_transfer) { auto* world_entity = m_world->registry().try_get(entity); auto* transform = m_world->registry().try_get(entity); if (!world_entity || !transform) continue; EntityInfo info { .name = world_entity->name, .position = transform->position() }; // Remove client tracking on the sending side. // The owner (e.g. ZoneServer) is responsible for calling add_client on // the receiving zone with the correct session mapping. auto session_it = m_entity_sessions.find(entity); if (session_it != m_entity_sessions.end()) { spdlog::info( "Transferring interest {} entity {} to neighbour zone", session_it->second, (uint32_t)entity ); m_interest_system->remove_interest(session_it->second); m_client_entities.erase(session_it->second); m_entity_sessions.erase(session_it); } proxy->transfer_entity(std::move(info)); // TODO: notify physics world to release the Jolt CharacterVirtual. m_world->registry().destroy(entity); } } } void ZoneManager::update(uint32_t frame_idx, float delta_time) { ZoneScopedN("ZoneManager::tick"); FrameMarkStart("Interest System"); m_interest_system->update(); FrameMarkEnd("Interest System"); check_neighbor_transfers(); FrameMarkStart("World step"); m_world->step(delta_time); FrameMarkEnd("World step"); FrameMarkStart("Physics step"); m_physics_world.step(frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME); FrameMarkEnd("Physics step"); m_session_acked_frame = m_session_input_frame; } } // namespace tw::net