Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
770819b
feat: Implement Detour navigation mesh manager and related collision …
HKevinH May 30, 2026
0629b5f
Add VMapManager2 and WorldModelRuntime for enhanced map collision han…
HKevinH May 30, 2026
e54524b
feat(collision): enhance pathfinding with improved query filtering an…
HKevinH May 30, 2026
08e227c
fix(mmap_generator): update file path handling and include DetourNavM…
HKevinH May 30, 2026
d65d970
refactor(mmap_generator): enhance SaveNavMeshTile function and update…
HKevinH May 30, 2026
d8385a2
feat(mmap_generator): enhance terrain data loading and add progress r…
HKevinH May 30, 2026
39d9451
feat(mmap_generator): enhance terrain data handling and improve tile …
HKevinH May 30, 2026
5c2762d
feat(mmap_generator): add logging for existing terrain tiles in Gener…
HKevinH May 30, 2026
15384e0
refactor(mmap_generator): remove MmapGenerator.cpp file and associate…
HKevinH May 30, 2026
b50972f
feat(extractors): add locale support for DBC and map extraction tasks
HKevinH May 30, 2026
20d3018
feat(CommandService): add .mmap command for navmesh pathfinding and v…
HKevinH May 30, 2026
12b22b9
feat(combat): implement GM mode for creature aggro immunity and enhan…
HKevinH May 30, 2026
c0a98b0
feat(logging): enhance MMAP logging for collision service and navmesh…
HKevinH May 30, 2026
60e3b4f
feat(collision): enhance navmesh functionality with loaded map and ti…
HKevinH May 30, 2026
e17907b
feat(logging): implement MMAP-specific logging enhancements across se…
HKevinH May 30, 2026
aef96f9
feat(logging): add MMAP logging configuration options and enhance mma…
HKevinH May 30, 2026
06afc8c
feat(mmap): enhance tile generation with batch progress tracking
HKevinH May 30, 2026
a411361
feat(mmap): add mmap marker allocation and enhance navmesh tile proce…
HKevinH May 30, 2026
3643104
fix(mmap): correct tile coordinate convention and ground-clamp creatu…
May 30, 2026
d166c09
chore(mmap): add chase-Z debug logs to trace airborne creature follow
May 30, 2026
7589f45
fix(mmap): pick the floor under (x,y), not the 3D-nearest poly
May 30, 2026
ec8e2aa
fix(chase): reject navmesh ground when it would teleport creature ver…
May 30, 2026
1656be9
fix(char-enum): resolve displayId from DBC when item_template has a z…
May 30, 2026
f51a82d
fix(chase): skip navmesh waypoints that lead away from the target
May 30, 2026
f9347b0
fix(trinitycore): remove TrinityCore submodule reference
May 30, 2026
6eaedab
fix(mmap-command): make .mmap path markers visible above the floor
May 31, 2026
33b2eba
fix(chase): stabilize path so the NPC stops oscillating toward the pl…
May 31, 2026
c06c3c5
fix(mmap): silence tile-missing noise for tiles that have no .map source
May 31, 2026
ce0894b
Merge branch 'FirelandsProject:master' into master
HKevinH Jun 1, 2026
92b3606
feat(navigation): enhance ground snapping and collision handling in c…
HKevinH Jun 1, 2026
64631c7
feat(commands): implement MySQL command definition repository and int…
HKevinH Jun 1, 2026
4ab6359
fix(mmap): update agentMaxSlope to match WoW standards for walkable t…
HKevinH Jun 1, 2026
9d8fec0
feat(commands): update command permissions to use required_permission…
HKevinH Jun 2, 2026
9bf12ad
Merge branch 'FirelandsProject:master' into master
HKevinH Jun 2, 2026
321d778
Merge branch 'FirelandsProject:master' into master
HKevinH Jun 3, 2026
b35518b
fix(commands): correct firelands_commands seed for speed, cd, faction…
HKevinH Jun 5, 2026
a2436fc
refactor(commands): extract .mmap into MmapDebugCommands
HKevinH Jun 5, 2026
46cd40a
fix(logger): only create the mmap log when a path is configured
HKevinH Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.10)
project(Firelands CXX)
project(Firelands CXX C)

