From f78196c2456bb6938371caf29d59373f25c02b83 Mon Sep 17 00:00:00 2001 From: Zach Toogood Date: Thu, 7 May 2026 16:57:14 +0100 Subject: [PATCH 1/5] Core: Use ximesh for raycasts, remove LOSmeshes Co-authored-by: InoUno --- scripts/commands/cansee.lua | 28 + scripts/commands/inwater.lua | 18 + scripts/specs/core/CBaseEntity.lua | 11 + src/map/CMakeLists.txt | 21 +- src/map/ai/controllers/trust_controller.cpp | 4 +- src/map/ai/helpers/pathfind.cpp | 117 +-- src/map/ai/helpers/pathfind.h | 1 - src/map/ai/states/ability_state.cpp | 2 +- src/map/ai/states/magic_state.cpp | 2 +- src/map/ai/states/range_state.cpp | 2 +- src/map/ai/states/weaponskill_state.cpp | 2 +- src/map/entities/baseentity.cpp | 28 +- src/map/entities/baseentity.h | 4 +- src/map/items/item_access.h | 2 +- src/map/items/transaction.cpp | 2 +- src/map/los/CMakeLists.txt | 10 - src/map/los/common.h | 248 ----- src/map/los/los_tree.cpp | 75 -- src/map/los/los_tree.h | 48 - src/map/los/los_tree_node.cpp | 332 ------- src/map/los/los_tree_node.h | 84 -- src/map/los/zone_los.cpp | 150 --- src/map/los/zone_los.h | 49 - src/map/lua/lua_baseentity.cpp | 28 +- src/map/lua/lua_baseentity.h | 3 + src/map/lua/lua_zone.cpp | 41 +- src/map/lua/luautils.cpp | 4 +- src/map/navmesh/inavmesh.h | 80 ++ src/map/{ => navmesh}/navmesh.cpp | 52 - src/map/{ => navmesh}/navmesh.h | 39 +- src/map/{ => navmesh}/navmesh_builder.cpp | 32 +- src/map/{ => navmesh}/navmesh_builder.h | 14 +- src/map/{ => navmesh}/navmesh_config.h | 0 src/map/packets/c2s/0x05e_maprect.cpp | 6 +- src/map/utils/battleutils.cpp | 28 +- src/map/utils/zoneutils.cpp | 14 +- src/map/ximesh/iximesh.h | 110 ++ src/map/ximesh/transformation_matrix.h | 108 ++ src/map/ximesh/vector3.h | 116 +++ src/map/ximesh/ximesh.cpp | 936 ++++++++++++++++++ src/map/ximesh/ximesh.h | 72 ++ src/map/{ximesh.h => ximesh/ximesh_structs.h} | 78 +- src/map/zone.cpp | 118 +-- src/map/zone.h | 32 +- src/map/zone_entities.cpp | 28 +- src/map/zone_mesh.cpp | 482 --------- src/map/zone_mesh.h | 101 -- 47 files changed, 1798 insertions(+), 1964 deletions(-) create mode 100644 scripts/commands/cansee.lua create mode 100644 scripts/commands/inwater.lua delete mode 100644 src/map/los/CMakeLists.txt delete mode 100644 src/map/los/common.h delete mode 100644 src/map/los/los_tree.cpp delete mode 100644 src/map/los/los_tree.h delete mode 100644 src/map/los/los_tree_node.cpp delete mode 100644 src/map/los/los_tree_node.h delete mode 100644 src/map/los/zone_los.cpp delete mode 100644 src/map/los/zone_los.h create mode 100644 src/map/navmesh/inavmesh.h rename src/map/{ => navmesh}/navmesh.cpp (97%) rename src/map/{ => navmesh}/navmesh.h (68%) rename src/map/{ => navmesh}/navmesh_builder.cpp (96%) rename src/map/{ => navmesh}/navmesh_builder.h (87%) rename src/map/{ => navmesh}/navmesh_config.h (100%) create mode 100644 src/map/ximesh/iximesh.h create mode 100644 src/map/ximesh/transformation_matrix.h create mode 100644 src/map/ximesh/vector3.h create mode 100644 src/map/ximesh/ximesh.cpp create mode 100644 src/map/ximesh/ximesh.h rename src/map/{ximesh.h => ximesh/ximesh_structs.h} (55%) delete mode 100644 src/map/zone_mesh.cpp delete mode 100644 src/map/zone_mesh.h diff --git a/scripts/commands/cansee.lua b/scripts/commands/cansee.lua new file mode 100644 index 00000000000..37a5087cde4 --- /dev/null +++ b/scripts/commands/cansee.lua @@ -0,0 +1,28 @@ +----------------------------------- +-- func: cansee +-- desc: Can you see (via ximesh raycasting) your cursor target? +----------------------------------- +---@type TCommand +local commandObj = {} + +commandObj.cmdprops = +{ + permission = 1, + parameters = '' +} + +commandObj.onTrigger = function(player) + local target = player:getCursorTarget() + if not target then + player:printToPlayer('No cursor target provided') + return + end + + if player:canSee(target) then + player:printToPlayer(string.format('%s CAN see %s', player:getName(), target:getName())) + else + player:printToPlayer(string.format('%s CANNOT see %s', player:getName(), target:getName())) + end +end + +return commandObj diff --git a/scripts/commands/inwater.lua b/scripts/commands/inwater.lua new file mode 100644 index 00000000000..a30612c78b0 --- /dev/null +++ b/scripts/commands/inwater.lua @@ -0,0 +1,18 @@ +----------------------------------- +-- func: inwater +-- desc: Are you in water? +----------------------------------- +---@type TCommand +local commandObj = {} + +commandObj.cmdprops = +{ + permission = 1, + parameters = '' +} + +commandObj.onTrigger = function(player) + player:printToPlayer(player:inWater() and 'You are in water' or 'You are on land') +end + +return commandObj diff --git a/scripts/specs/core/CBaseEntity.lua b/scripts/specs/core/CBaseEntity.lua index 372259dc1fe..6db57851adf 100644 --- a/scripts/specs/core/CBaseEntity.lua +++ b/scripts/specs/core/CBaseEntity.lua @@ -555,6 +555,17 @@ end function CBaseEntity:setCarefulPathing(careful) end +---@nodiscard +---@param target CBaseEntity +---@return boolean +function CBaseEntity:canSee(target) +end + +---@nodiscard +---@return boolean +function CBaseEntity:inWater() +end + ---@param seconds integer? ---@return nil function CBaseEntity:openDoor(seconds) diff --git a/src/map/CMakeLists.txt b/src/map/CMakeLists.txt index c2a74c81878..9da808d6d39 100644 --- a/src/map/CMakeLists.txt +++ b/src/map/CMakeLists.txt @@ -3,7 +3,6 @@ add_subdirectory(ai) add_subdirectory(entities) add_subdirectory(enums) add_subdirectory(items) -add_subdirectory(los) add_subdirectory(lua) add_subdirectory(packets) add_subdirectory(utils) @@ -19,7 +18,6 @@ set(SOURCES ${ENTITY_SOURCES} ${ENUMS_SOURCES} ${ITEM_SOURCES} - ${LOS_SOURCES} ${LUA_SOURCES} ${PACKET_SOURCES} ${UTIL_SOURCES} @@ -116,14 +114,12 @@ set(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/modifier.h ${CMAKE_CURRENT_SOURCE_DIR}/monstrosity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/monstrosity.h - ${CMAKE_CURRENT_SOURCE_DIR}/navmesh.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/navmesh.h - ${CMAKE_CURRENT_SOURCE_DIR}/navmesh_builder.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/navmesh_builder.h - ${CMAKE_CURRENT_SOURCE_DIR}/navmesh_config.h - ${CMAKE_CURRENT_SOURCE_DIR}/zone_mesh.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/zone_mesh.h - ${CMAKE_CURRENT_SOURCE_DIR}/ximesh.h + ${CMAKE_CURRENT_SOURCE_DIR}/navmesh/inavmesh.h + ${CMAKE_CURRENT_SOURCE_DIR}/navmesh/navmesh.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/navmesh/navmesh.h + ${CMAKE_CURRENT_SOURCE_DIR}/navmesh/navmesh_builder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/navmesh/navmesh_builder.h + ${CMAKE_CURRENT_SOURCE_DIR}/navmesh/navmesh_config.h ${CMAKE_CURRENT_SOURCE_DIR}/notoriety_container.cpp ${CMAKE_CURRENT_SOURCE_DIR}/notoriety_container.h ${CMAKE_CURRENT_SOURCE_DIR}/packet_system.cpp @@ -166,6 +162,11 @@ set(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/universal_container.h ${CMAKE_CURRENT_SOURCE_DIR}/weapon_skill.cpp ${CMAKE_CURRENT_SOURCE_DIR}/weapon_skill.h + ${CMAKE_CURRENT_SOURCE_DIR}/ximesh/transformation_matrix.h + ${CMAKE_CURRENT_SOURCE_DIR}/ximesh/vector3.h + ${CMAKE_CURRENT_SOURCE_DIR}/ximesh/ximesh_structs.h + ${CMAKE_CURRENT_SOURCE_DIR}/ximesh/ximesh.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ximesh/ximesh.h ${CMAKE_CURRENT_SOURCE_DIR}/zone_entities.cpp ${CMAKE_CURRENT_SOURCE_DIR}/zone_entities.h ${CMAKE_CURRENT_SOURCE_DIR}/zone_instance.cpp diff --git a/src/map/ai/controllers/trust_controller.cpp b/src/map/ai/controllers/trust_controller.cpp index ad05f72fb93..11e5939ef36 100644 --- a/src/map/ai/controllers/trust_controller.cpp +++ b/src/map/ai/controllers/trust_controller.cpp @@ -405,7 +405,9 @@ void CTrustController::PathOutToDistance(CBattleEntity* PTarget, float amount) for (auto& potential_position : positions) { // Validate position - if (!position_found && POwner->PAI->PathFind->ValidPosition(potential_position) && POwner->CanSeeTarget(potential_position, true)) + if (!position_found && + POwner->PAI->PathFind->ValidPosition(potential_position) && + POwner->CanSeeTarget(potential_position)) { position_found = true; target_position = potential_position; diff --git a/src/map/ai/helpers/pathfind.cpp b/src/map/ai/helpers/pathfind.cpp index e2016e93da0..0dce41a54f9 100644 --- a/src/map/ai/helpers/pathfind.cpp +++ b/src/map/ai/helpers/pathfind.cpp @@ -30,8 +30,8 @@ #include "lua/luautils.h" +#include "map/navmesh/navmesh.h" #include "mob_modifier.h" -#include "navmesh.h" #include "status_effect_container.h" #include "zone.h" @@ -82,31 +82,13 @@ bool CPathFind::RoamAround(const position_t& point, float maxRadius, uint8 maxTu m_roamFlags = roamFlags; - if (isNavMeshEnabled()) + if (FindRandomPath(point, maxRadius, maxTurns, roamFlags)) { - if (FindRandomPath(point, maxRadius, maxTurns, roamFlags)) - { - return true; - } - else - { - Clear(); - return false; - } - } - else - { - // no point worm roaming cause it'll move one inch - if (m_roamFlags & ROAMFLAG_WORM) - { - Clear(); - return false; - } - - m_points.emplace_back(pathpoint_t{ { point.x - 1 + rand() % 2, point.y, point.z - 1 + rand() % 2, 0, 0 }, 0s, false }); + return true; } - return true; + Clear(); + return false; } bool CPathFind::PathTo(const position_t& point, uint8 pathFlags, bool clear) @@ -127,37 +109,23 @@ bool CPathFind::PathTo(const position_t& point, uint8 pathFlags, bool clear) m_pathFlags = pathFlags; - if (isNavMeshEnabled()) - { - bool result = false; - - if (m_pathFlags & PATHFLAG_WALLHACK) - { - result = FindClosestPath(m_POwner->loc.p, point); - } - else - { - result = FindPath(m_POwner->loc.p, point); - } + bool result = false; - if (!result) - { - Clear(); - } - - return result; + if (m_pathFlags & PATHFLAG_WALLHACK) + { + result = FindClosestPath(m_POwner->loc.p, point); } else { - if (clear) - { - Clear(); - } + result = FindPath(m_POwner->loc.p, point); + } - m_points.emplace_back(pathpoint_t{ point, 0s, false }); + if (!result) + { + Clear(); } - return true; + return result; } bool CPathFind::PathInRange(const position_t& point, float range, uint8 pathFlags /*= 0*/, bool clear /*= true*/) @@ -248,24 +216,12 @@ void CPathFind::ResumePatrol() } } -bool CPathFind::isNavMeshEnabled() -{ - return m_POwner->loc.zone && m_POwner->loc.zone->m_navMesh != nullptr; -} - bool CPathFind::ValidPosition(const position_t& pos) { TracyZoneScoped; TracyZoneString(m_POwner->getName()); - if (isNavMeshEnabled()) - { - return m_POwner->loc.zone->m_navMesh->validPosition(pos); - } - else - { - return true; - } + return m_POwner->loc.zone->navMesh()->validPosition(pos); } void CPathFind::LimitDistance(float maxLength) @@ -328,9 +284,9 @@ void CPathFind::FollowPath(timer::time_point tick) pathpoint_t targetPoint = m_points[m_currentPoint]; - if (isNavMeshEnabled() && m_carefulPathing) + if (m_carefulPathing) { - m_POwner->loc.zone->m_navMesh->snapToValidPosition(m_POwner->loc.p); + m_POwner->loc.zone->navMesh()->snapToValidPosition(m_POwner->loc.p); } if (m_maxDistance && m_distanceMoved >= m_maxDistance) @@ -476,12 +432,7 @@ bool CPathFind::FindPath(const position_t& start, const position_t& end) return false; } - if (!isNavMeshEnabled()) - { - return false; - } - - m_points = m_POwner->loc.zone->m_navMesh->findPath(start, end); + m_points = m_POwner->loc.zone->navMesh()->findPath(start, end); m_currentPoint = 0; if (m_points.empty()) @@ -498,11 +449,6 @@ bool CPathFind::FindRandomPath(const position_t& start, float maxRadius, uint8 m TracyZoneScoped; TracyZoneString(m_POwner->getName()); - if (!isNavMeshEnabled()) - { - return false; - } - auto m_turnLength = static_cast(xirand::GetRandomNumber(maxTurns) + 1); // Seemingly arbitrary value to pass for maxRadius, all values seem to give similar results, likely due to navmesh polygons being too dense? @@ -513,7 +459,7 @@ bool CPathFind::FindRandomPath(const position_t& start, float maxRadius, uint8 m for (int i = 0; i < m_turnLength * 2; i++) { // look for new turnPoint. findRandomPosition doesn't guarantee the new point is within the radius - auto status = m_POwner->loc.zone->m_navMesh->findRandomPosition(startPosition, maxRadiusForPolyQuery); + auto status = m_POwner->loc.zone->navMesh()->findRandomPosition(startPosition, maxRadiusForPolyQuery); // couldn't find one point so just break out if (status.first != 0) @@ -538,7 +484,7 @@ bool CPathFind::FindRandomPath(const position_t& start, float maxRadius, uint8 m } if (m_turnPoints.size() > 0) { - m_points = m_POwner->loc.zone->m_navMesh->findPath(start, m_turnPoints[0]); + m_points = m_POwner->loc.zone->navMesh()->findPath(start, m_turnPoints[0]); m_currentPoint = 0; } @@ -555,12 +501,7 @@ bool CPathFind::FindClosestPath(const position_t& start, const position_t& end) return false; } - if (!isNavMeshEnabled()) - { - return false; - } - - m_points = m_POwner->loc.zone->m_navMesh->findPath(start, end); + m_points = m_POwner->loc.zone->navMesh()->findPath(start, end); m_currentPoint = 0; m_points.emplace_back(pathpoint_t{ end, 0s, false }); // this prevents exploits with navmesh / impassible terrain @@ -619,12 +560,9 @@ bool CPathFind::AtPoint(const position_t& pos) bool CPathFind::InWater() { - if (isNavMeshEnabled()) - { - return m_POwner->loc.zone->m_navMesh->inWater(m_POwner->loc.p); - } - - return false; + const auto& pos = m_POwner->loc.p; + const auto terrain = m_POwner->loc.zone->xiMesh()->getTerrainAt(pos.x, pos.y, pos.z); + return terrain == TerrainType::ShallowWater || terrain == TerrainType::DeepWater; } const position_t& CPathFind::GetDestination() const @@ -642,7 +580,9 @@ void CPathFind::Clear() m_distanceFromPoint = 0; m_pathFlags = 0; m_roamFlags = 0; + m_points.clear(); + m_timeAtPoint = timer::time_point::min(); m_currentPoint = 0; @@ -686,8 +626,7 @@ void CPathFind::FinishedPath() { m_currentTurn++; - // turning is only available to navmeshed maps - if (m_currentTurn < m_turnPoints.size() && isNavMeshEnabled()) + if (m_currentTurn < m_turnPoints.size()) { // move on to next turn position_t& nextTurn = m_turnPoints[m_currentTurn]; diff --git a/src/map/ai/helpers/pathfind.h b/src/map/ai/helpers/pathfind.h index 7b848944326..f053d2c3c5c 100644 --- a/src/map/ai/helpers/pathfind.h +++ b/src/map/ai/helpers/pathfind.h @@ -100,7 +100,6 @@ class CPathFind // clear current path void Clear(); - bool isNavMeshEnabled(); bool ValidPosition(const position_t& pos); diff --git a/src/map/ai/states/ability_state.cpp b/src/map/ai/states/ability_state.cpp index 99609a7764b..3827609c995 100644 --- a/src/map/ai/states/ability_state.cpp +++ b/src/map/ai/states/ability_state.cpp @@ -280,7 +280,7 @@ bool CAbilityState::CanUseAbility() return false; } - if (m_PEntity->loc.zone->CanUseMisc(MISC_LOS_PLAYER_BLOCK) && !m_PEntity->CanSeeTarget(PTarget, false)) + if (m_PEntity->loc.zone->CanUseMisc(MISC_LOS_PLAYER_BLOCK) && !m_PEntity->CanSeeTarget(PTarget)) { PChar->pushPacket(PChar, PTarget, 0, 0, MsgBasic::UnableToSeeTarget); return false; diff --git a/src/map/ai/states/magic_state.cpp b/src/map/ai/states/magic_state.cpp index db70e97a9c4..34a961b7802 100644 --- a/src/map/ai/states/magic_state.cpp +++ b/src/map/ai/states/magic_state.cpp @@ -403,7 +403,7 @@ bool CMagicState::CanCastSpell(CBattleEntity* PTarget, bool isEndOfCast) } } - if (!isEndOfCast && m_PEntity->objtype == TYPE_PC && m_PEntity->loc.zone->CanUseMisc(MISC_LOS_PLAYER_BLOCK) && !m_PEntity->CanSeeTarget(PTarget, false)) + if (!isEndOfCast && m_PEntity->objtype == TYPE_PC && m_PEntity->loc.zone->CanUseMisc(MISC_LOS_PLAYER_BLOCK) && !m_PEntity->CanSeeTarget(PTarget)) { m_errorMsg = std::make_unique(m_PEntity, PTarget, static_cast(m_PSpell->getID()), 0, MsgBasic::CannotPerformAction); return false; diff --git a/src/map/ai/states/range_state.cpp b/src/map/ai/states/range_state.cpp index 8f159ca5bf7..f8079cbac37 100644 --- a/src/map/ai/states/range_state.cpp +++ b/src/map/ai/states/range_state.cpp @@ -273,7 +273,7 @@ bool CRangeState::CanUseRangedAttack(CBattleEntity* PTarget, bool isEndOfAttack) return false; } - if (!isEndOfAttack && !m_PEntity->CanSeeTarget(PTarget, false)) + if (!isEndOfAttack && !m_PEntity->CanSeeTarget(PTarget)) { m_errorMsg = std::make_unique(m_PEntity, PTarget, 0, 0, MsgBasic::CannotPerformAction); return false; diff --git a/src/map/ai/states/weaponskill_state.cpp b/src/map/ai/states/weaponskill_state.cpp index 6816c01d4ab..742ee81be85 100644 --- a/src/map/ai/states/weaponskill_state.cpp +++ b/src/map/ai/states/weaponskill_state.cpp @@ -57,7 +57,7 @@ CWeaponSkillState::CWeaponSkillState(CBattleEntity* PEntity, uint16 targid, uint } } - if (!m_PEntity->CanSeeTarget(PTarget, false)) + if (!m_PEntity->CanSeeTarget(PTarget)) { throw CStateInitException(std::make_unique(m_PEntity, PTarget, 0, 0, MsgBasic::CannotPerformAction)); } diff --git a/src/map/entities/baseentity.cpp b/src/map/entities/baseentity.cpp index cd82f2dedfe..f5bdfa35962 100644 --- a/src/map/entities/baseentity.cpp +++ b/src/map/entities/baseentity.cpp @@ -27,10 +27,11 @@ #include "battlefield.h" #include "instance.h" -#include "los/zone_los.h" -#include "navmesh.h" +#include "map/navmesh/navmesh.h" #include "zone.h" +#include + #include CBaseEntity::CBaseEntity() @@ -171,23 +172,22 @@ bool CBaseEntity::isWideScannable() return status != STATUS_TYPE::DISAPPEAR && !IsNameHidden() && !GetUntargetable(); } -bool CBaseEntity::CanSeeTarget(CBaseEntity* target, bool fallbackNavMesh) +bool CBaseEntity::CanSeeTarget(CBaseEntity* target) { - return CanSeeTarget(target->loc.p, fallbackNavMesh); + return CanSeeTarget(target->loc.p); } -bool CBaseEntity::CanSeeTarget(const position_t& targetPointBase, bool fallbackNavMesh) +bool CBaseEntity::CanSeeTarget(const position_t& targetPointBase) { - if (loc.zone->lineOfSight) - { - return loc.zone->lineOfSight->CanEntitySee(this, targetPointBase); - } - else if (fallbackNavMesh && loc.zone->m_navMesh) - { - return loc.zone->m_navMesh->raycast(loc.p, targetPointBase); - } + constexpr float ENTITY_HEIGHT = 2.0f; + + // TODO: Handle: + // if (GetTypeMask() & ZONE_TYPE::CITY || (m_miscMask & MISC_LOS_OFF)) + // -> Skip cities and zones with line of sight turned off - return true; + const auto src = Vector3{ loc.p.x, loc.p.y - ENTITY_HEIGHT, loc.p.z }; + const auto dst = Vector3{ targetPointBase.x, targetPointBase.y - ENTITY_HEIGHT, targetPointBase.z }; + return !this->loc.zone->xiMesh()->rayIntersect(src, dst); } CBaseEntity* CBaseEntity::GetEntity(uint16 targid, uint8 filter) const diff --git a/src/map/entities/baseentity.h b/src/map/entities/baseentity.h index d83ace6d593..585527d3605 100644 --- a/src/map/entities/baseentity.h +++ b/src/map/entities/baseentity.h @@ -282,8 +282,8 @@ class CBaseEntity virtual bool GetUntargetable() const; // checks if entity is untargetable virtual bool isWideScannable(); // checks if the entity should show up on wide scan - bool CanSeeTarget(CBaseEntity* target, bool fallbackNavMesh = true); - bool CanSeeTarget(const position_t& targetPoint, bool fallbackNavMesh = true); + bool CanSeeTarget(CBaseEntity* target); + bool CanSeeTarget(const position_t& targetPoint); CBaseEntity* GetEntity(uint16 targid, uint8 filter = -1) const; void SendZoneUpdate(); diff --git a/src/map/items/item_access.h b/src/map/items/item_access.h index f6e30f90484..35d87907e36 100644 --- a/src/map/items/item_access.h +++ b/src/map/items/item_access.h @@ -79,7 +79,7 @@ struct ItemAccess // Refuse on either side of the transition. if (target == ItemState::InTransaction || item->state() == ItemState::InTransaction) { - ShowErrorFmt("ItemAccess::mark: illegal tx transition for item {} (current={}, target={}) — use tx commit/rollback path", + ShowErrorFmt("ItemAccess::mark: illegal tx transition for item {} (current={}, target={}) - use tx commit/rollback path", item->getID(), magic_enum::enum_name(item->state()), magic_enum::enum_name(target)); diff --git a/src/map/items/transaction.cpp b/src/map/items/transaction.cpp index 3c8e182df96..ac81226b684 100644 --- a/src/map/items/transaction.cpp +++ b/src/map/items/transaction.cpp @@ -49,7 +49,7 @@ Transaction::~Transaction() { if (this->state_ == TransactionState::Open) { - ShowErrorFmt("Transaction::~Transaction: tx {} still Open — subclass dtor must call rollbackIfOpen()", this->id_); + ShowErrorFmt("Transaction::~Transaction: tx {} still Open - subclass dtor must call rollbackIfOpen()", this->id_); std::abort(); } } diff --git a/src/map/los/CMakeLists.txt b/src/map/los/CMakeLists.txt deleted file mode 100644 index 9dde997a879..00000000000 --- a/src/map/los/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -set(LOS_SOURCES - ${CMAKE_CURRENT_SOURCE_DIR}/common.h - ${CMAKE_CURRENT_SOURCE_DIR}/los_tree_node.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/los_tree_node.h - ${CMAKE_CURRENT_SOURCE_DIR}/los_tree.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/los_tree.h - ${CMAKE_CURRENT_SOURCE_DIR}/zone_los.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/zone_los.h - PARENT_SCOPE -) diff --git a/src/map/los/common.h b/src/map/los/common.h deleted file mode 100644 index ac7df2e858d..00000000000 --- a/src/map/los/common.h +++ /dev/null @@ -1,248 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2021 Eden Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#ifndef _LOS_COMMON_H -#define _LOS_COMMON_H - -#include -#include -#include - -enum class Axis : uint8 -{ - None = 255, - X = 0, - Y = 1, - Z = 2, -}; - -struct BoundingBox -{ - float coords[6]; - - float getAxisMin(Axis axis) - { - return coords[((int)axis) * 2]; - } - - float getAxisMax(Axis axis) - { - return coords[((int)axis) * 2 + 1]; - } - - float getAxisSize(Axis axis) - { - return getAxisMax(axis) - getAxisMin(axis); - } - - float getAxisMiddle(Axis axis) - { - float axisMin = getAxisMin(axis); - return (getAxisMax(axis) - axisMin) / 2 + axisMin; - } - - void expandTo(const BoundingBox& other) - { - if (other.coords[0] < coords[0]) - { - coords[0] = other.coords[0]; - } - if (other.coords[1] > coords[1]) - { - coords[1] = other.coords[1]; - } - - if (other.coords[3] > coords[3]) - { - coords[3] = other.coords[3]; - } - if (other.coords[4] < coords[4]) - { - coords[4] = other.coords[4]; - } - - if (other.coords[5] < coords[5]) - { - coords[5] = other.coords[5]; - } - if (other.coords[5] > coords[5]) - { - coords[5] = other.coords[5]; - } - } -}; - -struct Vector3D -{ - float x; - float y; - float z; - - // Addition - Vector3D operator+(const Vector3D& vec) - { - return Vector3D{ x + vec.x, y + vec.y, z + vec.z }; - } - - Vector3D& operator+=(const Vector3D& vec) - { - x += vec.x; - y += vec.y; - z += vec.z; - return *this; - } - - // Subtraction - Vector3D operator-(const Vector3D& vec) - { - return Vector3D{ x - vec.x, y - vec.y, z - vec.z }; - } - - Vector3D& operator-=(const Vector3D& vec) - { - x -= vec.x; - y -= vec.y; - z -= vec.z; - return *this; - } - - // Scalar multiplication - Vector3D operator*(float value) - { - return Vector3D{ x * value, y * value, z * value }; - } - - Vector3D& operator*=(float value) - { - x *= value; - y *= value; - z *= value; - return *this; - } - - // Scalar division - Vector3D operator/(float value) - { - return Vector3D{ x / value, y / value, z / value }; - } - - Vector3D& operator/=(float value) - { - x /= value; - y /= value; - z /= value; - return *this; - } - - // Misc other - Vector3D crossProduct(const Vector3D& other) const - { - float ni = y * other.z - z * other.y; - float nj = z * other.x - x * other.z; - float nk = x * other.y - y * other.x; - return Vector3D{ ni, nj, nk }; - } - - float dotProduct(const Vector3D& other) const - { - return x * other.x + y * other.y + z * other.z; - } - - float magnitude() const - { - return sqrt(x * x + y * y + z * z); - } -}; - -struct Triangle -{ - Vector3D vertices[3]; - - BoundingBox getBoundingBox() - { - return BoundingBox{ - std::min(std::min(vertices[0].x, vertices[1].x), vertices[2].x), - std::max(std::max(vertices[0].x, vertices[1].x), vertices[2].x), - std::min(std::min(vertices[0].y, vertices[1].y), vertices[2].y), - std::max(std::max(vertices[0].y, vertices[1].y), vertices[2].y), - std::min(std::min(vertices[0].z, vertices[1].z), vertices[2].z), - std::max(std::max(vertices[0].z, vertices[1].z), vertices[2].z), - }; - } - - // Taken from: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm#C++_implementation - Maybe doesRayIntersect(Vector3D rayOrigin, Vector3D rayVector) - { - constexpr float EPSILON = 0.0000001f; - - Vector3D edge1; - Vector3D edge2; - Vector3D h; - Vector3D s; - Vector3D q; - - float a; - float f; - float u; - float v; - - edge1 = vertices[1] - vertices[0]; - edge2 = vertices[2] - vertices[0]; - h = rayVector.crossProduct(edge2); - a = edge1.dotProduct(h); - - if (a > -EPSILON && a < EPSILON) - { - return std::nullopt; // This ray is parallel to this triangle. - } - - f = 1.0f / a; - s = rayOrigin - vertices[0]; - u = f * s.dotProduct(h); - - if (u < 0.0 || u > 1.0) - { - return std::nullopt; - } - - q = s.crossProduct(edge1); - v = f * rayVector.dotProduct(q); - - if (v < 0.0 || u + v > 1.0) - { - return std::nullopt; - } - - // At this stage we can compute t to find out where the intersection point is on the line. - float t = f * edge2.dotProduct(q); - if (t > EPSILON && t <= 1.f) // ray intersection - { - Vector3D outIntersectionPoint = rayOrigin + rayVector * t; - return outIntersectionPoint; - } - else // This means that there is a line intersection but not a ray intersection. - { - return std::nullopt; - } - } -}; - -#endif // _LOS_COMMON_H diff --git a/src/map/los/los_tree.cpp b/src/map/los/los_tree.cpp deleted file mode 100644 index 6a581a1e056..00000000000 --- a/src/map/los/los_tree.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2021 Eden Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#include "los_tree.h" - -LosTree::LosTree(Triangle* elements, int elementCount) -{ - this->elements = elements; - this->elementNexts = new int[elementCount]; - this->elementCount = elementCount; - - BoundingBox* boundingBoxes = new BoundingBox[elementCount]; - int* indices = new int[elementCount]; - - for (int i = 0; i < elementCount; i++) - { - indices[i] = i; - this->elementNexts[i] = -1; - - this->elements[i] = elements[i]; - boundingBoxes[i] = elements[i].getBoundingBox(); - } - - root = new LosTreeNode(this->elements, boundingBoxes, this->elementNexts, indices, 0, elementCount - 1, 100, 1, 5, true); - - destroy_arr(boundingBoxes); - destroy_arr(indices); -} - -LosTree::~LosTree() -{ - destroy(root); - destroy_arr(elements); - destroy_arr(elementNexts); -} - -LosTreeNodeStats LosTree::GetStats() -{ - TracyZoneScoped; - return root->GetStats(this->elementNexts, this->elements); -} - -Maybe LosTree::DoesRayCollide(Vector3D& rayOrigin, Vector3D& rayEnd) const -{ - TracyZoneScoped; - BoundingBox bounds = BoundingBox(); - bounds.coords[0] = std::min(rayOrigin.x, rayEnd.x); - bounds.coords[1] = std::max(rayOrigin.x, rayEnd.x); - bounds.coords[2] = std::min(rayOrigin.y, rayEnd.y); - bounds.coords[3] = std::max(rayOrigin.y, rayEnd.y); - bounds.coords[4] = std::min(rayOrigin.z, rayEnd.z); - bounds.coords[5] = std::max(rayOrigin.z, rayEnd.z); - - Vector3D rayVector = rayEnd - rayOrigin; - - return root->DoesRayCollide(bounds, rayOrigin, rayVector, this->elementNexts, this->elements); -} diff --git a/src/map/los/los_tree.h b/src/map/los/los_tree.h deleted file mode 100644 index 4a5bcf6fa2d..00000000000 --- a/src/map/los/los_tree.h +++ /dev/null @@ -1,48 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2021 Eden Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#ifndef _LOS_TREE_H -#define _LOS_TREE_H - -#include "common/cbasetypes.h" - -#include "common.h" -#include "los_tree_node.h" - -class LosTree -{ -public: - LosTree(Triangle* elements, int elementCount); - ~LosTree(); - - LosTreeNodeStats GetStats(); - - Maybe DoesRayCollide(Vector3D& rayOrigin, Vector3D& rayVector) const; - -private: - Triangle* elements; - int* elementNexts; - size_t elementCount; - - LosTreeNode* root = nullptr; -}; - -#endif // _LOS_TREE_H diff --git a/src/map/los/los_tree_node.cpp b/src/map/los/los_tree_node.cpp deleted file mode 100644 index 8a2ef2adcc2..00000000000 --- a/src/map/los/los_tree_node.cpp +++ /dev/null @@ -1,332 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2021 Eden Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#include "los_tree_node.h" - -#include "common/utils.h" - -#define TOP_ANGLE M_PI / 5 -#define BOTTOM_ANGLE M_PI - M_PI / 5 - -template -int splitArraySort(int* arr, int start, int end, F&& splitFunc) -{ - while (start < end) - { - while (start < end && splitFunc(arr[start])) - { - ++start; - } - - while (start < end && !splitFunc(arr[end])) - { - --end; - } - - if (start >= end) - { - break; - } - - std::swap(arr[start++], arr[end--]); - } - return start; -} - -LosTreeNode::LosTreeNode( - Triangle* elements, - BoundingBox* boundingBoxes, - int* elementNexts, - int* elementIndices, - int indexStart, - int indexEnd, - int splitsLeft, - float boxSizeThreshold, - size_t elementsThreshold, - bool normalSplit) -{ - int indexCount = indexEnd - indexStart + 1; - if (indexCount <= 0) - { - // No elements to store in this node. - return; - } - - if (splitsLeft <= 0 || (int)elementsThreshold >= indexCount) - { - SetElements(elements, elementNexts, elementIndices, indexStart, indexEnd); - return; - } - - int indexSplit = indexStart; - - // Split by the normal vector of the triangle to separate vertical and horizontal layers - if (normalSplit) - { - splitAxis = Axis::None; - - // clang-format off - indexSplit = splitArraySort(elementIndices, indexStart, indexEnd, [&elements](int index) - { - auto element = elements[index]; - auto normal = (element.vertices[1] - element.vertices[0]).crossProduct(element.vertices[2] - element.vertices[0]); - auto angle = acosf(normal.y / normal.magnitude()); - return angle <= TOP_ANGLE || angle >= BOTTOM_ANGLE; - }); - // clang-format on - } - else - { - // Use longest axis as heuritistic for spliting the node, and split by its median of objects. - BoundingBox nodeBox = boundingBoxes[indexStart]; - float medians[3] = { 0, 0, 0 }; - for (auto i = indexStart; i <= indexEnd; i++) - { - auto& bounds = boundingBoxes[elementIndices[i]]; - nodeBox.expandTo(bounds); - medians[(int)Axis::X] += bounds.getAxisMiddle(Axis::X); - medians[(int)Axis::Y] += bounds.getAxisMiddle(Axis::Y); - medians[(int)Axis::Z] += bounds.getAxisMiddle(Axis::Z); - } - - float biggestAxisSize = 0; - splitAxis = Axis::None; - for (auto i = 0; i < 3; i++) - { - medians[i] /= indexCount; - float axisSize = nodeBox.getAxisSize((Axis)i); - if (axisSize > biggestAxisSize) - { - biggestAxisSize = axisSize; - splitAxis = (Axis)i; - } - } - - if (nodeBox.getAxisSize(splitAxis) < boxSizeThreshold) - { - SetElements(elements, elementNexts, elementIndices, indexStart, indexEnd); - return; - } - - // Split the nodes based on the axis - float splitValue = medians[(int)splitAxis]; - leftMax = nodeBox.getAxisMin(splitAxis); - rightMin = nodeBox.getAxisMax(splitAxis); - - // clang-format off - indexSplit = splitArraySort(elementIndices, indexStart, indexEnd, [&boundingBoxes, &splitValue, this](int index) - { - auto& bounds = boundingBoxes[index]; - if (bounds.getAxisMiddle(splitAxis) < splitValue) - { - leftMax = std::max(leftMax, bounds.getAxisMax(splitAxis)); - return true; - } - else - { - rightMin = std::min(rightMin, bounds.getAxisMin(splitAxis)); - return false; - } - }); - // clang-format on - } - - if (indexSplit > indexStart) - { - left = new LosTreeNode(elements, boundingBoxes, elementNexts, elementIndices, indexStart, indexSplit - 1, splitsLeft - 1, boxSizeThreshold, elementsThreshold); - minY = left->minY; - maxY = left->maxY; - } - - if (indexSplit <= indexEnd) - { - right = new LosTreeNode(elements, boundingBoxes, elementNexts, elementIndices, indexSplit, indexEnd, splitsLeft - 1, boxSizeThreshold, elementsThreshold); - if (right->minY < minY) - { - minY = right->minY; - } - if (right->maxY > maxY) - { - maxY = right->maxY; - } - } -} - -LosTreeNode::~LosTreeNode() -{ - destroy(left); - destroy(right); -} - -void LosTreeNode::SetElements(Triangle* elements, int* elementNexts, int* elementIndices, int indexStart, int indexEnd) -{ - // Store elements in a singly-linked list for this node. - headElementIdx = elementIndices[indexStart]; - auto& element = elements[headElementIdx]; - for (auto& vertex : element.vertices) - { - if (vertex.y > maxY) - { - maxY = vertex.y; - } - if (vertex.y < minY) - { - minY = vertex.y; - } - } - - for (int i = indexStart + 1; i <= indexEnd; i++) - { - auto& element = elements[elementIndices[i]]; - for (auto& vertex : element.vertices) - { - if (vertex.y > maxY) - { - maxY = vertex.y; - } - if (vertex.y < minY) - { - minY = vertex.y; - } - } - elementNexts[elementIndices[i - 1]] = elementIndices[i]; - } -} - -Maybe LosTreeNode::DoesRayCollide(BoundingBox& bounds, Vector3D& rayOrigin, Vector3D& rayVector, int* elementNexts, Triangle* elements) const -{ - TracyZoneScoped; - if (bounds.coords[2] > maxY || bounds.coords[3] < minY) - { - return std::nullopt; - } - - if (headElementIdx != -1) - { - int idx = headElementIdx; - while (idx != -1) - { - if (auto vec = elements[idx].doesRayIntersect(rayOrigin, rayVector)) - { - return vec; - } - idx = elementNexts[idx]; - } - - return std::nullopt; - } - - // Special case if split axis is not defined. Both children are visited. - if (splitAxis == Axis::None) - { - if (right) - { - if (auto rightPos = right->DoesRayCollide(bounds, rayOrigin, rayVector, elementNexts, elements)) - { - return rightPos; - } - } - - if (left) - { - if (auto leftPos = left->DoesRayCollide(bounds, rayOrigin, rayVector, elementNexts, elements)) - { - return leftPos; - } - } - - return std::nullopt; - } - - if (right && bounds.getAxisMax(splitAxis) >= rightMin) - { - if (auto rightPos = right->DoesRayCollide(bounds, rayOrigin, rayVector, elementNexts, elements)) - { - return rightPos; - } - } - - if (left && bounds.getAxisMin(splitAxis) <= leftMax) - { - if (auto leftPos = left->DoesRayCollide(bounds, rayOrigin, rayVector, elementNexts, elements)) - { - return leftPos; - } - } - - return std::nullopt; -} - -LosTreeNodeStats LosTreeNode::GetStats(int* elementNexts, Triangle* elements) -{ - TracyZoneScoped; - LosTreeNodeStats stats; - if (headElementIdx != -1) - { - int count = 1; - stats.boundingBox = elements[headElementIdx].getBoundingBox(); - - int next = elementNexts[headElementIdx]; - while (next != -1) - { - stats.boundingBox.expandTo(elements[next].getBoundingBox()); - next = elementNexts[next]; - count++; - } - - stats.emptyNodes = 0; - stats.nodes = 1; - stats.maxDepth = 1; - stats.minDepth = 1; - stats.minElements = count; - stats.maxElements = count; - - stats.maxAxis = std::max(std::max(stats.boundingBox.getAxisSize(Axis::X), stats.boundingBox.getAxisSize(Axis::Y)), stats.boundingBox.getAxisSize(Axis::Z)); - - return stats; - } - - LosTreeNodeStats leftStats = left ? left->GetStats(elementNexts, elements) : LosTreeNodeStats(); - LosTreeNodeStats rightStats = right ? right->GetStats(elementNexts, elements) : LosTreeNodeStats(); - stats.nodes = 1 + leftStats.nodes + rightStats.nodes; - stats.emptyNodes = (left ? leftStats.emptyNodes : 1) + (rightStats.emptyNodes ? 0 : 1); - stats.minDepth = std::min(left ? leftStats.minDepth : 0, right ? rightStats.minDepth : 0) + 1; - stats.maxDepth = std::max(left ? leftStats.maxDepth : 0, right ? rightStats.maxDepth : 0) + 1; - stats.minElements = std::min(leftStats.minElements, rightStats.minElements); - stats.maxElements = std::max(leftStats.maxElements, rightStats.maxElements); - stats.maxAxis = std::max(leftStats.maxAxis, rightStats.maxAxis); - - if (!left) - { - stats.boundingBox = rightStats.boundingBox; - } - else if (!right) - { - stats.boundingBox = leftStats.boundingBox; - } - else - { - stats.boundingBox = leftStats.boundingBox; - stats.boundingBox.expandTo(rightStats.boundingBox); - } - - return stats; -} diff --git a/src/map/los/los_tree_node.h b/src/map/los/los_tree_node.h deleted file mode 100644 index dedcbe38e02..00000000000 --- a/src/map/los/los_tree_node.h +++ /dev/null @@ -1,84 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2021 Eden Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#ifndef _LOS_TREE_NODE_H -#define _LOS_TREE_NODE_H - -#include "common/cbasetypes.h" - -#include "common.h" - -struct LosTreeNodeStats -{ - int minDepth = INT_MAX; - int maxDepth = INT_MIN; - int minElements = INT_MAX; - int maxElements = INT_MIN; - int nodes = 0; - int emptyNodes = 0; - - float maxAxis = 0; - - BoundingBox boundingBox; -}; - -class LosTreeNode -{ -public: - LosTreeNode( - Triangle* elements, - BoundingBox* boundingBoxes, - int* elementNexts, - int* elementIndices, - int indexStart, - int indexEnd, - int splitsLeft, - float boxSizeThreshold, - size_t elementsThreshold, - bool normalSplit = false); - - ~LosTreeNode(); - - LosTreeNodeStats GetStats(int* elementNexts, Triangle* elements); - - Maybe DoesRayCollide( - BoundingBox& bounds, - Vector3D& rayOrigin, - Vector3D& rayVector, - int* elementNexts, - Triangle* elements) const; - -private: - void SetElements(Triangle* elements, int* elementNexts, int* elementIndices, int indexStart, int indexEnd); - int headElementIdx = -1; - - // Keep bounds for Y-axis since rays are usually mostly horizontal, so we can skip a bunch of triangle checks. - float minY = 100000.0f; - float maxY = -100000.0f; - - Axis splitAxis = Axis::None; - float leftMax = 0; - float rightMin = 0; - LosTreeNode* left = nullptr; - LosTreeNode* right = nullptr; -}; - -#endif // _LOS_TREE_NODE_H diff --git a/src/map/los/zone_los.cpp b/src/map/los/zone_los.cpp deleted file mode 100644 index 2679123fe50..00000000000 --- a/src/map/los/zone_los.cpp +++ /dev/null @@ -1,150 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2021 Eden Dev Team - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#include "zone_los.h" - -#ifndef FAST_OBJ_IMPLEMENTATION -#define FAST_OBJ_IMPLEMENTATION -#include -#endif - -#include "common/tracy.h" -#include "common/zlib.h" -#include "entities/baseentity.h" - -#ifdef LOS_DEBUG -float totalMemory = 0; -#endif - -namespace -{ - -float ENTITY_HEIGHT = 2.0f; - -} - -ZoneLos::ZoneLos(Triangle* elements, int elementCount) -: tree(LosTree(elements, elementCount)) -{ -} - -auto ZoneLos::Load(uint16 zoneId, const std::string& pathToObj) -> std::unique_ptr -{ - TracyZoneScoped; - - // Check if file exists before loading the OBJ model. - if (FILE* file = fopen(pathToObj.c_str(), "r")) - { - fclose(file); - } - else - { - // No matching line-of-sight mesh file found, so skip it for this zone silently. - return nullptr; - } - - fastObjMesh* mesh = fast_obj_read(pathToObj.c_str()); - if (!mesh) - { - ShowWarning("Failed to load line-of-sight mesh: %s", pathToObj); - return nullptr; - } - - Triangle* elements = new Triangle[mesh->face_count]; - - // Loop over groups - unsigned int index_offset = 0; - for (unsigned int gi = 0; gi < mesh->group_count; gi++) - { - const fastObjGroup& grp = mesh->groups[gi]; - - // Loop over faces - for (unsigned int fi = 0; fi < mesh->face_count; fi++) - { - unsigned int fv = mesh->face_vertices[grp.face_offset + fi]; - - if (fv != 3) - { - ShowWarning("Skipping polygon with %d vertices. Expected 3.", fv); - continue; - } - - // Loop over vertices in the face. - for (unsigned int v = 0; v < fv; v++) - { - const fastObjIndex& mi = mesh->indices[grp.index_offset + index_offset + v]; - elements[fi].vertices[v] = { mesh->positions[3 * mi.p + 0], -mesh->positions[3 * mi.p + 1], -mesh->positions[3 * mi.p + 2] }; - } - index_offset += fv; - } - } - - auto zoneLos = std::unique_ptr(new ZoneLos(elements, mesh->face_count)); - -#ifdef LOS_DEBUG - auto stats = zoneLos->tree.GetStats(); - ShowDebug(""); - ShowDebug("File: %s", pathToObj); - ShowDebug("Nodes: %d", stats.nodes); - ShowDebug("Empty nodes: %d", stats.emptyNodes); - ShowDebug("Max elements: %d", stats.maxElements); - float treeMem = stats.nodes * sizeof(LosTreeNode) / 1000000.0f; - float elementMem = (mesh->face_count * sizeof(Triangle) + mesh->face_count * sizeof(int)) / 1000000.0f; - totalMemory += treeMem + elementMem; - ShowDebug("Tree memory (%db): %.2f mb", sizeof(LosTreeNode), treeMem); - ShowDebug("Element memory (%db): %.2f mb", sizeof(Triangle), elementMem); - ShowDebug("Total memory: %.2f mb", totalMemory); -#endif - - fast_obj_destroy(mesh); - - return zoneLos; -} - -bool ZoneLos::CanEntitySee(CBaseEntity* source, CBaseEntity* target) const -{ - TracyZoneScoped; - return CanEntitySee(source, target->loc.p); -} - -bool ZoneLos::CanEntitySee(CBaseEntity* source, const position_t& targetPointBase) const -{ - TracyZoneScoped; - return !DoesRayCollide({ source->loc.p.x, source->loc.p.y - ENTITY_HEIGHT, source->loc.p.z }, { targetPointBase.x, targetPointBase.y - ENTITY_HEIGHT, targetPointBase.z }); -} - -Maybe ZoneLos::Raycast(CBaseEntity* source, CBaseEntity* target) const -{ - TracyZoneScoped; - return Raycast(source->loc.p, target->loc.p); -} - -Maybe ZoneLos::Raycast(const position_t& source, const position_t& target) const -{ - TracyZoneScoped; - return DoesRayCollide({ source.x, source.y, source.z }, { target.x, target.y, target.z }); -} - -Maybe ZoneLos::DoesRayCollide(Vector3D rayOrigin, Vector3D rayEnd) const -{ - TracyZoneScoped; - return tree.DoesRayCollide(rayOrigin, rayEnd); -} diff --git a/src/map/los/zone_los.h b/src/map/los/zone_los.h deleted file mode 100644 index 851eeb39301..00000000000 --- a/src/map/los/zone_los.h +++ /dev/null @@ -1,49 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2021 Eden Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#ifndef _ZONE_LOS_H -#define _ZONE_LOS_H - -#include "common/mmo.h" -#include "los_tree.h" - -class CBaseEntity; - -class ZoneLos -{ -public: - static auto Load(uint16 zoneId, const std::string& pathToObj) -> std::unique_ptr; - - bool CanEntitySee(CBaseEntity* source, CBaseEntity* target) const; - bool CanEntitySee(CBaseEntity* source, const position_t& targetPointBase) const; - - Maybe Raycast(CBaseEntity* source, CBaseEntity* target) const; - Maybe Raycast(const position_t& source, const position_t& target) const; - -private: - ZoneLos(Triangle* elements, int elementCount); - - Maybe DoesRayCollide(Vector3D rayOrigin, Vector3D rayEnd) const; - - LosTree tree; -}; - -#endif // _ZONE_LOS_H diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 4172d527f19..6efe47e1b51 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -70,7 +70,6 @@ #include "treasure_pool.h" #include "weapon_skill.h" #include "zone.h" -#include "zone_mesh.h" #include "ai/ai_container.h" @@ -2231,6 +2230,31 @@ void CLuaBaseEntity::setCarefulPathing(bool careful) } } +/************************************************************************ + * Function: canSee(...) + * Purpose : + * Example : + ************************************************************************/ + +bool CLuaBaseEntity::canSee(const CLuaBaseEntity* target) +{ + return m_PBaseEntity->CanSeeTarget(target->GetBaseEntity()); +} + +/************************************************************************ + * Function: inWater(...) + * Purpose : + * Example : + ************************************************************************/ + +bool CLuaBaseEntity::inWater() +{ + // NOTE: Same logic as in PathFind::InWater + const auto& pos = m_PBaseEntity->loc.p; + const auto terrain = m_PBaseEntity->loc.zone->xiMesh()->getTerrainAt(pos.x, pos.y, pos.z); + return terrain == TerrainType::ShallowWater || terrain == TerrainType::DeepWater; +} + /************************************************************************ * Function: openDoor() * Purpose : Opens a door for 7 seconds; different time can be specified @@ -19716,6 +19740,8 @@ void CLuaBaseEntity::Register() SOL_REGISTER("hasFollowTarget", CLuaBaseEntity::hasFollowTarget); SOL_REGISTER("unfollow", CLuaBaseEntity::unfollow); SOL_REGISTER("setCarefulPathing", CLuaBaseEntity::setCarefulPathing); + SOL_REGISTER("canSee", CLuaBaseEntity::canSee); + SOL_REGISTER("inWater", CLuaBaseEntity::inWater); SOL_REGISTER("openDoor", CLuaBaseEntity::openDoor); SOL_REGISTER("closeDoor", CLuaBaseEntity::closeDoor); diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index 475e88cc6ac..86579fab532 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -160,6 +160,9 @@ class CLuaBaseEntity // int32 LimitDistance(lua_Stat* L); // limits the current path distance to given max distance void setCarefulPathing(bool careful); + bool canSee(const CLuaBaseEntity* target); + bool inWater(); + void openDoor(const sol::object& seconds); void closeDoor(const sol::object& seconds); void setElevator(uint8 id, uint32 lowerDoor, uint32 upperDoor, uint32 elevatorId, bool reversed); diff --git a/src/map/lua/lua_zone.cpp b/src/map/lua/lua_zone.cpp index 41fa1fb4a05..1780fc9e4ec 100644 --- a/src/map/lua/lua_zone.cpp +++ b/src/map/lua/lua_zone.cpp @@ -26,14 +26,14 @@ #include "entities/charentity.h" #include "entities/npcentity.h" -#include "los/zone_los.h" #include "lua_baseentity.h" -#include "navmesh.h" +#include "map/navmesh/navmesh.h" #include "trigger_area.h" #include "utils/mobutils.h" #include "zone.h" #include "zone_entities.h" -#include "zone_mesh.h" + +#include CLuaZone::CLuaZone(CZone* PZone) : m_pLuaZone(PZone) @@ -250,14 +250,7 @@ bool CLuaZone::isNavigablePoint(const sol::table& point) }; // clang-format on - if (m_pLuaZone->m_navMesh) - { - return m_pLuaZone->m_navMesh->validPosition(position); - } - else // No navmesh, just nod and smile - { - return true; - } + return m_pLuaZone->navMesh()->validPosition(position); } auto CLuaZone::insertDynamicEntity(sol::table table) -> CBaseEntity* @@ -351,15 +344,10 @@ sol::table CLuaZone::queryEntitiesByName(const std::string& name) auto CLuaZone::getTerrainType(const sol::table& position) -> TerrainType { - if (auto mesh = m_pLuaZone->zoneMesh()) - { - return (*mesh)->getTerrainAt( - position["x"].get_or(0), - position["y"].get_or(0), - position["z"].get_or(0)); - } - - return TerrainType::None; + return m_pLuaZone->xiMesh()->getTerrainAt( + position["x"].get_or(0), + position["y"].get_or(0), + position["z"].get_or(0)); } /************************************************************************ @@ -370,15 +358,10 @@ auto CLuaZone::getTerrainType(const sol::table& position) -> TerrainType auto CLuaZone::getFloorId(const sol::table& position) -> uint8 { - if (auto mesh = m_pLuaZone->zoneMesh()) - { - return (*mesh)->getFloorId( - position["x"].get_or(0), - position["y"].get_or(0), - position["z"].get_or(0)); - } - - return 0; + return m_pLuaZone->xiMesh()->getFloorId( + position["x"].get_or(0), + position["y"].get_or(0), + position["z"].get_or(0)); } //======================================================// diff --git a/src/map/lua/luautils.cpp b/src/map/lua/luautils.cpp index d11b34f6f36..59ab90c5b87 100644 --- a/src/map/lua/luautils.cpp +++ b/src/map/lua/luautils.cpp @@ -80,11 +80,11 @@ #include "instance.h" #include "ipc_client.h" #include "items/item_furnishing.h" +#include "map/navmesh/navmesh.h" #include "map_engine.h" #include "mob_modifier.h" #include "mobskill.h" #include "monstrosity.h" -#include "navmesh.h" #include "packets/s2c/0x039_mapschedulor.h" #include "petskill.h" #include "roe.h" @@ -5108,7 +5108,7 @@ sol::table GetFurthestValidPosition(CLuaBaseEntity* fromTarget, float distance, position_t pos = nearPosition(entity->loc.p, distance, theta); float validPos[3]; - bool success = entity->loc.zone->m_navMesh->findFurthestValidPoint(entity->loc.p, pos, validPos); + bool success = entity->loc.zone->navMesh()->findFurthestValidPoint(entity->loc.p, pos, validPos); if (!success) { return sol::lua_nil; diff --git a/src/map/navmesh/inavmesh.h b/src/map/navmesh/inavmesh.h new file mode 100644 index 00000000000..3ed7792eed1 --- /dev/null +++ b/src/map/navmesh/inavmesh.h @@ -0,0 +1,80 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include "common/mmo.h" + +#include +#include + +class INavMesh +{ +public: + virtual ~INavMesh() = default; + + virtual auto findPath(const position_t& start, const position_t& end) -> std::vector = 0; + virtual auto findRandomPosition(const position_t& start, float maxRadius) -> std::pair = 0; + virtual auto raycast(const position_t& start, const position_t& end) -> bool = 0; + virtual auto validPosition(const position_t& position) -> bool = 0; + virtual auto findClosestValidPoint(const position_t& position, float* validPoint) -> bool = 0; + virtual auto findFurthestValidPoint(const position_t& startPosition, const position_t& endPosition, float* validPoint) -> bool = 0; + virtual void snapToValidPosition(position_t& position) = 0; +}; + +class NullNavMesh final : public INavMesh +{ +public: + auto findPath(const position_t&, const position_t&) -> std::vector override + { + return {}; + } + + auto findRandomPosition(const position_t& start, float) -> std::pair override + { + return { 0, start }; + } + + auto raycast(const position_t&, const position_t&) -> bool override + { + return true; + } + + auto validPosition(const position_t&) -> bool override + { + return true; + } + + auto findClosestValidPoint(const position_t&, float*) -> bool override + { + return false; + } + + auto findFurthestValidPoint(const position_t&, const position_t&, float*) -> bool override + { + return false; + } + + void snapToValidPosition(position_t&) override + { + // NOOP + } +}; diff --git a/src/map/navmesh.cpp b/src/map/navmesh/navmesh.cpp similarity index 97% rename from src/map/navmesh.cpp rename to src/map/navmesh/navmesh.cpp index 751b4bf4a5f..3a5d24ff8e5 100644 --- a/src/map/navmesh.cpp +++ b/src/map/navmesh/navmesh.cpp @@ -325,12 +325,6 @@ auto CNavMesh::findPath(const position_t& start, const position_t& end) -> std:: { TracyZoneScoped; - if (!m_navMesh) - { - DebugNavmesh("CNavMesh::findPath No navmesh loaded (%u)", m_zoneID); - return {}; - } - if (std::isnan(start.x) || std::isnan(start.y) || std::isnan(start.z) || std::isnan(end.x) || std::isnan(end.y) || std::isnan(end.z)) { @@ -483,11 +477,6 @@ std::pair CNavMesh::findRandomPosition(const position_t& star { TracyZoneScoped; - if (!m_navMesh) - { - return {}; - } - DebugNavmesh("CNavMesh::findRandomPosition (%f, %f, %f) (%u)", start.x, start.y, start.z, m_zoneID); dtStatus status = 0; @@ -544,26 +533,10 @@ std::pair CNavMesh::findRandomPosition(const position_t& star return std::make_pair(0, position_t{ randomPt[0], randomPt[1], randomPt[2], 0, 0 }); } -bool CNavMesh::inWater(const position_t& point) -{ - if (!m_navMesh) - { - return false; - } - - // TODO: - return false; -} - bool CNavMesh::validPosition(const position_t& position) { TracyZoneScoped; - if (!m_navMesh) - { - return true; - } - DebugNavmesh("CNavMesh::validPosition (%f, %f, %f) (%u)", position.x, position.y, position.z, m_zoneID); float spos[3]; @@ -591,11 +564,6 @@ bool CNavMesh::findClosestValidPoint(const position_t& position, float* validPoi { TracyZoneScoped; - if (!m_navMesh) - { - return true; - } - DebugNavmesh("CNavMesh::findClosestValidPoint (%f, %f, %f) (%u)", position.x, position.y, position.z, m_zoneID); float spos[3]; @@ -622,11 +590,6 @@ bool CNavMesh::findFurthestValidPoint(const position_t& startPosition, const pos { TracyZoneScoped; - if (!m_navMesh) - { - return true; - } - DebugNavmesh("CNavMesh::findFurthestValidPoint (%f, %f, %f) -> (%f, %f, %f) (%u)", startPosition.x, startPosition.y, startPosition.z, endPosition.x, endPosition.y, endPosition.z, m_zoneID); float spos[3]; @@ -666,11 +629,6 @@ void CNavMesh::snapToValidPosition(position_t& position) { TracyZoneScoped; - if (!m_navMesh) - { - return; - } - DebugNavmesh("CNavMesh::snapToValidPosition (%f, %f, %f) (%u)", position.x, position.y, position.z, m_zoneID); float spos[3]; @@ -761,11 +719,6 @@ bool CNavMesh::onSameFloor(const position_t& start, float* spos, const position_ { TracyZoneScoped; - if (!m_navMesh) - { - return true; - } - DebugNavmesh("CNavMesh::onSameFloor (%f, %f, %f) -> (%f, %f, %f) (%u)", start.x, start.y, start.z, end.x, end.y, end.z, m_zoneID); float verticalDistance = abs(start.y - end.y); @@ -831,11 +784,6 @@ bool CNavMesh::raycast(const position_t& start, const position_t& end) return true; } - if (!m_navMesh) - { - return true; - } - DebugNavmesh("CNavMesh::raycast (%f, %f, %f) -> (%f, %f, %f) (%u)", start.x, start.y, start.z, end.x, end.y, end.z, m_zoneID); dtStatus status = 0; diff --git a/src/map/navmesh.h b/src/map/navmesh/navmesh.h similarity index 68% rename from src/map/navmesh.h rename to src/map/navmesh/navmesh.h index b1de6517d4a..a992c344f96 100644 --- a/src/map/navmesh.h +++ b/src/map/navmesh/navmesh.h @@ -1,4 +1,4 @@ -/* +/* =========================================================================== Copyright (c) 2010-2015 Darkstar Dev Teams @@ -25,13 +25,13 @@ #include #include "common/logging.h" -#include "common/mmo.h" +#include "inavmesh.h" #include #include #include -class CNavMesh +class CNavMesh final : public INavMesh { public: static void ToFFXIPos(const position_t* pos, float* out); @@ -42,33 +42,24 @@ class CNavMesh static void ToDetourPos(position_t* out); public: - CNavMesh(uint16 zoneID); - ~CNavMesh(); + explicit CNavMesh(uint16 zoneID); + ~CNavMesh() override; + + DISALLOW_COPY_AND_MOVE(CNavMesh); bool load(const std::string& path); bool installNavMesh(dtNavMesh* newNavMesh); bool save(const std::string& path) const; void unload(); - auto findPath(const position_t& start, const position_t& end) -> std::vector; - auto findRandomPosition(const position_t& start, float maxRadius) -> std::pair; - - // Returns true if the point is in water (not implemented) - bool inWater(const position_t& point); - - // Returns true if no wall was hit - // - // Recast Detour Docs: - // Casts a 'walkability' ray along the surface of the navigation mesh from the start position toward the end position. - // Note: This is not a point-to-point in 3D space calculation, it is 2D across the navmesh! - bool raycast(const position_t& start, const position_t& end); - - bool validPosition(const position_t& position); - bool findClosestValidPoint(const position_t& position, float* validPoint); - bool findFurthestValidPoint(const position_t& startPosition, const position_t& endPosition, float* validPoint); - - // Like validPosition(), but will also set the given position to the valid position that it finds. - void snapToValidPosition(position_t& position); + // INavMesh + auto findPath(const position_t& start, const position_t& end) -> std::vector override; + auto findRandomPosition(const position_t& start, float maxRadius) -> std::pair override; + auto raycast(const position_t& start, const position_t& end) -> bool override; + auto validPosition(const position_t& position) -> bool override; + auto findClosestValidPoint(const position_t& position, float* validPoint) -> bool override; + auto findFurthestValidPoint(const position_t& startPosition, const position_t& endPosition, float* validPoint) -> bool override; + void snapToValidPosition(position_t& position) override; [[nodiscard]] static auto detourStatusString(const uint32 status) -> std::string; diff --git a/src/map/navmesh_builder.cpp b/src/map/navmesh/navmesh_builder.cpp similarity index 96% rename from src/map/navmesh_builder.cpp rename to src/map/navmesh/navmesh_builder.cpp index d88b30f8527..70db7944a2f 100644 --- a/src/map/navmesh_builder.cpp +++ b/src/map/navmesh/navmesh_builder.cpp @@ -21,11 +21,11 @@ #include "navmesh_builder.h" -#include "zone_mesh.h" - #include "common/logging.h" #include "common/timer.h" +#include + #include #include #include @@ -61,15 +61,15 @@ auto transform(const std::array& rot, const std::array& tran } // namespace -NavMeshBuilder::NavMeshBuilder(const CZoneMesh& zoneMesh) -: zoneMesh_(&zoneMesh) -, gridWidth_(zoneMesh.gridWidth()) -, gridHeight_(zoneMesh.gridHeight()) +NavMeshBuilder::NavMeshBuilder(const IXiMesh& xiMesh) +: xiMesh_(&xiMesh) +, gridWidth_(xiMesh.gridWidth()) +, gridHeight_(xiMesh.gridHeight()) { - const auto& blocks = zoneMesh.blocks(); - const auto& placements = zoneMesh.placements(); - const auto& entries = zoneMesh.entries(); - const auto& cells = zoneMesh.cells(); + const auto& blocks = xiMesh.blocks(); + const auto& placements = xiMesh.placements(); + const auto& entries = xiMesh.entries(); + const auto& cells = xiMesh.cells(); const auto cellCount = static_cast(gridWidth_) * gridHeight_; for (uint32 cellIndex = 0; cellIndex < cellCount; ++cellIndex) @@ -141,9 +141,9 @@ void NavMeshBuilder::gatherTrianglesInAABB(const float* bmin, const float* bmax, out.indices.clear(); out.areas.clear(); - const auto& blocks = zoneMesh_->blocks(); - const auto& entries = zoneMesh_->entries(); - const auto& cells = zoneMesh_->cells(); + const auto& blocks = xiMesh_->blocks(); + const auto& entries = xiMesh_->entries(); + const auto& cells = xiMesh_->cells(); const auto [cxMin, czMin] = worldToCell(bmin[0], bmin[2]); const auto [cxMax, czMax] = worldToCell(bmax[0], bmax[2]); @@ -477,6 +477,12 @@ auto NavMeshBuilder::buildAsync(Scheduler& scheduler, const std::string& zoneNam float worldBmax[3]; getWorldBounds(worldBmin, worldBmax); + // No geometry was gathered (empty or null ximesh) - nothing to build. + if (worldBmin[0] > worldBmax[0]) + { + co_return nullptr; + } + const float detourBmin[3] = { worldBmin[0], -worldBmax[1], -worldBmax[2] }; const float detourBmax[3] = { worldBmax[0], -worldBmin[1], -worldBmin[2] }; diff --git a/src/map/navmesh_builder.h b/src/map/navmesh/navmesh_builder.h similarity index 87% rename from src/map/navmesh_builder.h rename to src/map/navmesh/navmesh_builder.h index 9469d303eb4..d3d33e122d5 100644 --- a/src/map/navmesh_builder.h +++ b/src/map/navmesh/navmesh_builder.h @@ -32,7 +32,7 @@ struct rcConfig; -class CZoneMesh; +class IXiMesh; class dtNavMesh; constexpr auto FloatMax = std::numeric_limits::max(); @@ -68,7 +68,7 @@ struct TileResult class NavMeshBuilder { public: - explicit NavMeshBuilder(const CZoneMesh& zoneMesh); + explicit NavMeshBuilder(const IXiMesh& xiMesh); void getWorldBounds(float* bmin, float* bmax) const; void gatherTrianglesInAABB(const float* bmin, const float* bmax, GatheredMesh& out) const; @@ -84,11 +84,11 @@ class NavMeshBuilder bool flipWinding{}; }; - const CZoneMesh* zoneMesh_{}; - uint16 gridWidth_{}; - uint16 gridHeight_{}; - float worldBmin_[3]{ FloatMax, FloatMax, FloatMax }; - float worldBmax_[3]{ FloatLowest, FloatLowest, FloatLowest }; + const IXiMesh* xiMesh_{}; + uint16 gridWidth_{}; + uint16 gridHeight_{}; + float worldBmin_[3]{ FloatMax, FloatMax, FloatMax }; + float worldBmax_[3]{ FloatLowest, FloatLowest, FloatLowest }; std::unordered_map preTransformed_; }; diff --git a/src/map/navmesh_config.h b/src/map/navmesh/navmesh_config.h similarity index 100% rename from src/map/navmesh_config.h rename to src/map/navmesh/navmesh_config.h diff --git a/src/map/packets/c2s/0x05e_maprect.cpp b/src/map/packets/c2s/0x05e_maprect.cpp index 527e3a1d921..2ae1257264e 100644 --- a/src/map/packets/c2s/0x05e_maprect.cpp +++ b/src/map/packets/c2s/0x05e_maprect.cpp @@ -26,7 +26,7 @@ #include "common/utils.h" #include "entities/charentity.h" #include "enums/msg_std.h" -#include "navmesh.h" +#include "map/navmesh/navmesh.h" #include "packets/s2c/0x053_systemmes.h" #include "packets/s2c/0x065_wpos2.h" #include "utils/charutils.h" @@ -247,9 +247,9 @@ void GP_CLI_COMMAND_MAPRECT::process(MapSession* PSession, CCharEntity* PChar) c PChar->loc.p = PZoneLine->nextSpawnPosition(); // Snap to navmesh for elevation on uneven zonelines - if (PDestination && PDestination->m_navMesh) + if (PDestination) { - PDestination->m_navMesh->snapToValidPosition(PChar->loc.p); + PDestination->navMesh()->snapToValidPosition(PChar->loc.p); } charutils::SavePrevZoneLineID(PChar, PZoneLine->zoneLineId); diff --git a/src/map/utils/battleutils.cpp b/src/map/utils/battleutils.cpp index 6e450e2c92b..2c8fba2ee58 100644 --- a/src/map/utils/battleutils.cpp +++ b/src/map/utils/battleutils.cpp @@ -57,12 +57,11 @@ #include "items.h" #include "items/item_weapon.h" #include "job_points.h" -#include "los/zone_los.h" +#include "map/navmesh/navmesh.h" #include "map_engine.h" #include "mob_modifier.h" #include "mobskill.h" #include "modifier.h" -#include "navmesh.h" #include "notoriety_container.h" #include "packets/pet_sync.h" #include "packets/s2c/0x029_battle_message.h" @@ -80,6 +79,8 @@ #include "weapon_skill.h" #include "zoneutils.h" +#include + /************************************************************************ * * * Lists used in battleutils * @@ -5262,24 +5263,19 @@ void DrawIn(CBattleEntity* PTarget, const position_t pos, const float offset, co return; } - // Make sure we can raycast to that position - // from the position's "eyeline" to the ground where we want to draw players in to - if (PTarget->loc.zone->lineOfSight) + // If geometry blocks the path from the source eyeline to the draw-in point, abort the + // draw-in - navmesh snapToValidPosition below will handle snapping to a valid position. + constexpr float ENTITY_HEIGHT = 2.0f; + + const auto src = Vector3{ pos.x, pos.y - ENTITY_HEIGHT, pos.z }; + const auto dst = Vector3{ nearEntity.x, nearEntity.y, nearEntity.z }; + if (PTarget->loc.zone->xiMesh()->rayIntersect(src, dst)) { - const auto entityHeight = 2.0f; - const auto posEyeline = position_t{ pos.x, pos.y - entityHeight, pos.z, 0, 0 }; - if (const auto optHit = PTarget->loc.zone->lineOfSight->Raycast(posEyeline, nearEntity)) - { - auto hit = *optHit; - nearEntity = { hit.x, hit.y, hit.z, 0, 0 }; - } + return; } // Snap nearEntity to a guaranteed valid position - if (PTarget->loc.zone->m_navMesh) - { - PTarget->loc.zone->m_navMesh->snapToValidPosition(nearEntity); - } + PTarget->loc.zone->navMesh()->snapToValidPosition(nearEntity); // Move the target a little higher, just in case nearEntity.y -= 1.0f; diff --git a/src/map/utils/zoneutils.cpp b/src/map/utils/zoneutils.cpp index 0a114afa1ce..81bc6e90ead 100644 --- a/src/map/utils/zoneutils.cpp +++ b/src/map/utils/zoneutils.cpp @@ -786,9 +786,9 @@ auto LoadZones(Scheduler& scheduler, MapConfig config, const std::vector g_PZoneList[0] = CreateZone(scheduler, config, 0); } - // Phase 1: Load zone meshes and LOS data (navmesh build depends on zone mesh) + // Phase 1: Load ximeshes (navmesh build depends on ximesh) co_await Scheduler::TaskGroup( - zonesIdsToLoad.size() * 2, + zonesIdsToLoad.size(), [&](auto& add) { for (const auto zoneId : zonesIdsToLoad) @@ -796,18 +796,12 @@ auto LoadZones(Scheduler& scheduler, MapConfig config, const std::vector add(scheduler.spawnOnWorkerThread( [zoneId]() { - g_PZoneList[zoneId]->LoadZoneMesh(); - })); - - add(scheduler.spawnOnWorkerThread( - [zoneId]() - { - g_PZoneList[zoneId]->LoadZoneLos(); + g_PZoneList[zoneId]->LoadXiMesh(); })); } }); - // Phase 2: Load/build navmeshes (requires zone mesh; processed serially because + // Phase 2: Load/build navmeshes (requires ximesh; processed serially because // each zone's build is a coroutine that dispatches tile work to workers) for (const auto zoneId : zonesIdsToLoad) { diff --git a/src/map/ximesh/iximesh.h b/src/map/ximesh/iximesh.h new file mode 100644 index 00000000000..3ccaca48419 --- /dev/null +++ b/src/map/ximesh/iximesh.h @@ -0,0 +1,110 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include "common/cbasetypes.h" +#include "map/ximesh/ximesh_structs.h" + +#include +#include + +class IXiMesh +{ +public: + virtual ~IXiMesh() = default; + + virtual auto query(float x, float y, float z) const -> std::optional = 0; + virtual auto getTerrainAt(float x, float y, float z) const -> TerrainType = 0; + virtual auto getFloorId(float x, float y, float z) const -> uint8 = 0; + virtual auto rayIntersect(const Vector3& start, const Vector3& end, bool transparentBarriers = false) const -> bool = 0; + virtual auto getPositionInfo(const Vector3& position, YOffsets yOffsets, bool transparentBarriers = false) const -> std::optional = 0; + + virtual auto blocks() const -> const std::vector& = 0; + virtual auto placements() const -> const std::vector& = 0; + virtual auto entries() const -> const std::vector& = 0; + virtual auto cells() const -> const std::vector& = 0; + virtual auto gridWidth() const -> uint16 = 0; + virtual auto gridHeight() const -> uint16 = 0; +}; + +class NullXiMesh final : public IXiMesh +{ +public: + auto query(float, float, float) const -> std::optional override + { + return std::nullopt; + } + + auto getTerrainAt(float, float, float) const -> TerrainType override + { + return TerrainType::None; + } + + auto getFloorId(float, float, float) const -> uint8 override + { + return 0; + } + + auto rayIntersect(const Vector3&, const Vector3&, bool) const -> bool override + { + return false; + } + + auto getPositionInfo(const Vector3&, YOffsets, bool) const -> std::optional override + { + return std::nullopt; + } + + auto blocks() const -> const std::vector& override + { + static const std::vector empty; + return empty; + } + + auto placements() const -> const std::vector& override + { + static const std::vector empty; + return empty; + } + + auto entries() const -> const std::vector& override + { + static const std::vector empty; + return empty; + } + + auto cells() const -> const std::vector& override + { + static const std::vector empty; + return empty; + } + + auto gridWidth() const -> uint16 override + { + return 0; + } + + auto gridHeight() const -> uint16 override + { + return 0; + } +}; diff --git a/src/map/ximesh/transformation_matrix.h b/src/map/ximesh/transformation_matrix.h new file mode 100644 index 00000000000..7122ee96c7a --- /dev/null +++ b/src/map/ximesh/transformation_matrix.h @@ -0,0 +1,108 @@ +/* +=========================================================================== + + Copyright (c) 2021-2025 Eden Dev Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + + This file is part of the Eden server source code. + +=========================================================================== +*/ + +#pragma once + +#include "vector3.h" + +// clang-format off +struct TransformationMatrix +{ + float elements[4][3]; + + constexpr float determinant() const + { + const auto el = this->elements; + + return el[0][0] * el[1][1] * el[2][2] + + el[0][1] * el[1][2] * el[2][0] + + el[0][2] * el[1][0] * el[2][1] + - el[0][0] * el[1][2] * el[2][1] + - el[0][1] * el[1][0] * el[2][2] + - el[0][2] * el[1][1] * el[2][0]; + } + + constexpr TransformationMatrix getInverted() const + { + const auto el = this->elements; + + TransformationMatrix inv; + auto el_inv = inv.elements; + + float t11 = -el[1][2] * el[2][1] + el[1][1] * el[2][2]; + float t12 = el[0][2] * el[2][1] - el[0][1] * el[2][2]; + float t13 = -el[0][2] * el[1][1] + el[0][1] * el[1][2]; + + float det = el[0][0] * t11 + el[1][0] * t12 + el[2][0] * t13; + + if (det == 0) + { + return inv; + } + + float inv_det = 1 / det; + + el_inv[0][0] = t11 * inv_det; + el_inv[1][0] = (el[1][2] * el[2][0] - el[1][0] * el[2][2]) * inv_det; + el_inv[2][0] = (-el[1][1] * el[2][0] + el[1][0] * el[2][1]) * inv_det; + el_inv[3][0] = (el[1][2] * el[2][1] * el[3][0] - el[1][1] * el[2][2] * el[3][0] - el[1][2] * el[2][0] * el[3][1] + el[1][0] * el[2][2] * el[3][1] + el[1][1] * el[2][0] * el[3][2] - el[1][0] * el[2][1] * el[3][2]) * inv_det; + + el_inv[0][1] = t12 * inv_det; + el_inv[1][1] = (-el[0][2] * el[2][0] + el[0][0] * el[2][2]) * inv_det; + el_inv[2][1] = (el[0][1] * el[2][0] - el[0][0] * el[2][1]) * inv_det; + el_inv[3][1] = (el[0][1] * el[2][2] * el[3][0] - el[0][2] * el[2][1] * el[3][0] + el[0][2] * el[2][0] * el[3][1] - el[0][0] * el[2][2] * el[3][1] - el[0][1] * el[2][0] * el[3][2] + el[0][0] * el[2][1] * el[3][2]) * inv_det; + + el_inv[0][2] = t13 * inv_det; + el_inv[1][2] = (el[0][2] * el[1][0] - el[0][0] * el[1][2]) * inv_det; + el_inv[2][2] = (-el[0][1] * el[1][0] + el[0][0] * el[1][1]) * inv_det; + el_inv[3][2] = (el[0][2] * el[1][1] * el[3][0] - el[0][1] * el[1][2] * el[3][0] - el[0][2] * el[1][0] * el[3][1] + el[0][0] * el[1][2] * el[3][1] + el[0][1] * el[1][0] * el[3][2] - el[0][0] * el[1][1] * el[3][2]) * inv_det; + + return inv; + } + + constexpr Vector3 applyToCopy(const Vector3& vec) const + { + Vector3 out = vec; + applyTo(out); + return out; + } + + constexpr void applyTo(Vector3& vec) const + { + const auto* el = this->elements; + + const Vector3 vecCopy = vec; + + vec.x = el[0][0] * vecCopy.x + el[1][0] * vecCopy.y + el[2][0] * vecCopy.z + el[3][0]; + vec.y = el[0][1] * vecCopy.x + el[1][1] * vecCopy.y + el[2][1] * vecCopy.z + el[3][1]; + vec.z = el[0][2] * vecCopy.x + el[1][2] * vecCopy.y + el[2][2] * vecCopy.z + el[3][2]; + } + + constexpr float applyGetY(const Vector3& vec) const + { + const auto* el = this->elements; + + return el[0][1] * vec.x + el[1][1] * vec.y + el[2][1] * vec.z + el[3][1]; + } +}; +// clang-format on diff --git a/src/map/ximesh/vector3.h b/src/map/ximesh/vector3.h new file mode 100644 index 00000000000..9aaf6b51bb8 --- /dev/null +++ b/src/map/ximesh/vector3.h @@ -0,0 +1,116 @@ +/* +=========================================================================== + + Copyright (c) 2021-2025 Eden Dev Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + + This file is part of the Eden server source code. + +=========================================================================== +*/ + +#pragma once + +#include + +struct Vector3 +{ + float x; + float y; + float z; + + Vector3 operator+(const Vector3& vec) const + { + return Vector3{ x + vec.x, y + vec.y, z + vec.z }; + } + + Vector3& operator+=(const Vector3& vec) + { + x += vec.x; + y += vec.y; + z += vec.z; + return *this; + } + + Vector3 operator-(const Vector3& vec) const + { + return Vector3{ x - vec.x, y - vec.y, z - vec.z }; + } + + Vector3& operator-=(const Vector3& vec) + { + x -= vec.x; + y -= vec.y; + z -= vec.z; + return *this; + } + + Vector3 operator*(float value) const + { + return Vector3{ x * value, y * value, z * value }; + } + + Vector3& operator*=(float value) + { + x *= value; + y *= value; + z *= value; + return *this; + } + + Vector3 operator/(float value) const + { + return Vector3{ x / value, y / value, z / value }; + } + + Vector3& operator/=(float value) + { + x /= value; + y /= value; + z /= value; + return *this; + } + + Vector3& operator=(const Vector3& vec) + { + x = vec.x; + y = vec.y; + z = vec.z; + return *this; + } + + Vector3 crossProduct(const Vector3& other) const + { + float ni = y * other.z - z * other.y; + float nj = z * other.x - x * other.z; + float nk = x * other.y - y * other.x; + return Vector3{ ni, nj, nk }; + } + + float dotProduct(const Vector3& other) const + { + return x * other.x + y * other.y + z * other.z; + } + + float magnitude() const + { + return std::sqrt(x * x + y * y + z * z); + } + + float magnitudeSquared() const + { + return x * x + y * y + z * z; + } +}; diff --git a/src/map/ximesh/ximesh.cpp b/src/map/ximesh/ximesh.cpp new file mode 100644 index 00000000000..8886f072627 --- /dev/null +++ b/src/map/ximesh/ximesh.cpp @@ -0,0 +1,936 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#include "ximesh.h" + +#include "common/logging.h" +#include "common/tracy.h" +#include "common/utils.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + +template +auto readAt(const std::span buf, const uint32 offset) -> T +{ + if (offset + sizeof(T) > buf.size()) + { + return T{}; + } + + T val{}; + std::memcpy(&val, buf.data() + offset, sizeof(T)); + return val; +} + +constexpr uint32 BYTES_PER_VERTEX = 3 * sizeof(float); // x, y, z +constexpr uint32 BYTES_PER_TRIANGLE = 3 * sizeof(uint16); // 3 indices +constexpr float CELL_SIZE = 4.0f; // world units per grid cell + +auto zlibDecompress(const std::vector& rawData) -> std::vector +{ + constexpr size_t CHUNK_SIZE = 32 * 1024; // 32 KB + constexpr size_t MAX_DECOMPRESSED_SIZE = 64 * 1024 * 1024; // 64 MB + + z_stream stream{}; + if (inflateInit(&stream) != Z_OK) + { + return {}; + } + + stream.next_in = (Bytef*)rawData.data(); + stream.avail_in = static_cast(rawData.size()); + + std::vector result; + result.reserve(rawData.size() * 4); + std::array chunk{}; + + int rc = Z_OK; + while (rc != Z_STREAM_END) + { + stream.next_out = chunk.data(); + stream.avail_out = static_cast(chunk.size()); + + rc = inflate(&stream, Z_NO_FLUSH); + if (rc != Z_OK && rc != Z_STREAM_END) + { + inflateEnd(&stream); + return {}; + } + + const size_t bytesWritten = chunk.size() - stream.avail_out; + result.insert(result.end(), chunk.data(), chunk.data() + bytesWritten); + + if (result.size() > MAX_DECOMPRESSED_SIZE) + { + inflateEnd(&stream); + return {}; + } + } + + inflateEnd(&stream); + return result; +} + +// Local to world space. +auto transform(const std::array& rot, const std::array& trans, const float* vertex) -> std::array +{ + return { + rot[0] * vertex[0] + rot[3] * vertex[1] + rot[6] * vertex[2] + trans[0], + rot[1] * vertex[0] + rot[4] * vertex[1] + rot[7] * vertex[2] + trans[1], + rot[2] * vertex[0] + rot[5] * vertex[1] + rot[8] * vertex[2] + trans[2], + }; +} + +auto buildO2W(const MeshPlacement& p) -> TransformationMatrix +{ + TransformationMatrix m{}; + m.elements[0][0] = p.rotation[0]; + m.elements[0][1] = p.rotation[1]; + m.elements[0][2] = p.rotation[2]; + m.elements[1][0] = p.rotation[3]; + m.elements[1][1] = p.rotation[4]; + m.elements[1][2] = p.rotation[5]; + m.elements[2][0] = p.rotation[6]; + m.elements[2][1] = p.rotation[7]; + m.elements[2][2] = p.rotation[8]; + m.elements[3][0] = p.translation[0]; + m.elements[3][1] = p.translation[1]; + m.elements[3][2] = p.translation[2]; + return m; +} + +auto vertexAt(const MeshBlock& block, const uint16 vertexIdx) -> Vector3 +{ + const float* v = &block.vertices[vertexIdx * 3]; + return Vector3{ v[0], v[1], v[2] }; +} + +// Möller–Trumbore ray/triangle intersection. +template +auto rayIntersectTriangle(const Vector3& v1, const Vector3& v2, const Vector3& v3, const Vector3& rayOrigin, const Vector3& rayVector) +{ + constexpr auto missReturn = []() + { + if constexpr (ReturnIntersectionPoint) + { + return std::optional(); + } + else + { + return false; + } + }(); + + constexpr float EPSILON = 0.0000001f; + + const auto edge1 = v2 - v1; + const auto edge2 = v3 - v1; + const auto h = rayVector.crossProduct(edge2); + const auto a = edge1.dotProduct(h); + if (a > -EPSILON && a < EPSILON) + { + return missReturn; + } + + const auto f = 1.0f / a; + const auto s = rayOrigin - v1; + const auto u = f * s.dotProduct(h); + if (u < 0.0f || u > 1.0f) + { + return missReturn; + } + + const auto q = s.crossProduct(edge1); + const auto v = f * rayVector.dotProduct(q); + if (v < 0.0f || u + v > 1.0f) + { + return missReturn; + } + + const auto t = f * edge2.dotProduct(q); + if (t > EPSILON && t <= 1.0f) + { + if constexpr (ReturnIntersectionPoint) + { + return std::make_optional(rayOrigin + rayVector * t); + } + else + { + return true; + } + } + return missReturn; +} + +} // namespace + +XiMesh::XiMesh(const std::string& filename) +{ + if (!load(filename)) + { + throw std::runtime_error(fmt::format("Failed to load {}", filename)); + } +} + +auto XiMesh::load(const std::string& filename) -> bool +{ + TracyZoneScoped; + + // Step 1. Read the file + std::ifstream file(filename, std::ios::binary | std::ios::ate); + if (!file.good()) + { + return false; + } + + const auto fileSize = static_cast(file.tellg()); + file.seekg(0, std::ios::beg); + + std::vector rawData(fileSize); + file.read(reinterpret_cast(rawData.data()), fileSize); + if (file.fail()) + { + ShowErrorFmt("XiMesh::load: Failed to read file ({})", filename); + return false; + } + + // Step 2. Decompress ximesh zlib + const auto decompressed = zlibDecompress(rawData); + if (decompressed.empty()) + { + ShowErrorFmt("XiMesh::load: zlib decompression failed ({})", filename); + return false; + } + + const std::span buf = decompressed; + + // Step 3. Load in the header containing the size of the grid and prepare final vector + if (buf.size() < sizeof(XimeshHeader)) + { + ShowErrorFmt("XiMesh::load: File too small ({})", filename); + return false; + } + + std::memcpy(&header_, buf.data(), sizeof(XimeshHeader)); + if (header_.gridWidth == 0 || header_.gridHeight == 0) + { + ShowErrorFmt("XiMesh::load: Invalid grid {}x{} ({})", header_.gridWidth, header_.gridHeight, filename); + return false; + } + + const uint32 cellCount = static_cast(header_.gridWidth) * header_.gridHeight; + cells_.resize(cellCount); + blocks_.reserve(header_.blockCount); + placements_.reserve(header_.placementCount); + + uint32 totalEntries = 0; + for (uint32 cellIndex = 0; cellIndex < cellCount; ++cellIndex) + { + const uint32 cellDataOffset = readAt(buf, sizeof(XimeshHeader) + cellIndex * 4); + if (cellDataOffset != 0 && cellDataOffset + sizeof(XimeshCellHeader) <= buf.size()) + { + totalEntries += readAt(buf, cellDataOffset).entryCount; + } + } + entries_.reserve(totalEntries); + + // Step 3a. Define block and placement parsers (deduplicated by file offset) + std::unordered_map blockCache; + std::unordered_map placeCache; + + auto getOrParseBlock = [&](const uint32 fileOffset) -> std::optional + { + auto [it, inserted] = blockCache.try_emplace(fileOffset, static_cast(blocks_.size())); + if (!inserted) + { + return it->second; + } + + MeshBlock block; + + const auto vertexCount = readAt(buf, fileOffset); + const auto triangleCount = readAt(buf, fileOffset + 2); + const auto barrierFlag = readAt(buf, fileOffset + 4); + block.hasBarriers = barrierFlag > 0; + + const uint32 vertexBytes = vertexCount * BYTES_PER_VERTEX; + const uint32 indexBytes = triangleCount * BYTES_PER_TRIANGLE; + const uint32 metaBytes = triangleCount; // 1 byte per triangle + + const uint32 vertexOffset = fileOffset + 8; + const uint32 indexOffset = roundUpToNearestFour(vertexOffset + vertexBytes); + const uint32 metaOffset = roundUpToNearestFour(indexOffset + indexBytes); + + if (vertexOffset + vertexBytes > buf.size() || + indexOffset + indexBytes > buf.size() || + metaOffset + metaBytes > buf.size()) + { + ShowErrorFmt("XiMesh: Block OOB at offset 0x{:X} (bufSize={})", fileOffset, buf.size()); + return std::nullopt; + } + + block.vertices.resize(vertexCount * 3); + std::memcpy(block.vertices.data(), buf.data() + vertexOffset, vertexBytes); + + block.indices.resize(triangleCount * 3); + std::memcpy(block.indices.data(), buf.data() + indexOffset, indexBytes); + + block.metas.resize(metaBytes); + std::memcpy(block.metas.data(), buf.data() + metaOffset, metaBytes); + + blocks_.emplace_back(std::move(block)); + return it->second; + }; + + auto getOrParsePlacement = [&](const uint32 fileOffset) -> std::optional + { + auto [it, inserted] = placeCache.try_emplace(fileOffset, static_cast(placements_.size())); + if (!inserted) + { + return it->second; + } + + const auto flags = readAt(buf, fileOffset); + MeshPlacement placement{ + .mapId = static_cast(flags.mapIdHigh << 3 | flags.mapIdLow), + .roofed = flags.roofed != 0, + }; + + constexpr size_t TRANSFORM_BYTES = sizeof(placement.rotation) + sizeof(placement.translation); + if (fileOffset + 4 + TRANSFORM_BYTES > buf.size()) + { + ShowErrorFmt("XiMesh: Placement OOB at offset 0x{:X} (bufSize={})", fileOffset, buf.size()); + return std::nullopt; + } + + std::memcpy(placement.rotation.data(), buf.data() + fileOffset + 4, TRANSFORM_BYTES); + + placements_.emplace_back(placement); + return it->second; + }; + + // Step 3b. Parse cells + for (uint32 cellIndex = 0; cellIndex < cellCount; ++cellIndex) + { + const uint32 cellDataOffset = readAt(buf, sizeof(XimeshHeader) + cellIndex * 4); + if (cellDataOffset == 0 || cellDataOffset + sizeof(XimeshCellHeader) > buf.size()) + { + continue; + } + + const auto cellHeader = readAt(buf, cellDataOffset); + auto& cell = cells_[cellIndex]; + cell.offset = static_cast(entries_.size()); + cell.count = 0; + + for (uint16 entryIndex = 0; entryIndex < cellHeader.entryCount; ++entryIndex) + { + const uint32 entryOffset = cellDataOffset + sizeof(XimeshCellHeader) + entryIndex * sizeof(XimeshCellEntry); + if (entryOffset + sizeof(XimeshCellEntry) > buf.size()) + { + break; + } + + const auto rawEntry = readAt(buf, entryOffset); + const auto blockIdx = getOrParseBlock(rawEntry.blockOffset); + const auto placementIdx = getOrParsePlacement(rawEntry.placementOffset); + if (!blockIdx || !placementIdx) + { + ShowErrorFmt("XiMesh::load: Corrupt block/placement data ({})", filename); + return false; + } + + entries_.push_back({ *blockIdx, *placementIdx }); + cell.count++; + } + } + + // Step 4. Pre-compute Y bounds per placement (used by query culling). + for (uint32 cellIndex = 0; cellIndex < cellCount; ++cellIndex) + { + const auto& cell = cells_[cellIndex]; + for (uint16 ref = 0; ref < cell.count; ++ref) + { + const auto& [blockIdx, placementIdx] = entries_[cell.offset + ref]; + const auto& block = blocks_[blockIdx]; + auto& place = placements_[placementIdx]; + + for (size_t v = 0; v < block.vertices.size(); v += 3) + { + const auto world = transform(place.rotation, place.translation, &block.vertices[v]); + + place.yMin = std::min(place.yMin, world[1]); + place.yMax = std::max(place.yMax, world[1]); + } + } + } + + // Step 5. Pre-compute world->object matrices, winding flips, and per-cell Y ranges (used by ray queries). + w2os_.resize(placements_.size()); + placementFlips_.resize(placements_.size()); + for (size_t i = 0; i < placements_.size(); ++i) + { + const auto o2w = buildO2W(placements_[i]); + w2os_[i] = o2w.getInverted(); + placementFlips_[i] = o2w.determinant() > 0.0f; + } + + cellRanges_.assign(cellCount, YRange{}); + for (uint32 cellIndex = 0; cellIndex < cellCount; ++cellIndex) + { + auto& range = cellRanges_[cellIndex]; + const auto& cell = cells_[cellIndex]; + + for (uint16 ref = 0; ref < cell.count; ++ref) + { + const auto& [blockIdx, placementIdx] = entries_[cell.offset + ref]; + const auto& place = placements_[placementIdx]; + range.min = std::min(range.min, place.yMin); + range.max = std::max(range.max, place.yMax); + } + } + + return true; +} + +// World position to cell grid index. Each cell covers 4x4 world units. +auto XiMesh::worldToCell(const float x, const float z) const -> std::pair +{ + return { + static_cast(std::floor(x / CELL_SIZE)) + header_.gridWidth / 2, + static_cast(std::floor(z / CELL_SIZE)) + header_.gridHeight / 2, + }; +} + +// Returns the triangle under (x, z) closest above y. +auto XiMesh::query(const float x, const float y, const float z) const -> std::optional +{ + TracyZoneScoped; + + const auto [cx, cz] = worldToCell(x, z); + const std::array point = { x, 0.0f, z }; + + auto searchCell = [&](const int cellX, const int cellZ) -> std::optional + { + if (cellX < 0 || cellX >= header_.gridWidth || cellZ < 0 || cellZ >= header_.gridHeight) + { + return std::nullopt; + } + + const auto& cell = cells_[static_cast(cellZ) * header_.gridWidth + cellX]; + if (cell.count == 0) + { + return std::nullopt; + } + + std::optional best; + for (uint16 ref = 0; ref < cell.count; ++ref) + { + constexpr float EPSILON = 0.01f; + const auto& [blockIdx, placementIdx] = entries_[cell.offset + ref]; + const auto& block = blocks_[blockIdx]; + const auto& place = placements_[placementIdx]; + + // Skip placements entirely above query point + if (place.yMax < y - EPSILON) + { + continue; + } + + for (size_t triIdx = 0; triIdx < block.metas.size(); ++triIdx) + { + const uint16 i0 = block.indices[triIdx * 3 + 0]; + const uint16 i1 = block.indices[triIdx * 3 + 1]; + const uint16 i2 = block.indices[triIdx * 3 + 2]; + + const auto world0 = transform(place.rotation, place.translation, &block.vertices[i0 * 3]); + const auto world1 = transform(place.rotation, place.translation, &block.vertices[i1 * 3]); + const auto world2 = transform(place.rotation, place.translation, &block.vertices[i2 * 3]); + + float triY = 0.0f; + if (!dtClosestHeightPointTriangle(point.data(), world0.data(), world1.data(), world2.data(), triY)) + { + continue; + } + + // Pick the closest triangle above the query point (Y is negative-up). + if (triY >= y - EPSILON && (!best || triY < best->y)) + { + const auto& meta = block.metas[triIdx]; + best = CellHit{ + .type = static_cast(meta.material), + .mapId = place.mapId, + .roofed = place.roofed, + .barrier = meta.barrier != 0, + .y = triY, + }; + } + } + } + + return best; + }; + + // Check target cell first + if (const auto best = searchCell(cx, cz)) + { + return best; + } + + // Miss, check neighbors + std::optional best; + for (int dz = -1; dz <= 1; ++dz) + { + for (int dx = -1; dx <= 1; ++dx) + { + if (dx == 0 && dz == 0) + { + continue; + } + + if (const auto hit = searchCell(cx + dx, cz + dz)) + { + if (!best || hit->y < best->y) + { + best = hit; + } + } + } + } + + return best; +} + +auto XiMesh::getTerrainAt(const float x, const float y, const float z) const -> TerrainType +{ + TracyZoneScoped; + + if (const auto hit = query(x, y, z)) + { + return hit->type; + } + + return TerrainType::None; +} + +auto XiMesh::getFloorId(const float x, const float y, const float z) const -> uint8 +{ + TracyZoneScoped; + + if (const auto hit = query(x, y, z)) + { + return hit->mapId; + } + + return 0; +} + +auto XiMesh::blocks() const -> const std::vector& +{ + return blocks_; +} + +auto XiMesh::placements() const -> const std::vector& +{ + return placements_; +} + +auto XiMesh::entries() const -> const std::vector& +{ + return entries_; +} + +auto XiMesh::cells() const -> const std::vector& +{ + return cells_; +} + +auto XiMesh::gridWidth() const -> uint16 +{ + return header_.gridWidth; +} + +auto XiMesh::gridHeight() const -> uint16 +{ + return header_.gridHeight; +} + +auto XiMesh::rayIntersect(const Vector3& start, const Vector3& end, const bool transparentBarriers) const -> bool +{ + TracyZoneScoped; + + const auto cellSearchDiff = header_.wideSearch > 0 ? 2 : 1; + + auto [col, row] = worldToCell(start.x, start.z); + + const auto yRange = YRange{ std::min(start.y, end.y), std::max(start.y, end.y) }; + + // Dispatch once on transparentBarriers so the inner cell loop has no branch. + const auto checkCell = [&](const uint32 cellIdx) -> bool + { + if (transparentBarriers) + return rayIntersectCell(start, end, yRange, cellIdx); + return rayIntersectCell(start, end, yRange, cellIdx); + }; + + auto row1 = std::max(0, row - cellSearchDiff); + auto row2 = std::min(header_.gridHeight - 1, row + cellSearchDiff); + auto col1 = std::max(0, col - cellSearchDiff); + auto col2 = std::min(header_.gridWidth - 1, col + cellSearchDiff); + + for (int32 r = row1; r <= row2; ++r) + { + const auto rGrid = static_cast(r) * header_.gridWidth; + for (int32 c = col1; c <= col2; ++c) + { + if (checkCell(rGrid + static_cast(c))) + { + return true; + } + } + } + + auto [endCol, endRow] = worldToCell(end.x, end.z); + + const auto dx = end.x - start.x; + const auto dz = end.z - start.z; + + if (std::abs(dx) < 1e-6f && std::abs(dz) < 1e-6f) + { + return false; + } + + const auto stepCol = dx > 0 ? 1 : -1; + const auto stepRow = dz > 0 ? 1 : -1; + const auto deltaX = std::abs(dx) < 1e-6f ? std::numeric_limits::infinity() : std::abs(CELL_SIZE / dx); + const auto deltaZ = std::abs(dz) < 1e-6f ? std::numeric_limits::infinity() : std::abs(CELL_SIZE / dz); + + float nextX{}; + float nextZ{}; + + if (dx > 0) + { + const auto rightEdge = (col + 1) * CELL_SIZE - header_.gridWidth * (CELL_SIZE / 2.0f); + nextX = (rightEdge - start.x) / dx; + } + else if (dx < 0) + { + const auto leftEdge = col * CELL_SIZE - header_.gridWidth * (CELL_SIZE / 2.0f); + nextX = (leftEdge - start.x) / dx; + } + else + { + nextX = std::numeric_limits::infinity(); + } + + if (dz > 0) + { + const auto bottomEdge = (row + 1) * CELL_SIZE - header_.gridHeight * (CELL_SIZE / 2.0f); + nextZ = (bottomEdge - start.z) / dz; + } + else if (dz < 0) + { + const auto topEdge = row * CELL_SIZE - header_.gridHeight * (CELL_SIZE / 2.0f); + nextZ = (topEdge - start.z) / dz; + } + else + { + nextZ = std::numeric_limits::infinity(); + } + + const auto rayLength = std::sqrt(dx * dx + dz * dz); + float currentDistance = 0.0f; + + while ((col != endCol || row != endRow) && currentDistance < rayLength) + { + // Move to whichever grid line is closer. + if (nextX < nextZ) + { + currentDistance = nextX; + nextX += deltaX; + col += stepCol; + + row1 = std::max(0, row - cellSearchDiff); + row2 = std::min(header_.gridHeight - 1, row + cellSearchDiff); + + col1 = std::clamp(col + stepCol, 0, header_.gridWidth - 1); + col2 = col1; + } + else + { + currentDistance = nextZ; + nextZ += deltaZ; + row += stepRow; + + row1 = std::clamp(row + stepRow, 0, header_.gridHeight - 1); + row2 = row1; + + col1 = std::max(0, col - cellSearchDiff); + col2 = std::min(header_.gridWidth - 1, col + cellSearchDiff); + } + + if (currentDistance <= rayLength) + { + for (int32 r = row1; r <= row2; ++r) + { + const auto rGrid = static_cast(r) * header_.gridWidth; + for (int32 c = col1; c <= col2; ++c) + { + if (transparentBarriers) + { + if (rayIntersectCell(start, end, yRange, rGrid + static_cast(c))) + { + return true; + } + } + else + { + if (rayIntersectCell(start, end, yRange, rGrid + static_cast(c))) + { + return true; + } + } + } + } + } + } + + return false; +} + +auto XiMesh::getPositionInfo(const Vector3& position, const YOffsets yOffsets, const bool transparentBarriers) const -> std::optional +{ + TracyZoneScoped; + + const auto cellSearchDiff = header_.wideSearch > 0 ? 2 : 1; + + const auto start = Vector3{ position.x, position.y + yOffsets.start, position.z }; + const auto end = Vector3{ position.x, position.y + yOffsets.start + yOffsets.end, position.z }; + + auto [col, row] = worldToCell(start.x, start.z); + + const auto yRange = YRange{ std::min(start.y, end.y), std::max(start.y, end.y) }; + + const auto row1 = std::max(0, row - cellSearchDiff); + const auto row2 = std::min(header_.gridHeight - 1, row + cellSearchDiff); + const auto col1 = std::max(0, col - cellSearchDiff); + const auto col2 = std::min(header_.gridWidth - 1, col + cellSearchDiff); + + std::optional closestHit; + + for (int32 r = row1; r <= row2; ++r) + { + const auto rGrid = static_cast(r) * header_.gridWidth; + for (int32 c = col1; c <= col2; ++c) + { + if (transparentBarriers) + { + rayIntersectCellHitInfo(start, end, yRange, rGrid + static_cast(c), closestHit); + } + else + { + rayIntersectCellHitInfo(start, end, yRange, rGrid + static_cast(c), closestHit); + } + } + } + + return closestHit; +} + +template +auto XiMesh::rayIntersectCell(const Vector3& start, const Vector3& end, const YRange yRange, const uint32 cellIdx) const -> bool +{ + if (cellIdx >= cells_.size()) + { + return false; + } + + const auto& cell = cells_[cellIdx]; + if (cell.count == 0) + { + return false; + } + + const auto& cRange = cellRanges_[cellIdx]; + if (yRange.min > cRange.max || yRange.max < cRange.min) + { + return false; + } + + const auto worldDiff = end - start; + + uint16 lastPlacementIdx = UINT16_MAX; + Vector3 oStart{}; + Vector3 oDiff{}; + uint32 v1Off = 0; + uint32 v3Off = 2; + + for (uint16 ref = 0; ref < cell.count; ++ref) + { + const auto& [blockIdx, placementIdx] = entries_[cell.offset + ref]; + const auto& block = blocks_[blockIdx]; + + if constexpr (TransparentBarriers) + { + if (block.hasBarriers) + { + continue; + } + } + + const auto& place = placements_[placementIdx]; + if (yRange.min > place.yMax || yRange.max < place.yMin) + { + continue; + } + + if (placementIdx != lastPlacementIdx) + { + lastPlacementIdx = placementIdx; + const auto& w2o = w2os_[placementIdx]; + oStart = w2o.applyToCopy(start); + const auto& el = w2o.elements; + oDiff = Vector3{ + el[0][0] * worldDiff.x + el[1][0] * worldDiff.y + el[2][0] * worldDiff.z, + el[0][1] * worldDiff.x + el[1][1] * worldDiff.y + el[2][1] * worldDiff.z, + el[0][2] * worldDiff.x + el[1][2] * worldDiff.y + el[2][2] * worldDiff.z, + }; + const auto flip = placementFlips_[placementIdx]; + v1Off = flip ? 2u : 0u; + v3Off = flip ? 0u : 2u; + } + + const auto triCount = block.metas.size(); + for (size_t triIdx = 0; triIdx < triCount; ++triIdx) + { + const auto base = triIdx * 3; + const auto va = vertexAt(block, block.indices[base + v1Off]); + const auto vb = vertexAt(block, block.indices[base + 1]); + const auto vc = vertexAt(block, block.indices[base + v3Off]); + + if (rayIntersectTriangle(va, vb, vc, oStart, oDiff)) + { + return true; + } + } + } + + return false; +} + +template +auto XiMesh::rayIntersectCellHitInfo(const Vector3& start, const Vector3& end, const YRange yRange, const uint32 cellIdx, std::optional& closestHit) const -> void +{ + if (cellIdx >= cells_.size()) + { + return; + } + + const auto& cell = cells_[cellIdx]; + if (cell.count == 0) + { + return; + } + + const auto& cRange = cellRanges_[cellIdx]; + if (yRange.min > cRange.max || yRange.max < cRange.min) + { + return; + } + + const auto worldDiff = end - start; + + uint16 lastPlacementIdx = UINT16_MAX; + Vector3 oStart{}; + Vector3 oDiff{}; + uint32 v1Off = 0; + uint32 v3Off = 2; + + for (uint16 ref = 0; ref < cell.count; ++ref) + { + const auto& [blockIdx, placementIdx] = entries_[cell.offset + ref]; + const auto& block = blocks_[blockIdx]; + + if constexpr (TransparentBarriers) + { + if (block.hasBarriers) + { + continue; + } + } + + const auto& place = placements_[placementIdx]; + if (yRange.min > place.yMax || yRange.max < place.yMin) + { + continue; + } + + if (placementIdx != lastPlacementIdx) + { + lastPlacementIdx = placementIdx; + const auto& w2o = w2os_[placementIdx]; + oStart = w2o.applyToCopy(start); + const auto& el = w2o.elements; + oDiff = Vector3{ + el[0][0] * worldDiff.x + el[1][0] * worldDiff.y + el[2][0] * worldDiff.z, + el[0][1] * worldDiff.x + el[1][1] * worldDiff.y + el[2][1] * worldDiff.z, + el[0][2] * worldDiff.x + el[1][2] * worldDiff.y + el[2][2] * worldDiff.z, + }; + const auto flip = placementFlips_[placementIdx]; + v1Off = flip ? 2u : 0u; + v3Off = flip ? 0u : 2u; + } + + const auto triCount = block.metas.size(); + for (size_t triIdx = 0; triIdx < triCount; ++triIdx) + { + const auto base = triIdx * 3; + const auto va = vertexAt(block, block.indices[base + v1Off]); + const auto vb = vertexAt(block, block.indices[base + 1]); + const auto vc = vertexAt(block, block.indices[base + v3Off]); + const auto hitOpt = rayIntersectTriangle(va, vb, vc, oStart, oDiff); + if (hitOpt.has_value()) + { + const auto intersection = hitOpt.value(); + const auto distanceSq = (oStart - intersection).magnitudeSquared(); + if (!closestHit.has_value() || closestHit->distanceSq > distanceSq) + { + const auto& meta = block.metas[triIdx]; + closestHit = RayHitInfo{ + .intersection = intersection, + .distanceSq = distanceSq, + .placement = &place, + .type = static_cast(meta.material), + .barrier = meta.barrier != 0, + }; + } + } + } + } +} diff --git a/src/map/ximesh/ximesh.h b/src/map/ximesh/ximesh.h new file mode 100644 index 00000000000..b92c09da4ad --- /dev/null +++ b/src/map/ximesh/ximesh.h @@ -0,0 +1,72 @@ +/* +=========================================================================== + + Copyright (c) 2026 LandSandBoat Dev Teams + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see http://www.gnu.org/licenses/ + +=========================================================================== +*/ + +#pragma once + +#include "map/ximesh/iximesh.h" +#include "map/ximesh/transformation_matrix.h" + +#include + +class XiMesh final : public IXiMesh +{ +public: + explicit XiMesh(const std::string& filename); + ~XiMesh() override = default; + + DISALLOW_COPY_AND_MOVE(XiMesh); + + auto query(float x, float y, float z) const -> std::optional override; + auto getTerrainAt(float x, float y, float z) const -> TerrainType override; + auto getFloorId(float x, float y, float z) const -> uint8 override; + + auto rayIntersect(const Vector3& start, const Vector3& end, bool transparentBarriers = false) const -> bool override; + auto getPositionInfo(const Vector3& position, YOffsets yOffsets, bool transparentBarriers = false) const -> std::optional override; + + auto blocks() const -> const std::vector& override; + auto placements() const -> const std::vector& override; + auto entries() const -> const std::vector& override; + auto cells() const -> const std::vector& override; + auto gridWidth() const -> uint16 override; + auto gridHeight() const -> uint16 override; + +private: + auto load(const std::string& filename) -> bool; + + auto worldToCell(float x, float z) const -> std::pair; + + template + auto rayIntersectCell(const Vector3& start, const Vector3& end, YRange yRange, uint32 cellIdx) const -> bool; + + template + auto rayIntersectCellHitInfo(const Vector3& start, const Vector3& end, YRange yRange, uint32 cellIdx, std::optional& closestHit) const -> void; + + XimeshHeader header_{}; + + std::vector blocks_; + std::vector placements_; + std::vector entries_; + std::vector cells_; + + std::vector w2os_; + std::vector placementFlips_; + std::vector cellRanges_; +}; diff --git a/src/map/ximesh.h b/src/map/ximesh/ximesh_structs.h similarity index 55% rename from src/map/ximesh.h rename to src/map/ximesh/ximesh_structs.h index d412923f8c1..6743bc36199 100644 --- a/src/map/ximesh.h +++ b/src/map/ximesh/ximesh_structs.h @@ -22,8 +22,16 @@ #pragma once #include "common/cbasetypes.h" +#include "map/enums/terrain_type.h" +#include "map/ximesh/vector3.h" -// Ximesh binary format -- zlib-compressed zone collision geometry. +#include +#include +#include +#include + +// +// Binary format structs - packed representations of the on-disk ximesh layout. // Reference: https://github.com/InoUno/xi-visualizer/blob/main/src/graphics/ximesh.ts // // After decompression: @@ -44,6 +52,7 @@ // Placement (at placementOffset): // [PlacementFlags] u32 bitfield // [float x 12] 3x3 rotation (col-major) + vec3 translation +// #pragma pack(push, 1) struct XimeshHeader @@ -86,3 +95,70 @@ struct PlacementFlags uint32 padding2 : 4; }; #pragma pack(pop) + +// +// Intermediate structs - parsed, in-memory representations built from the +// binary format above. +// + +struct MeshBlock +{ + std::vector vertices; // x,y,z interleaved (vertexCount * 3) + std::vector indices; // 3 per triangle + std::vector metas; // 1 per triangle + bool hasBarriers{}; // true if any triangle in this block is a barrier +}; + +struct MeshPlacement +{ + std::array rotation{}; // 3x3 column-major + std::array translation{}; + uint8 mapId{}; + bool roofed{}; + float yMin{ std::numeric_limits::max() }; + float yMax{ std::numeric_limits::lowest() }; +}; + +struct CellEntry +{ + uint16 blockIdx; + uint16 placementIdx; +}; + +struct CellSpan +{ + uint32 offset : 24; + uint32 count : 8; +}; + +// Result of a vertical downcast (query). Answers "what floor is under this XZ position?" +struct CellHit +{ + TerrainType type{ TerrainType::None }; + uint8 mapId{}; + bool roofed{}; + bool barrier{}; + float y{}; // world-space floor height at the query point +}; + +struct YOffsets +{ + float start; + float end; +}; + +struct YRange +{ + float min{ std::numeric_limits::max() }; + float max{ std::numeric_limits::lowest() }; +}; + +// Result of a directional ray cast (rayIntersect / getPositionInfo). Answers "where did this ray hit geometry?" +struct RayHitInfo +{ + Vector3 intersection{}; // 3D hit point in object space + float distanceSq{}; + const MeshPlacement* placement{}; + TerrainType type{ TerrainType::None }; + bool barrier{}; +}; diff --git a/src/map/zone.cpp b/src/map/zone.cpp index 396a50f0382..118f2bce5cc 100644 --- a/src/map/zone.cpp +++ b/src/map/zone.cpp @@ -40,23 +40,22 @@ constexpr std::uint16_t WeatherCycle = 2160; #include "common/vana_time.h" #include +#include #include "battlefield.h" #include "enums/loot_recast.h" #include "ipc_client.h" #include "latent_effect_container.h" -#include "los/zone_los.h" +#include "map/navmesh/navmesh.h" +#include "map/navmesh/navmesh_builder.h" #include "map_engine.h" #include "monstrosity.h" -#include "navmesh.h" -#include "navmesh_builder.h" #include "party.h" #include "recast_container.h" #include "spawn_handler.h" #include "status_effect_container.h" #include "treasure_pool.h" #include "zone_entities.h" -#include "zone_mesh.h" #include "entities/npcentity.h" #include "entities/petentity.h" @@ -67,9 +66,13 @@ constexpr std::uint16_t WeatherCycle = 2160; #include "utils/charutils.h" #include "utils/moduleutils.h" +#include + CZone::CZone(Scheduler& scheduler, MapConfig config, ZONEID ZoneID, REGION_TYPE RegionID, CONTINENT_TYPE ContinentID, uint8 levelRestriction) : scheduler_(scheduler) , config_(config) +, navMesh_{ std::make_unique() } +, xiMesh_{ std::make_unique() } , m_zoneID(ZoneID) , m_zoneType(ZONE_TYPE::UNKNOWN) , m_regionID(RegionID) @@ -79,9 +82,6 @@ CZone::CZone(Scheduler& scheduler, MapConfig config, ZONEID ZoneID, REGION_TYPE { TracyZoneScoped; - m_useNavMesh = false; - std::ignore = m_useNavMesh; - m_TreasurePool = nullptr; m_BattlefieldHandler = nullptr; m_Weather = Weather::None; @@ -467,78 +467,63 @@ void CZone::LoadZoneSettings() auto CZone::LoadNavMesh() -> Task { - if (m_navMesh == nullptr) - { - m_navMesh = std::make_unique(static_cast(GetID())); - } - - const auto file = fmt::format("navmeshes/{}.nav", getName()); + auto navMesh = std::make_unique(static_cast(GetID())); + const auto file = fmt::format("navmeshes/{}.nav", getName()); - if (!config_.rebuildNavmeshes && m_navMesh->load(file)) + if (!config_.rebuildNavmeshes && navMesh->load(file)) { + navMesh_ = std::move(navMesh); co_return; } - if (zoneMesh_ && zoneMesh_->isLoaded()) - { - NavMeshBuilder builder(*zoneMesh_); + NavMeshBuilder builder(*xiMesh_); - auto* navMesh = co_await builder.buildAsync(scheduler_, getName(), static_cast(GetID()), NavMeshConfig{}); - if (navMesh && m_navMesh->installNavMesh(navMesh)) - { - m_navMesh->save(file); - co_return; - } + auto* dtNavMesh = co_await builder.buildAsync(scheduler_, getName(), static_cast(GetID()), NavMeshConfig{}); + if (dtNavMesh && navMesh->installNavMesh(dtNavMesh)) + { + navMesh->save(file); + navMesh_ = std::move(navMesh); + co_return; } - DebugNavmesh("CZone::LoadNavMesh: No navmesh available for zone (%s)", getName().c_str()); - m_navMesh = nullptr; + DebugNavmesh("CZone::LoadNavMesh: Build failed for zone (%s)", getName().c_str()); } void CZone::RebuildNavMesh(const NavMeshConfig& config) { - if (!zoneMesh_ || !zoneMesh_->isLoaded()) - { - ShowErrorFmt("CZone::RebuildNavMesh: No zone mesh loaded for ({})", getName()); - return; - } - - const auto zoneName = getName(); - const auto zoneID = static_cast(GetID()); - const auto* zoneMeshPtr = zoneMesh_.get(); + const auto zoneName = getName(); + const auto zoneID = static_cast(GetID()); + const auto* xiMeshPtr = xiMesh_.get(); scheduler_.postToMainThread( - [this, zoneName, zoneID, config, zoneMeshPtr]() -> Task + [this, zoneName, zoneID, config, xiMeshPtr]() -> Task { - NavMeshBuilder builder(*zoneMeshPtr); + NavMeshBuilder builder(*xiMeshPtr); - auto* newNavMesh = co_await builder.buildAsync(scheduler_, zoneName, zoneID, config); - if (m_navMesh && m_navMesh->installNavMesh(newNavMesh)) + auto* dtNavMesh = co_await builder.buildAsync(scheduler_, zoneName, zoneID, config); + auto navMesh = std::make_unique(zoneID); + if (dtNavMesh && navMesh->installNavMesh(dtNavMesh)) { - m_navMesh->save(fmt::format("navmeshes/{}.nav", zoneName)); + navMesh->save(fmt::format("navmeshes/{}.nav", zoneName)); + navMesh_ = std::move(navMesh); } }); } -auto CZone::zoneMesh() const -> Maybe +auto CZone::navMesh() const -> INavMesh* { - if (zoneMesh_ && zoneMesh_->isLoaded()) - { - return zoneMesh_.get(); - } + return navMesh_.get(); +} - return std::nullopt; +auto CZone::xiMesh() const -> IXiMesh* +{ + return xiMesh_.get(); } -void CZone::LoadZoneMesh() +void CZone::LoadXiMesh() { TracyZoneScoped; - if (zoneMesh_ == nullptr) - { - zoneMesh_ = std::make_unique(); - } - // TODO: Align ximesh filenames with zone_settings names so this isn't needed. auto meshName = std::string(getName()); @@ -581,30 +566,17 @@ void CZone::LoadZoneMesh() } const auto file = fmt::format("ximeshes/{}.ximesh", meshName); - if (!zoneMesh_->load(file)) - { - DebugNavmesh("CZone::LoadZoneMesh: Cannot load zone mesh (%s)", file.c_str()); - zoneMesh_ = nullptr; - } -} - -void CZone::LoadZoneLos() -{ - TracyZoneScoped; - - if (GetTypeMask() & ZONE_TYPE::CITY || (m_miscMask & MISC_LOS_OFF)) + if (std::filesystem::exists(file)) { - // Skip cities and zones with line of sight turned off - return; - } - - if (lineOfSight) - { - // Clean up previous object if one exists. - lineOfSight = nullptr; + try + { + xiMesh_ = std::make_unique(file); + } + catch (const std::exception& e) + { + ShowErrorFmt("CZone::LoadXiMesh: Failed to load '{}': {}", file, e.what()); + } } - - lineOfSight = ZoneLos::Load((uint16)GetID(), fmt::sprintf("losmeshes/%s.obj", getName())); } void CZone::InsertMOB(CBaseEntity* PMob) diff --git a/src/map/zone.h b/src/map/zone.h index b8a4c2840a1..98e99296549 100644 --- a/src/map/zone.h +++ b/src/map/zone.h @@ -29,27 +29,30 @@ #include #include -#include -#include -#include - #include "battlefield_handler.h" #include "campaign_handler.h" +#include "map/navmesh/inavmesh.h" +#include "map/navmesh/navmesh_config.h" #include "map_config.h" -#include "navmesh_config.h" #include "packets/basic.h" #include "spawn_slot.h" #include "trigger_area.h" +#include + +#include +#include +#include +#include + // // Forward Declarations // enum class Weather : uint16_t; +class XiMesh; class CNavMesh; -class CZoneMesh; class SpawnHandler; -class ZoneLos; enum ZONEID : uint16 { @@ -674,19 +677,17 @@ class CZone auto spawnHandler() const -> SpawnHandler*; - std::unique_ptr m_navMesh; - std::unique_ptr lineOfSight; - - auto zoneMesh() const -> Maybe; - std::map> m_spawnSlots; // add unique slots to zone timer::time_point m_LoadedAt; // The time the zone was loaded + auto navMesh() const -> INavMesh*; + auto xiMesh() const -> IXiMesh*; + auto LoadNavMesh() -> Task; void RebuildNavMesh(const NavMeshConfig& config = {}); - void LoadZoneMesh(); - void LoadZoneLos(); + + void LoadXiMesh(); protected: Scheduler& scheduler_; @@ -705,7 +706,8 @@ class CZone std::unordered_map localVars_; private: - std::unique_ptr zoneMesh_; + std::unique_ptr navMesh_; + std::unique_ptr xiMesh_; ZONEID m_zoneID; ZONE_TYPE m_zoneType; diff --git a/src/map/zone_entities.cpp b/src/map/zone_entities.cpp index 23a4b74ef2b..daaea4b3dce 100644 --- a/src/map/zone_entities.cpp +++ b/src/map/zone_entities.cpp @@ -31,7 +31,6 @@ #include "status_effect_container.h" #include "trade_container.h" #include "treasure_pool.h" -#include "zone_mesh.h" #include "ai/ai_container.h" #include "ai/controllers/mob_controller.h" @@ -57,10 +56,13 @@ #include "utils/synthutils.h" #include "utils/zoneutils.h" +#include + namespace { constexpr auto DYNAMIC_ENTITY_TARGID_RANGE_START = 0x700; +constexpr auto DYNAMIC_ENTITY_TARGID_RANGE_MAX = 0x8FF; constexpr auto ENTITY_RENDER_DISTANCE = 50.0f; constexpr auto ENTITY_VERTICAL_RENDER_DISTANCE = 20.0f; constexpr auto VERTICAL_RENDER_DISTANCE_OFFSET = 0.5f; @@ -595,10 +597,10 @@ void CZoneEntities::AssignDynamicTargIDandLongID(CBaseEntity* PEntity) // Step targid up linearly from 0x700 one by one to 0x8FF unless that ID is already occupied. uint16 targid = m_nextDynamicTargID; - // Wrap around 0x8FF to 0x700 - if (targid > 0x8FF) + // Wrap around DYNAMIC_ENTITY_TARGID_RANGE_MAX (0x8FF) to DYNAMIC_ENTITY_TARGID_RANGE_START (0x700) + if (targid > DYNAMIC_ENTITY_TARGID_RANGE_MAX) { - targid = 0x700; + targid = DYNAMIC_ENTITY_TARGID_RANGE_START; } uint16 counter = 0; @@ -608,10 +610,10 @@ void CZoneEntities::AssignDynamicTargIDandLongID(CBaseEntity* PEntity) { ++targid; - // Wrap around 0x8FF to 0x700 - if (targid > 0x8FF) + // Wrap around DYNAMIC_ENTITY_TARGID_RANGE_MAX (0x8FF) to DYNAMIC_ENTITY_TARGID_RANGE_START (0x700) + if (targid > DYNAMIC_ENTITY_TARGID_RANGE_MAX) { - targid = 0x700; + targid = DYNAMIC_ENTITY_TARGID_RANGE_START; } if (counter > 0x1FF) @@ -1579,19 +1581,13 @@ void CZoneEntities::WideScan(CCharEntity* PChar, uint16 radius) { TracyZoneScoped; - const auto maybeZoneMesh = m_zone->zoneMesh(); - const auto& charPos = PChar->loc.p; - const auto charFloor = maybeZoneMesh ? (*maybeZoneMesh)->getFloorId(charPos.x, charPos.y, charPos.z) : uint8{ 0 }; + const auto& charPos = PChar->loc.p; + const auto charFloor = m_zone->xiMesh()->getFloorId(charPos.x, charPos.y, charPos.z); auto isSameFloor = [&](const CBaseEntity* PEntity) -> bool { - if (!maybeZoneMesh) - { - return true; - } - const auto& pos = PEntity->loc.p; - return (*maybeZoneMesh)->getFloorId(pos.x, pos.y, pos.z) == charFloor; + return m_zone->xiMesh()->getFloorId(pos.x, pos.y, pos.z) == charFloor; }; PChar->pushPacket(GP_TRACKING_STATE::ListStart); diff --git a/src/map/zone_mesh.cpp b/src/map/zone_mesh.cpp deleted file mode 100644 index 1f72c55de4e..00000000000 --- a/src/map/zone_mesh.cpp +++ /dev/null @@ -1,482 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2026 LandSandBoat Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#include "zone_mesh.h" - -#include "common/logging.h" -#include "common/tracy.h" -#include "common/utils.h" - -#include -#include -#include -#include -#include -#include - -namespace -{ - -template -auto readAt(const std::span buf, const uint32 offset) -> T -{ - if (offset + sizeof(T) > buf.size()) - { - return T{}; - } - - T val{}; - std::memcpy(&val, buf.data() + offset, sizeof(T)); - return val; -} - -constexpr uint32 BYTES_PER_VERTEX = 3 * sizeof(float); // x, y, z -constexpr uint32 BYTES_PER_TRIANGLE = 3 * sizeof(uint16); // 3 indices - -auto zlibDecompress(std::vector& rawData) -> std::vector -{ - constexpr size_t CHUNK_SIZE = 32 * 1024; // 32 KB - constexpr size_t MAX_DECOMPRESSED_SIZE = 64 * 1024 * 1024; // 64 MB - - z_stream stream{}; - if (inflateInit(&stream) != Z_OK) - { - return {}; - } - - stream.next_in = rawData.data(); - stream.avail_in = static_cast(rawData.size()); - - std::vector result; - result.reserve(rawData.size() * 4); - std::array chunk{}; - - int rc = Z_OK; - while (rc != Z_STREAM_END) - { - stream.next_out = chunk.data(); - stream.avail_out = static_cast(chunk.size()); - - rc = inflate(&stream, Z_NO_FLUSH); - if (rc != Z_OK && rc != Z_STREAM_END) - { - inflateEnd(&stream); - return {}; - } - - const size_t bytesWritten = chunk.size() - stream.avail_out; - result.insert(result.end(), chunk.data(), chunk.data() + bytesWritten); - - if (result.size() > MAX_DECOMPRESSED_SIZE) - { - inflateEnd(&stream); - return {}; - } - } - - inflateEnd(&stream); - return result; -} - -// Local to world space. -auto transform(const std::array& rot, const std::array& trans, const float* vertex) -> std::array -{ - return { - rot[0] * vertex[0] + rot[3] * vertex[1] + rot[6] * vertex[2] + trans[0], - rot[1] * vertex[0] + rot[4] * vertex[1] + rot[7] * vertex[2] + trans[1], - rot[2] * vertex[0] + rot[5] * vertex[1] + rot[8] * vertex[2] + trans[2], - }; -} - -} // namespace - -auto CZoneMesh::load(const std::string& filename) -> bool -{ - TracyZoneScoped; - - // Step 1. Read the file - std::ifstream file(filename, std::ios::binary | std::ios::ate); - if (!file.good()) - { - return false; - } - - auto fileSize = static_cast(file.tellg()); - file.seekg(0, std::ios::beg); - - std::vector rawData(fileSize); - file.read(reinterpret_cast(rawData.data()), fileSize); - if (file.fail()) - { - ShowErrorFmt("CZoneMesh::load: Failed to read file ({})", filename); - return false; - } - - // Step 2. Decompress ximesh zlib - const std::vector decompressed = zlibDecompress(rawData); - if (decompressed.empty()) - { - ShowErrorFmt("CZoneMesh::load: zlib decompression failed ({})", filename); - return false; - } - - const std::span buf = decompressed; - - // Step 3. Load in the header containing the size of the grid and prepare final vector - if (buf.size() < sizeof(XimeshHeader)) - { - ShowErrorFmt("CZoneMesh::load: File too small ({})", filename); - return false; - } - - std::memcpy(&header_, buf.data(), sizeof(XimeshHeader)); - if (header_.gridWidth == 0 || header_.gridHeight == 0) - { - ShowErrorFmt("CZoneMesh::load: Invalid grid {}x{} ({})", header_.gridWidth, header_.gridHeight, filename); - return false; - } - - const uint32 cellCount = static_cast(header_.gridWidth) * header_.gridHeight; - cells_.resize(cellCount); - blocks_.reserve(header_.blockCount); - placements_.reserve(header_.placementCount); - - uint32 totalEntries = 0; - for (uint32 cellIndex = 0; cellIndex < cellCount; ++cellIndex) - { - const uint32 cellDataOffset = readAt(buf, sizeof(XimeshHeader) + cellIndex * 4); - if (cellDataOffset != 0 && cellDataOffset + sizeof(XimeshCellHeader) <= buf.size()) - { - totalEntries += readAt(buf, cellDataOffset).entryCount; - } - } - entries_.reserve(totalEntries); - - // Step 3a. Define block and placement parsers (deduplicated by file offset) - std::unordered_map blockCache; - std::unordered_map placeCache; - - auto getOrParseBlock = [&](const uint32 fileOffset) -> std::optional - { - auto [it, inserted] = blockCache.try_emplace(fileOffset, static_cast(blocks_.size())); - if (!inserted) - { - return it->second; - } - - MeshBlock block; - const uint16 vertexCount = readAt(buf, fileOffset); - const uint16 triangleCount = readAt(buf, fileOffset + 2); - const uint16 barrierFlag = readAt(buf, fileOffset + 4); - block.hasBarriers = barrierFlag > 0; - - const uint32 vertexBytes = vertexCount * BYTES_PER_VERTEX; - const uint32 indexBytes = triangleCount * BYTES_PER_TRIANGLE; - const uint32 metaBytes = triangleCount; // 1 byte per triangle - - const uint32 vertexOffset = fileOffset + 8; - const uint32 indexOffset = roundUpToNearestFour(vertexOffset + vertexBytes); - const uint32 metaOffset = roundUpToNearestFour(indexOffset + indexBytes); - - if (vertexOffset + vertexBytes > buf.size() || - indexOffset + indexBytes > buf.size() || - metaOffset + metaBytes > buf.size()) - { - ShowErrorFmt("CZoneMesh: Block OOB at offset 0x{:X} (bufSize={})", fileOffset, buf.size()); - return std::nullopt; - } - - block.vertices.resize(vertexCount * 3); - std::memcpy(block.vertices.data(), buf.data() + vertexOffset, vertexBytes); - - block.indices.resize(triangleCount * 3); - std::memcpy(block.indices.data(), buf.data() + indexOffset, indexBytes); - - block.metas.resize(metaBytes); - std::memcpy(block.metas.data(), buf.data() + metaOffset, metaBytes); - - blocks_.emplace_back(std::move(block)); - return it->second; - }; - - auto getOrParsePlacement = [&](const uint32 fileOffset) -> std::optional - { - auto [it, inserted] = placeCache.try_emplace(fileOffset, static_cast(placements_.size())); - if (!inserted) - { - return it->second; - } - - const auto flags = readAt(buf, fileOffset); - MeshPlacement placement{ - .mapId = static_cast(flags.mapIdHigh << 3 | flags.mapIdLow), - .roofed = flags.roofed != 0, - }; - - constexpr size_t TRANSFORM_BYTES = sizeof(placement.rotation) + sizeof(placement.translation); - if (fileOffset + 4 + TRANSFORM_BYTES > buf.size()) - { - ShowErrorFmt("CZoneMesh: Placement OOB at offset 0x{:X} (bufSize={})", fileOffset, buf.size()); - return std::nullopt; - } - - std::memcpy(placement.rotation.data(), buf.data() + fileOffset + 4, TRANSFORM_BYTES); - - placements_.emplace_back(placement); - return it->second; - }; - - // Step 3b. Parse cells - for (uint32 cellIndex = 0; cellIndex < cellCount; ++cellIndex) - { - const uint32 cellDataOffset = readAt(buf, sizeof(XimeshHeader) + cellIndex * 4); - if (cellDataOffset == 0 || cellDataOffset + sizeof(XimeshCellHeader) > buf.size()) - { - continue; - } - - const auto cellHeader = readAt(buf, cellDataOffset); - auto& cell = cells_[cellIndex]; - cell.offset = static_cast(entries_.size()); - cell.count = 0; - - for (uint16 entryIndex = 0; entryIndex < cellHeader.entryCount; ++entryIndex) - { - const uint32 entryOffset = cellDataOffset + sizeof(XimeshCellHeader) + entryIndex * sizeof(XimeshCellEntry); - if (entryOffset + sizeof(XimeshCellEntry) > buf.size()) - { - break; - } - - const auto rawEntry = readAt(buf, entryOffset); - const auto blockIdx = getOrParseBlock(rawEntry.blockOffset); - const auto placementIdx = getOrParsePlacement(rawEntry.placementOffset); - if (!blockIdx || !placementIdx) - { - ShowErrorFmt("CZoneMesh::load: Corrupt block/placement data ({})", filename); - return false; - } - - entries_.push_back({ *blockIdx, *placementIdx }); - cell.count++; - } - } - - // Step 4. Pre-compute Y bounds per placement (used by query culling). - for (uint32 cellIndex = 0; cellIndex < cellCount; ++cellIndex) - { - const auto& cell = cells_[cellIndex]; - for (uint16 ref = 0; ref < cell.count; ++ref) - { - const auto& [blockIdx, placementIdx] = entries_[cell.offset + ref]; - const auto& block = blocks_[blockIdx]; - auto& place = placements_[placementIdx]; - - for (size_t v = 0; v < block.vertices.size(); v += 3) - { - const auto world = transform(place.rotation, place.translation, &block.vertices[v]); - - place.yMin = std::min(place.yMin, world[1]); - place.yMax = std::max(place.yMax, world[1]); - } - } - } - - loaded_ = true; - return true; -} - -// World position to cell grid index. Each cell covers 4x4 world units. -auto CZoneMesh::worldToCell(const float x, const float z) const -> std::pair -{ - return { - static_cast(std::floor(x / 4.0f)) + header_.gridWidth / 2, - static_cast(std::floor(z / 4.0f)) + header_.gridHeight / 2, - }; -} - -// Returns the triangle under (x, z) closest above y. -auto CZoneMesh::query(const float x, const float y, const float z) const -> std::optional -{ - TracyZoneScoped; - - const auto [cx, cz] = worldToCell(x, z); - const std::array point = { x, 0.0f, z }; - - auto searchCell = [&](const int cellX, const int cellZ) -> std::optional - { - if (cellX < 0 || cellX >= header_.gridWidth || cellZ < 0 || cellZ >= header_.gridHeight) - { - return std::nullopt; - } - - const auto& cell = cells_[static_cast(cellZ) * header_.gridWidth + cellX]; - if (cell.count == 0) - { - return std::nullopt; - } - - std::optional best; - for (uint16 ref = 0; ref < cell.count; ++ref) - { - constexpr float EPSILON = 0.01f; - const auto& [blockIdx, placementIdx] = entries_[cell.offset + ref]; - const auto& block = blocks_[blockIdx]; - const auto& place = placements_[placementIdx]; - - // Skip placements entirely above query point - if (place.yMax < y - EPSILON) - { - continue; - } - - for (size_t triIdx = 0; triIdx < block.metas.size(); ++triIdx) - { - const uint16 i0 = block.indices[triIdx * 3 + 0]; - const uint16 i1 = block.indices[triIdx * 3 + 1]; - const uint16 i2 = block.indices[triIdx * 3 + 2]; - - const auto world0 = transform(place.rotation, place.translation, &block.vertices[i0 * 3]); - const auto world1 = transform(place.rotation, place.translation, &block.vertices[i1 * 3]); - const auto world2 = transform(place.rotation, place.translation, &block.vertices[i2 * 3]); - - float triY = 0.0f; - if (!dtClosestHeightPointTriangle(point.data(), world0.data(), world1.data(), world2.data(), triY)) - { - continue; - } - - // Pick the closest triangle above the query point (Y is negative-up). - if (triY >= y - EPSILON && (!best || triY < best->y)) - { - const auto& meta = block.metas[triIdx]; - best = CellHit{ - .type = static_cast(meta.material), - .mapId = place.mapId, - .roofed = place.roofed, - .barrier = meta.barrier != 0, - .y = triY, - }; - } - } - } - - return best; - }; - - // Check target cell first - if (const auto best = searchCell(cx, cz)) - { - return best; - } - - // Miss — check neighbors - std::optional best; - for (int dz = -1; dz <= 1; ++dz) - { - for (int dx = -1; dx <= 1; ++dx) - { - if (dx == 0 && dz == 0) - { - continue; - } - - if (const auto hit = searchCell(cx + dx, cz + dz)) - { - if (!best || hit->y < best->y) - { - best = hit; - } - } - } - } - - return best; -} - -auto CZoneMesh::getTerrainAt(const float x, const float y, const float z) const -> TerrainType -{ - TracyZoneScoped; - - if (!loaded_) - { - return TerrainType::None; - } - - if (const auto hit = query(x, y, z)) - { - return hit->type; - } - - return TerrainType::None; -} - -auto CZoneMesh::getFloorId(const float x, const float y, const float z) const -> uint8 -{ - TracyZoneScoped; - - if (!loaded_) - { - return 0; - } - - if (const auto hit = query(x, y, z)) - { - return hit->mapId; - } - - return 0; -} - -auto CZoneMesh::isLoaded() const -> bool -{ - return loaded_; -} - -auto CZoneMesh::blocks() const -> const std::vector& -{ - return blocks_; -} - -auto CZoneMesh::placements() const -> const std::vector& -{ - return placements_; -} - -auto CZoneMesh::entries() const -> const std::vector& -{ - return entries_; -} - -auto CZoneMesh::cells() const -> const std::vector& -{ - return cells_; -} - -auto CZoneMesh::gridWidth() const -> uint16 -{ - return header_.gridWidth; -} - -auto CZoneMesh::gridHeight() const -> uint16 -{ - return header_.gridHeight; -} diff --git a/src/map/zone_mesh.h b/src/map/zone_mesh.h deleted file mode 100644 index f5ccea2e282..00000000000 --- a/src/map/zone_mesh.h +++ /dev/null @@ -1,101 +0,0 @@ -/* -=========================================================================== - - Copyright (c) 2026 LandSandBoat Dev Teams - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see http://www.gnu.org/licenses/ - -=========================================================================== -*/ - -#pragma once - -#include "common/cbasetypes.h" -#include "enums/terrain_type.h" -#include "ximesh.h" - -#include -#include -#include -#include -#include - -struct MeshBlock -{ - std::vector vertices; // x,y,z interleaved (vertexCount * 3) - std::vector indices; // 3 per triangle - std::vector metas; // 1 per triangle - bool hasBarriers{}; // true if any triangle in this block is a barrier -}; - -struct MeshPlacement -{ - std::array rotation{}; // 3x3 column-major - std::array translation{}; - uint8 mapId{}; - bool roofed{}; - float yMin{ std::numeric_limits::max() }; - float yMax{ std::numeric_limits::lowest() }; -}; - -struct CellEntry -{ - uint16 blockIdx; - uint16 placementIdx; -}; - -struct CellSpan -{ - uint32 offset : 24; - uint32 count : 8; -}; - -struct CellHit -{ - TerrainType type{ TerrainType::None }; - uint8 mapId{}; - bool roofed{}; - bool barrier{}; - float y{}; -}; - -class CZoneMesh -{ -public: - CZoneMesh() = default; - - auto load(const std::string& filename) -> bool; - auto query(float x, float y, float z) const -> std::optional; - auto getTerrainAt(float x, float y, float z) const -> TerrainType; - auto getFloorId(float x, float y, float z) const -> uint8; - auto isLoaded() const -> bool; - - auto blocks() const -> const std::vector&; - auto placements() const -> const std::vector&; - auto entries() const -> const std::vector&; - auto cells() const -> const std::vector&; - auto gridWidth() const -> uint16; - auto gridHeight() const -> uint16; - -private: - auto worldToCell(float x, float z) const -> std::pair; - - bool loaded_{}; - XimeshHeader header_{}; - - std::vector blocks_; - std::vector placements_; - std::vector entries_; - std::vector cells_; -}; From dd80a48a2b4266cdc9d8e08924297d115fccde93 Mon Sep 17 00:00:00 2001 From: Zach Toogood Date: Thu, 7 May 2026 16:59:33 +0100 Subject: [PATCH 2/5] Removed losmeshes submodule --- .gitmodules | 3 --- losmeshes | 1 - 2 files changed, 4 deletions(-) delete mode 160000 losmeshes diff --git a/.gitmodules b/.gitmodules index 79512ff9f65..1d9607b812f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "navmeshes"] path = navmeshes url = https://github.com/LandSandBoat/xiNavmeshes.git -[submodule "losmeshes"] - path = losmeshes - url = https://github.com/LandSandBoat/losmeshes.git [submodule "ximeshes"] path = ximeshes url = https://github.com/InoUno/ximeshes.git diff --git a/losmeshes b/losmeshes deleted file mode 160000 index 538e6c94af9..00000000000 --- a/losmeshes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 538e6c94af965c7c3dd489154a104461557cf328 From 2ded4c9f3e10035c350622f1d601c632771db8f4 Mon Sep 17 00:00:00 2001 From: Zach Toogood Date: Thu, 7 May 2026 17:33:13 +0100 Subject: [PATCH 3/5] Core: Clean up Vector3 --- src/map/ximesh/vector3.h | 46 +++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/map/ximesh/vector3.h b/src/map/ximesh/vector3.h index 9aaf6b51bb8..f0a6805ee33 100644 --- a/src/map/ximesh/vector3.h +++ b/src/map/ximesh/vector3.h @@ -31,12 +31,12 @@ struct Vector3 float y; float z; - Vector3 operator+(const Vector3& vec) const + constexpr Vector3 operator+(const Vector3& vec) const { return Vector3{ x + vec.x, y + vec.y, z + vec.z }; } - Vector3& operator+=(const Vector3& vec) + constexpr Vector3& operator+=(const Vector3& vec) { x += vec.x; y += vec.y; @@ -44,12 +44,12 @@ struct Vector3 return *this; } - Vector3 operator-(const Vector3& vec) const + constexpr Vector3 operator-(const Vector3& vec) const { return Vector3{ x - vec.x, y - vec.y, z - vec.z }; } - Vector3& operator-=(const Vector3& vec) + constexpr Vector3& operator-=(const Vector3& vec) { x -= vec.x; y -= vec.y; @@ -57,12 +57,12 @@ struct Vector3 return *this; } - Vector3 operator*(float value) const + constexpr Vector3 operator*(float value) const { return Vector3{ x * value, y * value, z * value }; } - Vector3& operator*=(float value) + constexpr Vector3& operator*=(float value) { x *= value; y *= value; @@ -70,12 +70,12 @@ struct Vector3 return *this; } - Vector3 operator/(float value) const + constexpr Vector3 operator/(float value) const { return Vector3{ x / value, y / value, z / value }; } - Vector3& operator/=(float value) + constexpr Vector3& operator/=(float value) { x /= value; y /= value; @@ -83,34 +83,28 @@ struct Vector3 return *this; } - Vector3& operator=(const Vector3& vec) + constexpr auto crossProduct(const Vector3& other) const -> Vector3 { - x = vec.x; - y = vec.y; - z = vec.z; - return *this; - } - - Vector3 crossProduct(const Vector3& other) const - { - float ni = y * other.z - z * other.y; - float nj = z * other.x - x * other.z; - float nk = x * other.y - y * other.x; - return Vector3{ ni, nj, nk }; + return Vector3{ + y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x, + }; } - float dotProduct(const Vector3& other) const + constexpr auto dotProduct(const Vector3& other) const -> float { return x * other.x + y * other.y + z * other.z; } - float magnitude() const + constexpr auto magnitudeSquared() const -> float { - return std::sqrt(x * x + y * y + z * z); + return x * x + y * y + z * z; } - float magnitudeSquared() const + // NOTE: std::sqrt is only constexpr in C++23 + auto magnitude() const -> float { - return x * x + y * y + z * z; + return std::sqrt(magnitudeSquared()); } }; From 0beb1b06443a66491755d8b4834ea9c58a0e5478 Mon Sep 17 00:00:00 2001 From: Zach Toogood Date: Thu, 7 May 2026 17:35:15 +0100 Subject: [PATCH 4/5] Remove mentions of losmeshes --- .github/workflows/docker_test.yml | 1 - .github/workflows/publish_meshes.yml | 1 - dev.docker-compose.yml | 5 ----- docker/README.md | 8 ++------ docker/alpine.Dockerfile | 1 - docker/meshes.Dockerfile | 3 +-- docker/ubuntu.Dockerfile | 1 - src/map/map_engine.cpp | 10 +++++----- 8 files changed, 8 insertions(+), 22 deletions(-) diff --git a/.github/workflows/docker_test.yml b/.github/workflows/docker_test.yml index bfd196eb568..4d07215ef0e 100644 --- a/.github/workflows/docker_test.yml +++ b/.github/workflows/docker_test.yml @@ -91,7 +91,6 @@ jobs: --user root \ --network ${{ job.services.database.network }} \ --mount type=bind,src=./navmeshes,dst=/server/navmeshes \ - --mount type=bind,src=./losmeshes,dst=/server/losmeshes \ --mount type=bind,src=./ximeshes,dst=/server/ximeshes \ --mount type=bind,src=./modules,dst=/server/modules \ --mount type=bind,src=.,dst=/server/log \ diff --git a/.github/workflows/publish_meshes.yml b/.github/workflows/publish_meshes.yml index 6797b862a85..906dcecdbf8 100644 --- a/.github/workflows/publish_meshes.yml +++ b/.github/workflows/publish_meshes.yml @@ -5,7 +5,6 @@ on: branches: - base paths: - - './losmeshes/**' - './navmeshes/**' - './ximeshes/**' - '.gitmodules' diff --git a/dev.docker-compose.yml b/dev.docker-compose.yml index e37ad16b98c..078094a1d42 100644 --- a/dev.docker-compose.yml +++ b/dev.docker-compose.yml @@ -102,7 +102,6 @@ services: XI_NETWORK_SQL_HOST: database XI_NETWORK_SQL_PORT: 3306 volumes: - - losmeshes:/server/losmeshes - navmeshes:/server/navmeshes - ${LOCAL_WORKSPACE_FOLDER:-./}/log:/server/log depends_on: @@ -117,7 +116,6 @@ services: XI_NETWORK_SQL_HOST: database XI_NETWORK_SQL_PORT: 3306 volumes: - - losmeshes:/server/losmeshes - navmeshes:/server/navmeshes - ${LOCAL_WORKSPACE_FOLDER:-./}/log:/server/log depends_on: @@ -192,7 +190,6 @@ services: ports: - "54230:54230/udp" volumes: - - losmeshes:/server/losmeshes - navmeshes:/server/navmeshes - ${LOCAL_WORKSPACE_FOLDER:-./}/log:/server/log depends_on: @@ -207,7 +204,5 @@ services: condition: service_started volumes: - losmeshes: - external: true navmeshes: external: true diff --git a/docker/README.md b/docker/README.md index 8c9c6736f29..081b17ed0d5 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,10 +2,10 @@ ## Meshes -`losmeshes` and `navmeshes` are included in a separate image. You can load these into a volume with the following command: +`navmeshes` are included in a separate image. You can load these into a volume with the following command: ```sh -docker run --rm -v losmeshes:/losmeshes -v navmeshes:/navmeshes ghcr.io/landsandboat/ximeshes:latest +docker run --rm -v navmeshes:/navmeshes ghcr.io/landsandboat/ximeshes:latest ``` Once the volumes are created, you can delete the image. @@ -47,7 +47,6 @@ docker run --name some-lsb-server \ -p 54002:54002 \ -p 54230:54230 \ -p 54231:54231 \ --v losmeshes:/server/losmeshes \ -v navmeshes:/server/navmeshes \ -it ghcr.io/landsandboat/server:latest ``` @@ -114,7 +113,6 @@ x-common: &common XI_NETWORK_SQL_HOST: database # XI_{file}_{setting}: value volumes: - - losmeshes:/server/losmeshes - navmeshes:/server/navmeshes # - ./config.yaml:/server/tools/config.yaml # - ./map.lua:/server/settings/map.lua @@ -201,8 +199,6 @@ services: volumes: database: - losmeshes: - external: true navmeshes: external: true ``` diff --git a/docker/alpine.Dockerfile b/docker/alpine.Dockerfile index 6e454a65e8a..63e8a76a923 100644 --- a/docker/alpine.Dockerfile +++ b/docker/alpine.Dockerfile @@ -136,7 +136,6 @@ USER $UNAME # https://docs.docker.com/reference/dockerfile/#copy---exclude (docker/dockerfile:1.7-labs) COPY --chown=$UNAME:$UGROUP \ --exclude=.git \ - --exclude=losmeshes/** \ --exclude=navmeshes/** \ --exclude=ximeshes/** \ --exclude=scripts \ diff --git a/docker/meshes.Dockerfile b/docker/meshes.Dockerfile index 0dcd2194597..ffbc4ec5113 100644 --- a/docker/meshes.Dockerfile +++ b/docker/meshes.Dockerfile @@ -1,7 +1,6 @@ FROM --platform=$BUILDPLATFORM busybox:latest -COPY ./losmeshes/*.obj /losmeshes/ COPY ./navmeshes/*.nav /navmeshes/ COPY ./ximeshes/*.ximesh /ximeshes/ -VOLUME /navmeshes /losmeshes /ximeshes +VOLUME /navmeshes /ximeshes diff --git a/docker/ubuntu.Dockerfile b/docker/ubuntu.Dockerfile index 5e75cf4f4f4..108bdb8300f 100644 --- a/docker/ubuntu.Dockerfile +++ b/docker/ubuntu.Dockerfile @@ -148,7 +148,6 @@ USER $UNAME # https://docs.docker.com/reference/dockerfile/#copy---exclude (docker/dockerfile:1.7-labs) COPY --chown=$UNAME:$UGROUP \ --exclude=.git \ - --exclude=losmeshes/** \ --exclude=navmeshes/** \ --exclude=ximeshes/** \ --exclude=scripts \ diff --git a/src/map/map_engine.cpp b/src/map/map_engine.cpp index 7bbb000d0bb..6a4fa9f38d0 100644 --- a/src/map/map_engine.cpp +++ b/src/map/map_engine.cpp @@ -192,14 +192,14 @@ auto MapEngine::init() -> Task synergyutils::LoadSynergyRecipes(); CItemEquipment::LoadAugmentData(); // TODO: Move to itemutils - if (!std::filesystem::exists("./navmeshes/") || std::filesystem::is_empty("./navmeshes/")) + if (!std::filesystem::exists("./ximeshes/") || std::filesystem::is_empty("./ximeshes/")) { - ShowInfo("./navmeshes/ directory isn't present or is empty"); + ShowError("./ximeshes/ directory isn't present or is empty"); } - - if (!std::filesystem::exists("./losmeshes/") || std::filesystem::is_empty("./losmeshes/")) + + if (!std::filesystem::exists("./navmeshes/") || std::filesystem::is_empty("./navmeshes/")) { - ShowInfo("./losmeshes/ directory isn't present or is empty"); + ShowWarning("./navmeshes/ directory isn't present or is empty"); } co_await zoneutils::Initialize(scheduler_, config_); From 2966c451aabcf75ab8d37540a38d2378cb368847 Mon Sep 17 00:00:00 2001 From: Zach Toogood Date: Thu, 7 May 2026 17:40:19 +0100 Subject: [PATCH 5/5] Core: Add safety check to entity:canSee(...) binding --- scripts/commands/cansee.lua | 7 ++----- src/map/lua/lua_baseentity.cpp | 20 +++++++++++++------- src/map/lua/lua_baseentity.h | 2 +- src/map/map_engine.cpp | 2 +- src/map/zone.h | 1 - 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/scripts/commands/cansee.lua b/scripts/commands/cansee.lua index 37a5087cde4..51520579ac1 100644 --- a/scripts/commands/cansee.lua +++ b/scripts/commands/cansee.lua @@ -18,11 +18,8 @@ commandObj.onTrigger = function(player) return end - if player:canSee(target) then - player:printToPlayer(string.format('%s CAN see %s', player:getName(), target:getName())) - else - player:printToPlayer(string.format('%s CANNOT see %s', player:getName(), target:getName())) - end + local str = player:canSee(target) and 'CAN' or 'CANNOT' + player:printToPlayer(string.format('%s %s see %s', player:getName(), str, target:getName())) end return commandObj diff --git a/src/map/lua/lua_baseentity.cpp b/src/map/lua/lua_baseentity.cpp index 6efe47e1b51..8776407313b 100644 --- a/src/map/lua/lua_baseentity.cpp +++ b/src/map/lua/lua_baseentity.cpp @@ -2232,19 +2232,25 @@ void CLuaBaseEntity::setCarefulPathing(bool careful) /************************************************************************ * Function: canSee(...) - * Purpose : - * Example : + * Purpose : Execute a raycast between ENTITY_HEIGHT and the found at target's feet + * Example : player:canSee(mob) ************************************************************************/ -bool CLuaBaseEntity::canSee(const CLuaBaseEntity* target) +bool CLuaBaseEntity::canSee(const CLuaBaseEntity* PTarget) { - return m_PBaseEntity->CanSeeTarget(target->GetBaseEntity()); + if (!PTarget) + { + ShowWarning("Attempting to see invalid entity (from %s).", m_PBaseEntity->getName()); + return false; + } + + return m_PBaseEntity->CanSeeTarget(PTarget->GetBaseEntity()); } /************************************************************************ - * Function: inWater(...) - * Purpose : - * Example : + * Function: inWater() + * Purpose : Execute an ximesh cell search downwards to check if entity is in water + * Example : if player:inWater() then ... ************************************************************************/ bool CLuaBaseEntity::inWater() diff --git a/src/map/lua/lua_baseentity.h b/src/map/lua/lua_baseentity.h index 86579fab532..ab689109e36 100644 --- a/src/map/lua/lua_baseentity.h +++ b/src/map/lua/lua_baseentity.h @@ -160,7 +160,7 @@ class CLuaBaseEntity // int32 LimitDistance(lua_Stat* L); // limits the current path distance to given max distance void setCarefulPathing(bool careful); - bool canSee(const CLuaBaseEntity* target); + bool canSee(const CLuaBaseEntity* PTarget); bool inWater(); void openDoor(const sol::object& seconds); diff --git a/src/map/map_engine.cpp b/src/map/map_engine.cpp index 6a4fa9f38d0..abe214e5937 100644 --- a/src/map/map_engine.cpp +++ b/src/map/map_engine.cpp @@ -196,7 +196,7 @@ auto MapEngine::init() -> Task { ShowError("./ximeshes/ directory isn't present or is empty"); } - + if (!std::filesystem::exists("./navmeshes/") || std::filesystem::is_empty("./navmeshes/")) { ShowWarning("./navmeshes/ directory isn't present or is empty"); diff --git a/src/map/zone.h b/src/map/zone.h index 98e99296549..41e5de58528 100644 --- a/src/map/zone.h +++ b/src/map/zone.h @@ -717,7 +717,6 @@ class CZone std::string m_zoneName; uint16 m_zonePort{}; uint32 m_zoneIP{}; - bool m_useNavMesh; Weather m_Weather; uint32 m_WeatherChangeTime;