feat(movement): Port BotMovement system Phase 1 from playerbot-dev

Port movement system refactoring commits from TrinityCore playerbot-dev:
- Phase 1.1: Core Infrastructure Setup (BotMovementDefines, ValidationResult)
- Phase 1.2: Configuration System (BotMovementConfig)
- Phase 1.3: BotMovementManager Singleton
- Phase 1.6: BotMovementController Base Implementation
- Phase 1.7: Integration Testing

Movement System Components:
- BotMovementManager: Singleton managing bot movement globally
- BotMovementController: Per-bot movement control
- BotMovementConfig: Configuration with validation levels and thresholds
- PositionValidator: Position bounds and map ID validation
- GroundValidator: Ground/terrain validation
- PathCache: Path caching for performance
- MovementMetrics: Movement statistics tracking
- ValidationResult: Validation result structures

Key Features:
- ValidationLevel enum (None, Basic, Standard, Strict)
- MovementStateType enum (Idle, Ground, Swimming, Flying, Falling, Stuck)
- RecoveryLevel progressive recovery (5 levels from path recalculation to evade)
- Stuck detection with configurable thresholds
- Path caching with configurable TTL
- NaN/Infinity coordinate detection
- Map ID validation
- Comprehensive integration tests

Ported commits:
- ad0cf3c6af Phase 1.1 - Core Infrastructure Setup
- a819487d00 Phase 1.2 - Configuration System
- 4abacbc805, f6aab87e7d, 7713626fc0 Phase 1.3 - BotMovementManager
- f96836f550 Phase 1.6 - BotMovementController Base
- 79ddf8479d Phase 1.7 - Integration Testing

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
agatho
2026-01-27 17:49:19 +01:00
co-authored by Claude Opus 4.5
parent 3dbf17ae4b
commit 2277d32f74
16 changed files with 1615 additions and 0 deletions
@@ -0,0 +1,44 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 "BotMovementConfig.h"
#include "Config.h"
void BotMovementConfig::Load()
{
_enabled = sConfigMgr->GetBoolDefault("BotMovement.Enable", true);
uint32 validationLevelValue = sConfigMgr->GetIntDefault("BotMovement.ValidationLevel", 2);
if (validationLevelValue > static_cast<uint32>(ValidationLevel::Strict))
validationLevelValue = static_cast<uint32>(ValidationLevel::Standard);
_validationLevel = static_cast<ValidationLevel>(validationLevelValue);
_stuckPosThreshold = Milliseconds(sConfigMgr->GetIntDefault("BotMovement.StuckDetection.PositionThreshold", 3000));
_stuckDistThreshold = sConfigMgr->GetFloatDefault("BotMovement.StuckDetection.DistanceThreshold", 2.0f);
_maxRecoveryAttempts = sConfigMgr->GetIntDefault("BotMovement.Recovery.MaxAttempts", 5);
_pathCacheSize = sConfigMgr->GetIntDefault("BotMovement.PathCache.Size", 1000);
_pathCacheTTL = Seconds(sConfigMgr->GetIntDefault("BotMovement.PathCache.TTL", 60));
_debugLogLevel = sConfigMgr->GetIntDefault("BotMovement.Debug.LogLevel", 2);
}
void BotMovementConfig::Reload()
{
Load();
}
@@ -0,0 +1,54 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 TRINITYCORE_BOT_MOVEMENT_CONFIG_H
#define TRINITYCORE_BOT_MOVEMENT_CONFIG_H
#include "Define.h"
#include "Duration.h"
#include "BotMovementDefines.h"
class TC_GAME_API BotMovementConfig
{
public:
BotMovementConfig() = default;
~BotMovementConfig() = default;
void Load();
void Reload();
bool IsEnabled() const { return _enabled; }
ValidationLevel GetValidationLevel() const { return _validationLevel; }
Milliseconds GetStuckPositionThreshold() const { return _stuckPosThreshold; }
float GetStuckDistanceThreshold() const { return _stuckDistThreshold; }
uint32 GetMaxRecoveryAttempts() const { return _maxRecoveryAttempts; }
uint32 GetPathCacheSize() const { return _pathCacheSize; }
Seconds GetPathCacheTTL() const { return _pathCacheTTL; }
uint32 GetDebugLogLevel() const { return _debugLogLevel; }
private:
bool _enabled = true;
ValidationLevel _validationLevel = ValidationLevel::Standard;
Milliseconds _stuckPosThreshold = Milliseconds(3000);
float _stuckDistThreshold = 2.0f;
uint32 _maxRecoveryAttempts = 5;
uint32 _pathCacheSize = 1000;
Seconds _pathCacheTTL = Seconds(60);
uint32 _debugLogLevel = 2;
};
#endif
@@ -0,0 +1,57 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 "BotMovementController.h"
#include "Unit.h"
#include "Position.h"
BotMovementController::BotMovementController(Unit* owner)
: _owner(owner)
, _totalTimePassed(0)
{
if (_owner)
RecordPosition();
}
BotMovementController::~BotMovementController()
{
}
void BotMovementController::Update(uint32 diff)
{
_totalTimePassed += diff;
}
Position const* BotMovementController::GetLastPosition() const
{
if (_positionHistory.empty())
return nullptr;
return &_positionHistory.back().pos;
}
void BotMovementController::RecordPosition()
{
if (!_owner)
return;
Position currentPos = _owner->GetPosition();
_positionHistory.emplace_back(currentPos, _totalTimePassed);
if (_positionHistory.size() > MAX_POSITION_HISTORY)
_positionHistory.pop_front();
}
@@ -0,0 +1,50 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 TRINITY_BOTMOVEMENTCONTROLLER_H
#define TRINITY_BOTMOVEMENTCONTROLLER_H
#include "Define.h"
#include "BotMovementDefines.h"
#include <deque>
class Unit;
struct Position;
class TC_GAME_API BotMovementController
{
public:
explicit BotMovementController(Unit* owner);
~BotMovementController();
Unit* GetOwner() const { return _owner; }
void Update(uint32 diff);
Position const* GetLastPosition() const;
void RecordPosition();
std::deque<PositionSnapshot> const& GetPositionHistory() const { return _positionHistory; }
private:
Unit* _owner;
std::deque<PositionSnapshot> _positionHistory;
uint32 _totalTimePassed;
static constexpr size_t MAX_POSITION_HISTORY = 100;
};
#endif
@@ -0,0 +1,84 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 TRINITY_BOTMOVEMENTDEFINES_H
#define TRINITY_BOTMOVEMENTDEFINES_H
#include "Common.h"
#include "Position.h"
struct PositionSnapshot
{
Position pos;
uint32 timestamp;
PositionSnapshot() : pos(), timestamp(0) { }
PositionSnapshot(Position const& p, uint32 time) : pos(p), timestamp(time) { }
};
enum class MovementStateType : uint8
{
Idle = 0,
Ground,
Swimming,
Flying,
Falling,
Stuck
};
enum class ValidationFailureReason : uint8
{
None = 0,
InvalidPosition,
OutOfBounds,
InvalidMapId,
NoGroundHeight,
VoidPosition,
CollisionDetected,
PathBlocked,
DestinationUnreachable,
LiquidDanger,
UnsafeTerrain
};
enum class StuckType : uint8
{
None = 0,
PositionStuck,
ProgressStuck,
CollisionStuck,
PathFailureStuck
};
enum class RecoveryLevel : uint8
{
Level1_RecalculatePath = 1,
Level2_BackupAndRetry = 2,
Level3_RandomNearbyPosition = 3,
Level4_TeleportToSafePosition = 4,
Level5_EvadeAndReset = 5
};
enum class ValidationLevel : uint8
{
None = 0,
Basic,
Standard,
Strict
};
#endif
@@ -0,0 +1,115 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 "BotMovementManager.h"
#include "BotMovementController.h"
#include "Unit.h"
#include "Log.h"
BotMovementManager::BotMovementManager()
{
_config.Load();
}
BotMovementManager::~BotMovementManager()
{
for (auto const& [guid, controller] : _controllers)
delete controller;
_controllers.clear();
}
BotMovementManager* BotMovementManager::Instance()
{
static BotMovementManager instance;
return &instance;
}
BotMovementController* BotMovementManager::GetControllerForUnit(Unit* unit)
{
if (!unit)
return nullptr;
auto itr = _controllers.find(unit->GetGUID());
if (itr != _controllers.end())
return itr->second;
return nullptr;
}
BotMovementController* BotMovementManager::RegisterController(Unit* unit)
{
if (!unit)
{
TC_LOG_ERROR("movement.bot", "BotMovementManager::RegisterController - Attempted to register null unit");
return nullptr;
}
ObjectGuid guid = unit->GetGUID();
auto itr = _controllers.find(guid);
if (itr != _controllers.end())
{
TC_LOG_WARN("movement.bot", "BotMovementManager::RegisterController - Controller already exists for unit {}", guid.ToString());
return itr->second;
}
BotMovementController* controller = new BotMovementController(unit);
_controllers[guid] = controller;
TC_LOG_DEBUG("movement.bot", "BotMovementManager::RegisterController - Registered controller for unit {}", guid.ToString());
return controller;
}
void BotMovementManager::UnregisterController(Unit* unit)
{
if (!unit)
return;
UnregisterController(unit->GetGUID());
}
void BotMovementManager::UnregisterController(ObjectGuid const& guid)
{
auto itr = _controllers.find(guid);
if (itr == _controllers.end())
return;
TC_LOG_DEBUG("movement.bot", "BotMovementManager::UnregisterController - Unregistering controller for unit {}", guid.ToString());
delete itr->second;
_controllers.erase(itr);
}
void BotMovementManager::ReloadConfig()
{
TC_LOG_INFO("movement.bot", "BotMovementManager::ReloadConfig - Reloading bot movement configuration");
_config.Reload();
_globalCache.Clear();
}
MovementMetrics BotMovementManager::GetGlobalMetrics() const
{
return _metrics;
}
void BotMovementManager::ResetMetrics()
{
TC_LOG_INFO("movement.bot", "BotMovementManager::ResetMetrics - Resetting global movement metrics");
_metrics.Reset();
}
@@ -0,0 +1,67 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 TRINITY_BOTMOVEMENTMANAGER_H
#define TRINITY_BOTMOVEMENTMANAGER_H
#include "Define.h"
#include "ObjectGuid.h"
#include "BotMovementConfig.h"
#include "MovementMetrics.h"
#include "PathCache.h"
#include <unordered_map>
class BotMovementController;
class Unit;
class TC_GAME_API BotMovementManager
{
private:
BotMovementManager();
~BotMovementManager();
public:
BotMovementManager(BotMovementManager const&) = delete;
BotMovementManager(BotMovementManager&&) = delete;
BotMovementManager& operator=(BotMovementManager const&) = delete;
BotMovementManager& operator=(BotMovementManager&&) = delete;
static BotMovementManager* Instance();
BotMovementController* GetControllerForUnit(Unit* unit);
BotMovementController* RegisterController(Unit* unit);
void UnregisterController(Unit* unit);
void UnregisterController(ObjectGuid const& guid);
BotMovementConfig const& GetConfig() const { return _config; }
void ReloadConfig();
PathCache* GetPathCache() { return &_globalCache; }
MovementMetrics GetGlobalMetrics() const;
void ResetMetrics();
private:
BotMovementConfig _config;
PathCache _globalCache;
std::unordered_map<ObjectGuid, BotMovementController*> _controllers;
MovementMetrics _metrics;
};
#define sBotMovementManager BotMovementManager::Instance()
#endif
@@ -0,0 +1,41 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 TRINITY_MOVEMENTMETRICS_H
#define TRINITY_MOVEMENTMETRICS_H
#include "Define.h"
struct TC_GAME_API MovementMetrics
{
uint64 totalPathsCalculated = 0;
uint64 totalValidationFailures = 0;
uint64 totalStuckIncidents = 0;
uint64 totalRecoveryAttempts = 0;
uint64 totalRecoverySuccesses = 0;
void Reset()
{
totalPathsCalculated = 0;
totalValidationFailures = 0;
totalStuckIncidents = 0;
totalRecoveryAttempts = 0;
totalRecoverySuccesses = 0;
}
};
#endif
@@ -0,0 +1,48 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 TRINITY_VALIDATIONRESULT_H
#define TRINITY_VALIDATIONRESULT_H
#include "BotMovementDefines.h"
#include <string>
struct ValidationResult
{
bool isValid = false;
ValidationFailureReason failureReason = ValidationFailureReason::None;
std::string errorMessage;
ValidationResult() = default;
ValidationResult(bool valid, ValidationFailureReason reason = ValidationFailureReason::None, std::string message = "")
: isValid(valid), failureReason(reason), errorMessage(std::move(message))
{
}
static ValidationResult Success()
{
return ValidationResult(true, ValidationFailureReason::None, "");
}
static ValidationResult Failure(ValidationFailureReason reason, std::string message = "")
{
return ValidationResult(false, reason, std::move(message));
}
};
#endif
@@ -0,0 +1,32 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 TRINITY_PATHCACHE_H
#define TRINITY_PATHCACHE_H
#include "Define.h"
class TC_GAME_API PathCache
{
public:
PathCache() = default;
~PathCache() = default;
void Clear() { }
};
#endif
@@ -0,0 +1,193 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 "GroundValidator.h"
#include "Unit.h"
#include "Map.h"
#include "Position.h"
#include "GridDefines.h"
#include "GameTime.h"
#include "MapDefines.h"
#include <sstream>
#include <cmath>
std::unordered_map<uint64, GroundHeightCache> GroundValidator::_heightCache;
GroundValidator::GroundValidator()
{
}
uint64 GroundValidator::MakeCacheKey(uint32 mapId, float x, float y)
{
uint32 gridX = static_cast<uint32>(std::floor(x / 10.0f));
uint32 gridY = static_cast<uint32>(std::floor(y / 10.0f));
return (static_cast<uint64>(mapId) << 32) | (static_cast<uint64>(gridX) << 16) | gridY;
}
float GroundValidator::GetGroundHeight(Unit const* unit)
{
if (!unit || !unit->IsInWorld())
return INVALID_HEIGHT;
Map* map = unit->GetMap();
if (!map)
return INVALID_HEIGHT;
float x = unit->GetPositionX();
float y = unit->GetPositionY();
float z = unit->GetPositionZ();
uint64 cacheKey = MakeCacheKey(map->GetId(), x, y);
uint32 currentTime = GameTime::GetGameTimeMS();
auto it = _heightCache.find(cacheKey);
if (it != _heightCache.end() && (currentTime - it->second.timestamp) < CACHE_LIFETIME_MS)
{
return it->second.height;
}
float height = map->GetHeight(unit->GetPhaseShift(), x, y, z, true);
GroundHeightCache cache;
cache.height = height;
cache.timestamp = currentTime;
_heightCache[cacheKey] = cache;
return height;
}
ValidationResult GroundValidator::ValidateGroundHeight(Unit const* unit, float maxHeightDiff)
{
if (!unit)
{
return ValidationResult::Failure(
ValidationFailureReason::InvalidPosition,
"Unit is null");
}
if (!unit->IsInWorld())
{
return ValidationResult::Failure(
ValidationFailureReason::InvalidPosition,
"Unit is not in world");
}
float x = unit->GetPositionX();
float y = unit->GetPositionY();
float z = unit->GetPositionZ();
float groundHeight = GetGroundHeight(unit);
if (groundHeight == INVALID_HEIGHT)
{
std::ostringstream oss;
oss << "No ground height found at position (" << x << ", " << y << ", " << z << ")";
return ValidationResult::Failure(ValidationFailureReason::NoGroundHeight, oss.str());
}
if (groundHeight <= VOID_HEIGHT)
{
std::ostringstream oss;
oss << "Void position detected at (" << x << ", " << y << ", " << z << "), ground height: " << groundHeight;
return ValidationResult::Failure(ValidationFailureReason::VoidPosition, oss.str());
}
float heightDiff = std::abs(z - groundHeight);
if (heightDiff > maxHeightDiff)
{
if (z < groundHeight - maxHeightDiff)
{
std::ostringstream oss;
oss << "Position too far below ground at (" << x << ", " << y << ", " << z
<< "), ground height: " << groundHeight << ", diff: " << heightDiff;
return ValidationResult::Failure(ValidationFailureReason::InvalidPosition, oss.str());
}
else if (z > groundHeight + BOT_MAX_FALL_DISTANCE)
{
std::ostringstream oss;
oss << "Position too far above ground at (" << x << ", " << y << ", " << z
<< "), ground height: " << groundHeight << ", diff: " << heightDiff;
return ValidationResult::Failure(ValidationFailureReason::InvalidPosition, oss.str());
}
}
return ValidationResult::Success();
}
bool GroundValidator::IsVoidPosition(Unit const* unit)
{
if (!unit || !unit->IsInWorld())
return true;
float groundHeight = GetGroundHeight(unit);
return (groundHeight == INVALID_HEIGHT || groundHeight <= VOID_HEIGHT);
}
bool GroundValidator::IsOnBridge(Unit const* unit)
{
if (!unit || !unit->IsInWorld())
return false;
Map* map = unit->GetMap();
if (!map)
return false;
float x = unit->GetPositionX();
float y = unit->GetPositionY();
float z = unit->GetPositionZ();
float heightWithVMap = map->GetHeight(unit->GetPhaseShift(), x, y, z, true);
float heightWithoutVMap = map->GetHeight(unit->GetPhaseShift(), x, y, z, false);
if (heightWithVMap == INVALID_HEIGHT || heightWithoutVMap == INVALID_HEIGHT)
return false;
return std::abs(heightWithVMap - heightWithoutVMap) > 1.0f;
}
bool GroundValidator::IsUnsafeTerrain(Unit const* unit)
{
if (!unit || !unit->IsInWorld())
return true;
if (IsVoidPosition(unit))
return true;
Map* map = unit->GetMap();
if (!map)
return true;
float x = unit->GetPositionX();
float y = unit->GetPositionY();
float z = unit->GetPositionZ();
LiquidData liquidData;
ZLiquidStatus liquidStatus = map->GetLiquidStatus(unit->GetPhaseShift(), x, y, z, map_liquidHeaderTypeFlags::AllLiquids, &liquidData);
if (liquidStatus != LIQUID_MAP_NO_WATER)
{
if (liquidData.type_flags.HasFlag(map_liquidHeaderTypeFlags::Magma) || liquidData.type_flags.HasFlag(map_liquidHeaderTypeFlags::Slime))
return true;
}
return false;
}
void GroundValidator::ClearCache()
{
_heightCache.clear();
}
@@ -0,0 +1,61 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 TRINITY_GROUNDVALIDATOR_H
#define TRINITY_GROUNDVALIDATOR_H
#include "ValidationResult.h"
#include "Define.h"
#include <unordered_map>
struct Position;
class Unit;
class Map;
struct GroundHeightCache
{
float height = 0.0f;
uint32 timestamp = 0;
};
class TC_GAME_API GroundValidator
{
public:
GroundValidator();
~GroundValidator() = default;
static float GetGroundHeight(Unit const* unit);
static ValidationResult ValidateGroundHeight(Unit const* unit, float maxHeightDiff = 10.0f);
static bool IsVoidPosition(Unit const* unit);
static bool IsOnBridge(Unit const* unit);
static bool IsUnsafeTerrain(Unit const* unit);
void ClearCache();
private:
static constexpr float VOID_HEIGHT = -500.0f;
static constexpr float BOT_MAX_FALL_DISTANCE = 50.0f;
static constexpr uint32 CACHE_LIFETIME_MS = 5000;
static std::unordered_map<uint64, GroundHeightCache> _heightCache;
static uint64 MakeCacheKey(uint32 mapId, float x, float y);
};
#endif
@@ -0,0 +1,89 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 "PositionValidator.h"
#include "Position.h"
#include "Unit.h"
#include "GridDefines.h"
#include "MapManager.h"
#include <sstream>
ValidationResult PositionValidator::ValidateBounds(Position const& pos)
{
return ValidateBounds(pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ());
}
ValidationResult PositionValidator::ValidateBounds(float x, float y, float z)
{
if (!Trinity::IsValidMapCoord(x, y, z))
{
std::ostringstream oss;
oss << "Position out of bounds: (" << x << ", " << y << ", " << z << ")";
return ValidationResult::Failure(ValidationFailureReason::OutOfBounds, oss.str());
}
return ValidationResult::Success();
}
ValidationResult PositionValidator::ValidateMapId(uint32 mapId)
{
if (!MapManager::IsValidMAP(mapId))
{
std::ostringstream oss;
oss << "Invalid map ID: " << mapId;
return ValidationResult::Failure(ValidationFailureReason::InvalidMapId, oss.str());
}
return ValidationResult::Success();
}
ValidationResult PositionValidator::ValidatePosition(uint32 mapId, Position const& pos)
{
return ValidatePosition(mapId, pos.GetPositionX(), pos.GetPositionY(), pos.GetPositionZ());
}
ValidationResult PositionValidator::ValidatePosition(uint32 mapId, float x, float y, float z)
{
ValidationResult mapResult = ValidateMapId(mapId);
if (!mapResult.isValid)
return mapResult;
ValidationResult boundsResult = ValidateBounds(x, y, z);
if (!boundsResult.isValid)
return boundsResult;
return ValidationResult::Success();
}
ValidationResult PositionValidator::ValidateUnitPosition(Unit const* unit)
{
if (!unit)
{
return ValidationResult::Failure(
ValidationFailureReason::InvalidPosition,
"Unit is null");
}
if (!unit->IsInWorld())
{
return ValidationResult::Failure(
ValidationFailureReason::InvalidPosition,
"Unit is not in world");
}
return ValidatePosition(unit->GetMapId(), *unit);
}
@@ -0,0 +1,43 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 TRINITY_POSITIONVALIDATOR_H
#define TRINITY_POSITIONVALIDATOR_H
#include "ValidationResult.h"
struct Position;
class Unit;
class TC_GAME_API PositionValidator
{
public:
PositionValidator() = default;
~PositionValidator() = default;
static ValidationResult ValidateBounds(Position const& pos);
static ValidationResult ValidateBounds(float x, float y, float z);
static ValidationResult ValidateMapId(uint32 mapId);
static ValidationResult ValidatePosition(uint32 mapId, Position const& pos);
static ValidationResult ValidatePosition(uint32 mapId, float x, float y, float z);
static ValidationResult ValidateUnitPosition(Unit const* unit);
};
#endif
+202
View File
@@ -0,0 +1,202 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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/>.
*/
#define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER
#include "tc_catch2.h"
#include "BotMovementConfig.h"
#include "Config.h"
#include <boost/filesystem.hpp>
#include <fstream>
#include <map>
#include <string>
std::string CreateBotMovementConfig(std::map<std::string, std::string> const& map)
{
auto tempFileRel = boost::filesystem::unique_path("botmovement_test.ini");
auto tempFileAbs = boost::filesystem::temp_directory_path() / tempFileRel;
std::ofstream iniStream;
iniStream.open(tempFileAbs.c_str());
iniStream << "[worldserver]\n";
for (auto const& itr : map)
iniStream << itr.first << " = " << itr.second << "\n";
iniStream.close();
return tempFileAbs.string();
}
TEST_CASE("BotMovementConfig - Default Values", "[BotMovement][Config]")
{
std::map<std::string, std::string> config;
auto filePath = CreateBotMovementConfig(config);
std::string err;
REQUIRE(sConfigMgr->LoadInitial(filePath, std::vector<std::string>(), err));
REQUIRE(err.empty());
BotMovementConfig botConfig;
botConfig.Load();
SECTION("Default values are correct")
{
REQUIRE(botConfig.IsEnabled() == true);
REQUIRE(botConfig.GetValidationLevel() == ValidationLevel::Standard);
REQUIRE(botConfig.GetStuckPositionThreshold() == Milliseconds(3000));
REQUIRE(botConfig.GetStuckDistanceThreshold() == 2.0f);
REQUIRE(botConfig.GetMaxRecoveryAttempts() == 5);
REQUIRE(botConfig.GetPathCacheSize() == 1000);
REQUIRE(botConfig.GetPathCacheTTL() == Seconds(60));
REQUIRE(botConfig.GetDebugLogLevel() == 2);
}
std::remove(filePath.c_str());
}
TEST_CASE("BotMovementConfig - Custom Values", "[BotMovement][Config]")
{
std::map<std::string, std::string> config;
config["BotMovement.Enable"] = "0";
config["BotMovement.ValidationLevel"] = "3";
config["BotMovement.StuckDetection.PositionThreshold"] = "5000";
config["BotMovement.StuckDetection.DistanceThreshold"] = "3.5";
config["BotMovement.Recovery.MaxAttempts"] = "10";
config["BotMovement.PathCache.Size"] = "2000";
config["BotMovement.PathCache.TTL"] = "120";
config["BotMovement.Debug.LogLevel"] = "4";
auto filePath = CreateBotMovementConfig(config);
std::string err;
REQUIRE(sConfigMgr->LoadInitial(filePath, std::vector<std::string>(), err));
REQUIRE(err.empty());
BotMovementConfig botConfig;
botConfig.Load();
SECTION("Custom values are loaded correctly")
{
REQUIRE(botConfig.IsEnabled() == false);
REQUIRE(botConfig.GetValidationLevel() == ValidationLevel::Strict);
REQUIRE(botConfig.GetStuckPositionThreshold() == Milliseconds(5000));
REQUIRE(botConfig.GetStuckDistanceThreshold() == 3.5f);
REQUIRE(botConfig.GetMaxRecoveryAttempts() == 10);
REQUIRE(botConfig.GetPathCacheSize() == 2000);
REQUIRE(botConfig.GetPathCacheTTL() == Seconds(120));
REQUIRE(botConfig.GetDebugLogLevel() == 4);
}
std::remove(filePath.c_str());
}
TEST_CASE("BotMovementConfig - Validation Level Bounds", "[BotMovement][Config]")
{
SECTION("Invalid validation level defaults to Standard")
{
std::map<std::string, std::string> config;
config["BotMovement.ValidationLevel"] = "99";
auto filePath = CreateBotMovementConfig(config);
std::string err;
REQUIRE(sConfigMgr->LoadInitial(filePath, std::vector<std::string>(), err));
REQUIRE(err.empty());
BotMovementConfig botConfig;
botConfig.Load();
REQUIRE(botConfig.GetValidationLevel() == ValidationLevel::Standard);
std::remove(filePath.c_str());
}
SECTION("ValidationLevel::None is valid")
{
std::map<std::string, std::string> config;
config["BotMovement.ValidationLevel"] = "0";
auto filePath = CreateBotMovementConfig(config);
std::string err;
REQUIRE(sConfigMgr->LoadInitial(filePath, std::vector<std::string>(), err));
REQUIRE(err.empty());
BotMovementConfig botConfig;
botConfig.Load();
REQUIRE(botConfig.GetValidationLevel() == ValidationLevel::None);
std::remove(filePath.c_str());
}
SECTION("ValidationLevel::Strict is valid")
{
std::map<std::string, std::string> config;
config["BotMovement.ValidationLevel"] = "3";
auto filePath = CreateBotMovementConfig(config);
std::string err;
REQUIRE(sConfigMgr->LoadInitial(filePath, std::vector<std::string>(), err));
REQUIRE(err.empty());
BotMovementConfig botConfig;
botConfig.Load();
REQUIRE(botConfig.GetValidationLevel() == ValidationLevel::Strict);
std::remove(filePath.c_str());
}
}
TEST_CASE("BotMovementConfig - Reload", "[BotMovement][Config]")
{
std::map<std::string, std::string> config;
config["BotMovement.Enable"] = "1";
config["BotMovement.ValidationLevel"] = "2";
auto filePath = CreateBotMovementConfig(config);
std::string err;
REQUIRE(sConfigMgr->LoadInitial(filePath, std::vector<std::string>(), err));
REQUIRE(err.empty());
BotMovementConfig botConfig;
botConfig.Load();
REQUIRE(botConfig.IsEnabled() == true);
REQUIRE(botConfig.GetValidationLevel() == ValidationLevel::Standard);
std::ofstream iniStream;
iniStream.open(filePath.c_str(), std::ios::trunc);
iniStream << "[worldserver]\n";
iniStream << "BotMovement.Enable = 0\n";
iniStream << "BotMovement.ValidationLevel = 1\n";
iniStream.close();
std::vector<std::string> errors;
REQUIRE(sConfigMgr->Reload(errors));
REQUIRE(errors.empty());
botConfig.Reload();
REQUIRE(botConfig.IsEnabled() == false);
REQUIRE(botConfig.GetValidationLevel() == ValidationLevel::Basic);
std::remove(filePath.c_str());
}
+435
View File
@@ -0,0 +1,435 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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/>.
*/
#define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER
#include "tc_catch2.h"
#include "BotMovementManager.h"
#include "BotMovementController.h"
#include "PositionValidator.h"
#include "GroundValidator.h"
#include "ValidationResult.h"
#include "BotMovementDefines.h"
#include "Position.h"
TEST_CASE("Phase 1 Integration - BotMovementManager Singleton", "[BotMovement][Integration][Phase1]")
{
SECTION("Manager instance is accessible")
{
BotMovementManager* manager = sBotMovementManager;
REQUIRE(manager != nullptr);
}
SECTION("Multiple calls return same instance")
{
BotMovementManager* manager1 = sBotMovementManager;
BotMovementManager* manager2 = sBotMovementManager;
REQUIRE(manager1 == manager2);
}
SECTION("Manager has valid config")
{
BotMovementManager* manager = sBotMovementManager;
BotMovementConfig const& config = manager->GetConfig();
REQUIRE(config.GetValidationLevel() != ValidationLevel::None);
}
SECTION("Manager has path cache")
{
BotMovementManager* manager = sBotMovementManager;
PathCache* cache = manager->GetPathCache();
REQUIRE(cache != nullptr);
}
}
TEST_CASE("Phase 1 Integration - PositionValidator Bounds", "[BotMovement][Integration][Phase1][Validation]")
{
SECTION("Valid position within normal bounds")
{
Position pos(0.0f, 0.0f, 0.0f);
ValidationResult result = PositionValidator::ValidateBounds(pos);
REQUIRE(result.isValid == true);
REQUIRE(result.failureReason == ValidationFailureReason::None);
}
SECTION("Valid position with normal coordinates")
{
Position pos(1000.0f, 1000.0f, 100.0f);
ValidationResult result = PositionValidator::ValidateBounds(pos);
REQUIRE(result.isValid == true);
REQUIRE(result.failureReason == ValidationFailureReason::None);
}
SECTION("Invalid position - NaN X coordinate")
{
float nanValue = std::numeric_limits<float>::quiet_NaN();
ValidationResult result = PositionValidator::ValidateBounds(nanValue, 0.0f, 0.0f);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::InvalidPosition);
REQUIRE_FALSE(result.errorMessage.empty());
}
SECTION("Invalid position - NaN Y coordinate")
{
float nanValue = std::numeric_limits<float>::quiet_NaN();
ValidationResult result = PositionValidator::ValidateBounds(0.0f, nanValue, 0.0f);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::InvalidPosition);
}
SECTION("Invalid position - NaN Z coordinate")
{
float nanValue = std::numeric_limits<float>::quiet_NaN();
ValidationResult result = PositionValidator::ValidateBounds(0.0f, 0.0f, nanValue);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::InvalidPosition);
}
SECTION("Invalid position - Infinity X coordinate")
{
float infValue = std::numeric_limits<float>::infinity();
ValidationResult result = PositionValidator::ValidateBounds(infValue, 0.0f, 0.0f);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::OutOfBounds);
}
SECTION("Invalid position - Negative infinity")
{
float negInfValue = -std::numeric_limits<float>::infinity();
ValidationResult result = PositionValidator::ValidateBounds(negInfValue, 0.0f, 0.0f);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::OutOfBounds);
}
SECTION("Invalid position - extremely large coordinates")
{
ValidationResult result = PositionValidator::ValidateBounds(1000000.0f, 1000000.0f, 100000.0f);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::OutOfBounds);
}
SECTION("Invalid position - extremely small coordinates")
{
ValidationResult result = PositionValidator::ValidateBounds(-1000000.0f, -1000000.0f, -100000.0f);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::OutOfBounds);
}
}
TEST_CASE("Phase 1 Integration - PositionValidator Map ID", "[BotMovement][Integration][Phase1][Validation]")
{
SECTION("Valid map ID 0 (Eastern Kingdoms)")
{
ValidationResult result = PositionValidator::ValidateMapId(0);
REQUIRE(result.isValid == true);
REQUIRE(result.failureReason == ValidationFailureReason::None);
}
SECTION("Valid map ID 1 (Kalimdor)")
{
ValidationResult result = PositionValidator::ValidateMapId(1);
REQUIRE(result.isValid == true);
REQUIRE(result.failureReason == ValidationFailureReason::None);
}
SECTION("Invalid map ID - out of range")
{
ValidationResult result = PositionValidator::ValidateMapId(999999);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::InvalidMapId);
REQUIRE_FALSE(result.errorMessage.empty());
}
}
TEST_CASE("Phase 1 Integration - PositionValidator Combined Validation", "[BotMovement][Integration][Phase1][Validation]")
{
SECTION("Valid position on valid map")
{
Position pos(0.0f, 0.0f, 0.0f);
ValidationResult result = PositionValidator::ValidatePosition(0, pos);
REQUIRE(result.isValid == true);
REQUIRE(result.failureReason == ValidationFailureReason::None);
}
SECTION("Invalid position on valid map - NaN coordinates")
{
float nanValue = std::numeric_limits<float>::quiet_NaN();
ValidationResult result = PositionValidator::ValidatePosition(0, nanValue, 0.0f, 0.0f);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::InvalidPosition);
}
SECTION("Valid position on invalid map")
{
Position pos(0.0f, 0.0f, 0.0f);
ValidationResult result = PositionValidator::ValidatePosition(999999, pos);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::InvalidMapId);
}
SECTION("Invalid position on invalid map - both checks fail")
{
float nanValue = std::numeric_limits<float>::quiet_NaN();
ValidationResult result = PositionValidator::ValidatePosition(999999, nanValue, 0.0f, 0.0f);
REQUIRE(result.isValid == false);
REQUIRE((result.failureReason == ValidationFailureReason::InvalidPosition ||
result.failureReason == ValidationFailureReason::InvalidMapId));
}
}
TEST_CASE("Phase 1 Integration - ValidationResult Structure", "[BotMovement][Integration][Phase1][Validation]")
{
SECTION("Success factory method")
{
ValidationResult result = ValidationResult::Success();
REQUIRE(result.isValid == true);
REQUIRE(result.failureReason == ValidationFailureReason::None);
REQUIRE(result.errorMessage.empty());
}
SECTION("Failure factory method")
{
ValidationResult result = ValidationResult::Failure(
ValidationFailureReason::CollisionDetected,
"Wall collision detected"
);
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::CollisionDetected);
REQUIRE(result.errorMessage == "Wall collision detected");
}
SECTION("Default constructor")
{
ValidationResult result;
REQUIRE(result.isValid == false);
REQUIRE(result.failureReason == ValidationFailureReason::None);
}
}
TEST_CASE("Phase 1 Integration - BotMovementDefines Enums", "[BotMovement][Integration][Phase1]")
{
SECTION("MovementStateType values are distinct")
{
REQUIRE(MovementStateType::Idle != MovementStateType::Ground);
REQUIRE(MovementStateType::Ground != MovementStateType::Swimming);
REQUIRE(MovementStateType::Swimming != MovementStateType::Flying);
REQUIRE(MovementStateType::Flying != MovementStateType::Falling);
REQUIRE(MovementStateType::Falling != MovementStateType::Stuck);
}
SECTION("ValidationFailureReason values are distinct")
{
REQUIRE(ValidationFailureReason::None != ValidationFailureReason::InvalidPosition);
REQUIRE(ValidationFailureReason::InvalidPosition != ValidationFailureReason::OutOfBounds);
REQUIRE(ValidationFailureReason::OutOfBounds != ValidationFailureReason::InvalidMapId);
REQUIRE(ValidationFailureReason::CollisionDetected != ValidationFailureReason::PathBlocked);
}
SECTION("ValidationLevel values are ordered correctly")
{
REQUIRE(static_cast<uint8>(ValidationLevel::None) < static_cast<uint8>(ValidationLevel::Basic));
REQUIRE(static_cast<uint8>(ValidationLevel::Basic) < static_cast<uint8>(ValidationLevel::Standard));
REQUIRE(static_cast<uint8>(ValidationLevel::Standard) < static_cast<uint8>(ValidationLevel::Strict));
}
SECTION("RecoveryLevel values are ordered correctly")
{
REQUIRE(static_cast<uint8>(RecoveryLevel::Level1_RecalculatePath) <
static_cast<uint8>(RecoveryLevel::Level2_BackupAndRetry));
REQUIRE(static_cast<uint8>(RecoveryLevel::Level2_BackupAndRetry) <
static_cast<uint8>(RecoveryLevel::Level3_RandomNearbyPosition));
REQUIRE(static_cast<uint8>(RecoveryLevel::Level3_RandomNearbyPosition) <
static_cast<uint8>(RecoveryLevel::Level4_TeleportToSafePosition));
REQUIRE(static_cast<uint8>(RecoveryLevel::Level4_TeleportToSafePosition) <
static_cast<uint8>(RecoveryLevel::Level5_EvadeAndReset));
}
}
TEST_CASE("Phase 1 Integration - PositionSnapshot Structure", "[BotMovement][Integration][Phase1]")
{
SECTION("Default constructor initializes correctly")
{
PositionSnapshot snapshot;
REQUIRE(snapshot.timestamp == 0);
}
SECTION("Parameterized constructor")
{
Position pos(100.0f, 200.0f, 50.0f);
uint32 time = 12345;
PositionSnapshot snapshot(pos, time);
REQUIRE(snapshot.pos.GetPositionX() == 100.0f);
REQUIRE(snapshot.pos.GetPositionY() == 200.0f);
REQUIRE(snapshot.pos.GetPositionZ() == 50.0f);
REQUIRE(snapshot.timestamp == time);
}
SECTION("Copy position data correctly")
{
Position pos1(1.0f, 2.0f, 3.0f);
PositionSnapshot snapshot1(pos1, 100);
Position pos2(10.0f, 20.0f, 30.0f);
PositionSnapshot snapshot2(pos2, 200);
REQUIRE(snapshot1.pos.GetPositionX() != snapshot2.pos.GetPositionX());
REQUIRE(snapshot1.timestamp != snapshot2.timestamp);
}
}
TEST_CASE("Phase 1 Integration - Validation Pipeline Correctness", "[BotMovement][Integration][Phase1][Validation]")
{
SECTION("Sequential validation - first check passes, all checks run")
{
Position validPos(100.0f, 100.0f, 10.0f);
ValidationResult boundsResult = PositionValidator::ValidateBounds(validPos);
REQUIRE(boundsResult.isValid == true);
ValidationResult mapResult = PositionValidator::ValidateMapId(0);
REQUIRE(mapResult.isValid == true);
ValidationResult combinedResult = PositionValidator::ValidatePosition(0, validPos);
REQUIRE(combinedResult.isValid == true);
}
SECTION("Sequential validation - first check fails, error captured")
{
float nanValue = std::numeric_limits<float>::quiet_NaN();
ValidationResult boundsResult = PositionValidator::ValidateBounds(nanValue, 0.0f, 0.0f);
REQUIRE(boundsResult.isValid == false);
REQUIRE(boundsResult.failureReason == ValidationFailureReason::InvalidPosition);
ValidationResult combinedResult = PositionValidator::ValidatePosition(0, nanValue, 0.0f, 0.0f);
REQUIRE(combinedResult.isValid == false);
REQUIRE(combinedResult.failureReason == ValidationFailureReason::InvalidPosition);
}
SECTION("Multiple validation failures - first failure is reported")
{
float nanValue = std::numeric_limits<float>::quiet_NaN();
uint32 invalidMapId = 999999;
ValidationResult result = PositionValidator::ValidatePosition(invalidMapId, nanValue, 0.0f, 0.0f);
REQUIRE(result.isValid == false);
}
}
TEST_CASE("Phase 1 Integration - Config and Manager Integration", "[BotMovement][Integration][Phase1]")
{
SECTION("Manager config can be reloaded")
{
BotMovementManager* manager = sBotMovementManager;
REQUIRE(manager != nullptr);
REQUIRE_NOTHROW(manager->ReloadConfig());
BotMovementConfig const& config = manager->GetConfig();
REQUIRE(config.GetValidationLevel() != ValidationLevel::None);
}
SECTION("Manager metrics are accessible")
{
BotMovementManager* manager = sBotMovementManager;
REQUIRE(manager != nullptr);
REQUIRE_NOTHROW(manager->GetGlobalMetrics());
}
SECTION("Manager metrics can be reset")
{
BotMovementManager* manager = sBotMovementManager;
REQUIRE(manager != nullptr);
REQUIRE_NOTHROW(manager->ResetMetrics());
}
}
TEST_CASE("Phase 1 Integration - Edge Cases and Boundary Conditions", "[BotMovement][Integration][Phase1][Validation]")
{
SECTION("Position at origin is valid")
{
Position origin(0.0f, 0.0f, 0.0f);
ValidationResult result = PositionValidator::ValidateBounds(origin);
REQUIRE(result.isValid == true);
}
SECTION("Position with very small positive values")
{
Position pos(0.001f, 0.001f, 0.001f);
ValidationResult result = PositionValidator::ValidateBounds(pos);
REQUIRE(result.isValid == true);
}
SECTION("Position with very small negative values")
{
Position pos(-0.001f, -0.001f, -0.001f);
ValidationResult result = PositionValidator::ValidateBounds(pos);
REQUIRE(result.isValid == true);
}
SECTION("Position with mixed positive and negative coordinates")
{
Position pos(-100.0f, 100.0f, -50.0f);
ValidationResult result = PositionValidator::ValidateBounds(pos);
REQUIRE(result.isValid == true);
}
SECTION("Large but valid coordinates")
{
Position pos(10000.0f, 10000.0f, 1000.0f);
ValidationResult result = PositionValidator::ValidateBounds(pos);
REQUIRE(result.isValid == true);
}
SECTION("Z coordinate at extremes")
{
Position highZ(0.0f, 0.0f, 5000.0f);
ValidationResult result = PositionValidator::ValidateBounds(highZ);
REQUIRE(result.isValid == true);
}
}
TEST_CASE("Phase 1 Integration - Error Message Quality", "[BotMovement][Integration][Phase1][Validation]")
{
SECTION("Invalid position error has descriptive message")
{
float nanValue = std::numeric_limits<float>::quiet_NaN();
ValidationResult result = PositionValidator::ValidateBounds(nanValue, 0.0f, 0.0f);
REQUIRE(result.isValid == false);
REQUIRE_FALSE(result.errorMessage.empty());
REQUIRE(result.errorMessage.length() > 10);
}
SECTION("Out of bounds error has descriptive message")
{
ValidationResult result = PositionValidator::ValidateBounds(1000000.0f, 0.0f, 0.0f);
REQUIRE(result.isValid == false);
REQUIRE_FALSE(result.errorMessage.empty());
REQUIRE(result.errorMessage.length() > 10);
}
SECTION("Invalid map ID error has descriptive message")
{
ValidationResult result = PositionValidator::ValidateMapId(999999);
REQUIRE(result.isValid == false);
REQUIRE_FALSE(result.errorMessage.empty());
REQUIRE(result.errorMessage.length() > 10);
}
}