# --- Policies ---
if(POLICY CMP0135)
Expand All @@ -13,6 +13,8 @@ set(CMAKE_POLICY_VERSION_MINIMUM 3.10)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# spdlog 1.14.x bundled fmt breaks under C++20 (FMT_STRING / consteval on Apple Clang).
Expand Down Expand Up @@ -132,11 +134,27 @@ FetchContent_MakeAvailable(json)
FetchContent_Declare(yaml-cpp URL https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.zip)
set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(yaml-cpp)
# yaml-cpp 0.8.0 missing <cstdint> with C++20 – force include it
target_compile_options(yaml-cpp PRIVATE -include cstdint)

# Lua
FetchContent_Declare(lua_cmake URL https://github.com/walterschell/Lua/archive/refs/tags/v5.4.7.zip)
FetchContent_MakeAvailable(lua_cmake)

# RecastNavigation (Recast + Detour for navmesh pathfinding)
FetchContent_Declare(recastnavigation
GIT_REPOSITORY https://github.com/recastnavigation/recastnavigation.git
GIT_TAG main
)
set(RECASTNAVIGATION_DEMO OFF CACHE BOOL "" FORCE)
set(RECASTNAVIGATION_TESTS OFF CACHE BOOL "" FORCE)
set(RECASTNAVIGATION_STATIC ON CACHE BOOL "" FORCE)
set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(recastnavigation)
# RecastNavigation defaults to C++98 which breaks with GCC 16 <cstdint>.
# Force C++17 for Recast (it's a static lib, no ABI concerns).
set_target_properties(Recast PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)

# MariaDB Connector/C++
FetchContent_Declare(mariadb-connector-cpp URL https://github.com/mariadb-corporation/mariadb-connector-cpp/archive/refs/tags/1.1.7.zip)
FetchContent_Populate(mariadb-connector-cpp)
Expand All @@ -148,6 +166,8 @@ set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
set(BUILD_TESTS_ONLY OFF CACHE BOOL "" FORCE)
include_directories(SYSTEM ${mariadb-connector-c_SOURCE_DIR}/include ${mariadb-connector-c_BINARY_DIR}/include)
add_subdirectory(${mariadb-connector-cpp_SOURCE_DIR} ${mariadb-connector-cpp_BINARY_DIR})
# MariaDB Connector/C++ 1.1.7 missing <cstdint> with new GCC (C++ only)
target_compile_options(mariadbcpp_obj PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-include> $<$<COMPILE_LANGUAGE:CXX>:cstdint>)

if(APPLE)
set(MARIADB_EXTRA_LIBS "-framework CoreFoundation -framework Security")
Expand Down Expand Up @@ -186,7 +206,9 @@ target_link_libraries(auth
PRIVATE OpenSSL::SSL
PRIVATE OpenSSL::Crypto
PRIVATE Boost::thread
PRIVATE mswsock
)
target_link_options(auth PRIVATE -static-libstdc++ -static-libgcc)

# WORLD SERVER
add_executable(world
Expand All @@ -207,7 +229,9 @@ target_link_libraries(world
PRIVATE OpenSSL::SSL
PRIVATE OpenSSL::Crypto
PRIVATE Boost::thread
PRIVATE mswsock
)
target_link_options(world PRIVATE -static-libstdc++ -static-libgcc)

# DEVTOOLS CLI
add_executable(FirelandsDevTools
Expand Down
69 changes: 69 additions & 0 deletions sql/migrations/71_world_firelands_commands.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
USE `firelands_world`;

DROP TABLE IF EXISTS `firelands_commands`;
CREATE TABLE `firelands_commands` (
`name` varchar(64) NOT NULL,
`description` varchar(255) NOT NULL DEFAULT '',
`syntax` varchar(255) NOT NULL DEFAULT '',
`required_permission_mask` bigint unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Permission mask values match shared/game/Permissions.h
-- 0 = anyone, otherwise must match GetAccountRolePermissionMask()

INSERT INTO `firelands_commands` (`name`, `description`, `syntax`, `required_permission_mask`) VALUES
-- Anyone (mask=0)
('help', 'Show available commands', '.help', 0),
('commands', 'Show available commands (alias)', '.commands', 0),

-- GPS / Position (mask=1 = CommandGps)
('gps', 'Show current position and map', '.gps', 1),
('mmap', 'Navmesh pathfinding info and visual markers', '.mmap [x y z [mapId]] | .mmap clear', 1),

-- Mailbox (mask=512 = CommandMailbox)
('email', 'Open mailbox anywhere', '.email', 512),

-- Teleport (mask=2 = CommandTeleport)
('tele', 'Teleport to coordinates or location name', '.tele <x> <y> <z> [mapId]', 2),

-- GM Tools (mask=64 = CommandGmTools)
('gm', 'Toggle GM mode on/off (NPCs ignore you)', '.gm [on|off]', 64),
('dnd', 'Toggle Do Not Disturb tag', '.dnd [on|off]', 64),
('dev', 'Toggle Developer tag', '.dev [on|off]', 64),
('visible', 'Toggle GM visibility to players', '.visible [on|off]', 64),
('fly', 'Toggle fly mode', '.fly [on|off]', 64),
('speed', 'Set run and flight speed (also affects fly)', '.speed <number> | .speed reset (default 7)', 64),

-- Manage Players (mask=8)
('online', 'List online players', '.online', 8),
('announce', 'Send server-wide announcement', '.announce <message>', 8),
('kick', 'Kick a player from the server', '.kick <playerName> [reason]', 8),
('goto', 'Teleport to a player', '.goto <playerName>', 8),
('appear', 'Teleport to a player (alias)', '.appear <playerName>', 8),
('summon', 'Summon a player to your location', '.summon <playerName>', 8),

-- Gameplay (mask=128 = CommandGameplay)
('learn', 'Learn a spell by ID', '.learn <spellId> [all]', 128),
('unlearn', 'Unlearn a spell by ID', '.unlearn <spellId> [all]', 128),
('money', 'Modify money (copper)', '.money <copper>', 128),
('additem', 'Add item to inventory', '.additem <itemId> [count]', 128),
('delitem', 'Delete item from inventory', '.delitem <itemId> [count]', 128),
('level', 'Set character level', '.level <value>', 128),
('cd', 'Reset all spell cooldowns (including racials)', '.cd', 128),
('damage', 'Deal damage to targeted creature', '.damage <amount>', 128),
('revive', 'Revive yourself or targeted player', '.revive', 128),
('faction', 'Force faction reaction rank or set faction template', '.faction forced set <id> <rank0-7> | forced clear <id> | forced clearall | template self|target <tpl>', 128),

-- GM Tickets (mask=256 = ManageGmTickets)
('ticket', 'Manage GM tickets', '.ticket queue|mine|ui|take <id>|reply <id> <msg>|close <id>', 256),

-- Accounts (mask=16 = ManageAccounts)
('account', 'Manage accounts (console only)', '.account create|delete|setaccess', 16),
('ban', 'Ban an account (console only)', '.ban <accountName> [reason]', 16),
('unban', 'Unban an account (console only)', '.unban <accountName>', 16),
('rbac', 'RBAC role management (console only)', '.rbac setstaff|grant|revoke|show', 16),

-- Server Control (mask=32)
('server', 'Server control commands', '.server shutdown|restart <seconds>', 32),
('npc', 'NPC management', '.npc spawn|delete|info <entry>', 32);
1 change: 1 addition & 0 deletions src/application/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ add_library(FirelandsApplication STATIC
services/CharacterActionButtons.cpp
services/RealmListService.cpp
services/CommandService.cpp
services/MmapDebugCommands.cpp
services/GmTicketService.cpp
services/OnlineCharacterSessionRegistry.cpp
services/MapService.cpp
Expand Down
3 changes: 2 additions & 1 deletion src/application/combat/CombatHostility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ bool CanMeleeAttack(Firelands::Player const &attacker, Firelands::Player const &

bool CanMeleeAttack(Firelands::Player const &attacker, Firelands::Creature const &target,
Firelands::FactionTemplateDbc const *factionTemplates) {
(void)attacker;
if (attacker.IsGmModeEnabled())
return false;
if (target.IsEvading())
return false;
if (!factionTemplates || !factionTemplates->IsLoaded())
Expand Down
168 changes: 168 additions & 0 deletions src/application/combat/CreatureChaseMovement.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
#include "CreatureChaseMovement.h"

#include <shared/network/MovementFlags.h>
#include <shared/network/MovementStateQueries.h>
#include <application/ports/IMapCollisionQueries.h>
#include <shared/Logger.h>
#include <cmath>

using Firelands::MOVEMENTFLAG_FORWARD;
using Firelands::MOVEMENTFLAG_NONE;
using Firelands::MovementInfo;
using Firelands::Vec3;

namespace application::combat {

Expand Down Expand Up @@ -124,4 +128,168 @@ bool ChaseTargetRelocated(float lastX, float lastY, float lastZ, float newX, flo
return (dx * dx + dy * dy + dz * dz) > thresholdSq;
}

std::vector<Vec3> ComputeNavMeshPath(uint32_t mapId,
MovementInfo const &start, float targetX,
float targetY, float targetZ,
Firelands::IMapCollisionQueries const *collision) {
std::vector<Vec3> waypoints;
if (!collision) {
LOG_DEBUG("CHASE navmesh path skipped: mapId={} no collision service", mapId);
return waypoints;
}

Firelands::FindPathRequest req;
req.mapId = mapId;
req.startX = start.x;
req.startY = start.y;
req.startZ = start.z;
req.endX = targetX;
req.endY = targetY;
req.endZ = targetZ;
req.smoothPath = true;
req.allowPartialPath = true;

auto result = collision->FindPath(req);
if (result.status == Firelands::FindPathStatus::Complete ||
result.status == Firelands::FindPathStatus::Partial) {
waypoints = std::move(result.waypoints);
} else {
LOG_DEBUG("CHASE navmesh path failed: mapId={} status={} start=({}, {}, {}) end=({}, {}, {})",
mapId, static_cast<int>(result.status), start.x, start.y, start.z,
targetX, targetY, targetZ);
}
return waypoints;
}

CreatureChaseStepResult StepCreatureAlongNavMeshPath(
MovementInfo const &current, float targetX, float targetY, float targetZ,
float deltaSeconds, CreatureChaseConfig const &config,
ChaseNavMeshState &state,
Firelands::IMapCollisionQueries const *collision,
uint32_t mapId) {
// Set the Z value of the result to the navmesh floor on every tick. Blending with
// a delay (zBlendPerTick) toward the waypoint’s Z value causes the creature to
// float on slopes (when climbing slowly) or sink into/pass through the slope (the Z value doesn’t rise
// as fast as the terrain). Solving the Z value against the actual ground eliminates both
// problems, and the creature always stays attached to the terrain.
// Only ground creatures get pinned to the floor. Airborne ones (flying /
// no-gravity / hover) keep the Z the step produced, so a flyer isn't yanked to
// the terrain while chasing. Today ground creatures carry no airborne flags, so
// this is inert for them and behaves exactly as before.
bool const airborne = Firelands::MovementIsAirborneTier(current);
auto snapToGround = [&](CreatureChaseStepResult res) {
if (collision && res.moved && !airborne) {
float const ground = collision->GetHeight(mapId, res.position.x,
res.position.y, res.position.z);
if (std::isfinite(ground))
res.position.z = ground;
}
return res;
};

// 3y threshold so the corridor persists across ticks while the player is
// walking. With the default 0.5y the path tore down and rebuilt every
// single tick, which on tiles with ghost polys made findNearestPoly
// alternate between bad start projections and the NPC oscillated.
constexpr float kChaseReplanThresholdYards = 3.0f;
bool const targetRelocated =
ChaseTargetRelocated(state.lastTargetX, state.lastTargetY,
state.lastTargetZ, targetX, targetY, targetZ,
kChaseReplanThresholdYards);

if (targetRelocated || state.waypoints.empty()) {
state.lastTargetX = targetX;
state.lastTargetY = targetY;
state.lastTargetZ = targetZ;
state.currentWaypoint = 0;
state.waypoints.clear();
}

// Short-range bypass: when the target is well within engagement distance,
// skip the navmesh corridor entirely. It rarely adds value and is the case
// most vulnerable to ghost-poly start projections, which is what makes the
// NPC walk toward the player, turn around, replan, and come back.
constexpr float kDirectChaseRangeYards = 8.0f;
float const directDxFast = targetX - current.x;
float const directDyFast = targetY - current.y;
float const directDistSqFast =
directDxFast * directDxFast + directDyFast * directDyFast;
if (directDistSqFast <=
kDirectChaseRangeYards * kDirectChaseRangeYards) {
state.waypoints.clear();
state.currentWaypoint = 0;
return snapToGround(StepCreatureTowardTarget(current, targetX, targetY,
targetZ, deltaSeconds, config));
}

if (targetRelocated && collision) {
state.waypoints = ComputeNavMeshPath(mapId, current, targetX, targetY, targetZ, collision);
if (!state.waypoints.empty()) {
state.waypoints.push_back(Vec3{targetX, targetY, targetZ});
state.currentWaypoint = 0;
LOG_DEBUG("CHASE navmesh path ready: mapId={} waypointCount={} current=({}, {}, {}) target=({}, {}, {})",
mapId, state.waypoints.size(), current.x, current.y, current.z,
targetX, targetY, targetZ);
} else {
LOG_DEBUG("CHASE navmesh path empty: mapId={} current=({}, {}, {}) target=({}, {}, {}) fallback=straight-line",
mapId, current.x, current.y, current.z, targetX, targetY, targetZ);
}
}

if (state.waypoints.empty() || state.currentWaypoint >= state.waypoints.size()) {
if (!collision) {
LOG_DEBUG("CHASE fallback without collision: mapId={} current=({}, {}, {}) target=({}, {}, {})",
mapId, current.x, current.y, current.z, targetX, targetY, targetZ);
}
return snapToGround(StepCreatureTowardTarget(current, targetX, targetY,
targetZ, deltaSeconds, config));
}

float const directDx = targetX - current.x;
float const directDy = targetY - current.y;
float const directDistSq = directDx * directDx + directDy * directDy;

while (state.currentWaypoint < state.waypoints.size()) {
Vec3 const &wp = state.waypoints[state.currentWaypoint];
float const dx = current.x - wp.x;
float const dy = current.y - wp.y;
float const distSq = dx * dx + dy * dy;

if (distSq < 0.25f) {
++state.currentWaypoint;
LOG_TRACE("CHASE waypoint reached: mapId={} waypointIndex={} pos=({}, {}, {})",
mapId, state.currentWaypoint - 1, wp.x, wp.y, wp.z);
continue;
}

// Skip a waypoint that goes the wrong way. Detour's findStraightPath can
// emit a start-projection point far from the actual creature when
// findNearestPoly picks a stale/corrupted poly; following it sends the NPC
// *away* from the player. "Wrong way" = the vector from current to wp
// points away from the target, AND wp is farther from the target than
// current already is.
float const wpToTargetX = targetX - wp.x;
float const wpToTargetY = targetY - wp.y;
float const wpToTargetDistSq =
wpToTargetX * wpToTargetX + wpToTargetY * wpToTargetY;
float const wpFromCurrentDot =
(wp.x - current.x) * directDx + (wp.y - current.y) * directDy;
if (wpFromCurrentDot < 0.f && wpToTargetDistSq > directDistSq) {
LOG_DEBUG(
"CHASE skipping wrong-way waypoint: mapId={} waypointIndex={} "
"wp=({}, {}, {}) current=({}, {}, {}) target=({}, {})",
mapId, state.currentWaypoint, wp.x, wp.y, wp.z, current.x, current.y,
current.z, targetX, targetY);
++state.currentWaypoint;
continue;
}

auto step = StepCreatureTowardTarget(current, wp.x, wp.y, wp.z, deltaSeconds, config);
return snapToGround(step);
}

return snapToGround(StepCreatureTowardTarget(current, targetX, targetY,
targetZ, deltaSeconds, config));
}

} // namespace application::combat
Loading