From dbffd4b596132388e7607391d67386ce02694960 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 13 Mar 2026 15:12:34 +0200 Subject: [PATCH 01/16] Fix BasicVector axis typo Bug from 2a78129e which breaks polyhedron math --- src/public/mathlib/polyhedron.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/public/mathlib/polyhedron.h b/src/public/mathlib/polyhedron.h index 22859167fb..1ce75a4434 100644 --- a/src/public/mathlib/polyhedron.h +++ b/src/public/mathlib/polyhedron.h @@ -26,8 +26,8 @@ struct BasicVector { BasicVector res; res.x = x + v.x; - res.x = y + v.y; - res.x = z + v.z; + res.y = y + v.y; + res.z = z + v.z; return res; } @@ -35,8 +35,8 @@ struct BasicVector { BasicVector res; res.x = x - v.x; - res.x = y - v.y; - res.x = z - v.z; + res.y = y - v.y; + res.z = z - v.z; return res; } @@ -44,8 +44,8 @@ struct BasicVector { BasicVector res; res.x = x * v; - res.x = y * v; - res.x = z * v; + res.y = y * v; + res.z = z * v; return res; } From 777e3ee3346efc33d8b8b67ce5c848d54e835b73 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 13 Mar 2026 13:47:51 +0200 Subject: [PATCH 02/16] wip --- src/game/server/nav_generate.cpp | 22 ++ src/game/server/nav_mesh.cpp | 465 ++++++++++++++++++++++++++++++ src/game/server/nav_mesh.h | 4 + src/game/server/ndebugoverlay.cpp | 11 +- src/game/server/ndebugoverlay.h | 4 + 5 files changed, 505 insertions(+), 1 deletion(-) diff --git a/src/game/server/nav_generate.cpp b/src/game/server/nav_generate.cpp index 4c4e7f8925..df9e256abb 100644 --- a/src/game/server/nav_generate.cpp +++ b/src/game/server/nav_generate.cpp @@ -3425,6 +3425,16 @@ void CNavMesh::AddWalkableSeeds( void ) */ void CNavMesh::BeginGeneration( bool incremental ) { + if (!BuildBrushLaddersFromBsp()) + { + Warning("Generating brush ladders...FAIL\n"); + } + else + { + Msg( "Generating brush ladders...DONE\n" ); + } + return; + IGameEvent *event = gameeventmanager->CreateEvent( "nav_generate" ); if ( event ) { @@ -4018,6 +4028,18 @@ bool CNavMesh::UpdateGeneration( float maxTime ) m_isAnalyzed = true; } +#if 0 //def NEO + // Post-gen because we want automatic merge with the final navmesh + if (!BuildBrushLaddersFromBsp()) + { + Warning("Generating brush ladders...FAIL\n"); + } + else + { + Msg( "Generating brush ladders...DONE\n" ); + } +#endif + // generation complete! float generationTime = Plat_FloatTime() - m_generationStartTime; Msg( "Generation complete! %0.1f seconds elapsed.\n", generationTime ); diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index 299bf76ff3..15d220ba44 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -26,6 +26,12 @@ #include "tf/nav_mesh/tf_nav_area.h" #endif +#ifdef NEO +#include "mathlib/polyhedron.h" +#include "nav.h" +#include "polylib.h" +#endif + #ifdef NEXT_BOT #include "NextBot/NavMeshEntities/func_nav_prerequisite.h" #endif @@ -3020,6 +3026,465 @@ void CNavMesh::DestroyLadders( void ) m_selectedLadder = NULL; } +#ifdef NEO +namespace Neo +{ + struct MockBspHdr { + decltype(dheader_t::ident) ident; + decltype(dheader_t::version) version; + }; + + // Sanity check BSP header, version, and endianness compatibility. + // Has the side-effect of reading sizeof(Neo::MockBspHdr) into the file handle. + static bool ValidateBspHeader(FileHandle_t f) + { + static_assert(VALVE_LITTLE_ENDIAN); + + MockBspHdr hdr; + filesystem->Read(&hdr, sizeof(hdr), f); + if (hdr.ident != IDBSPHEADER) + { + CByteswap().SwapBuffer(&hdr.ident, &hdr.ident); + if (hdr.ident != IDBSPHEADER) + { + Warning("%s: invalid BSP file ident 0x%x (expected 0x%x)\n", + __FUNCTION__, hdr.ident, IDBSPHEADER); + return false; + } + else + { + Warning("%s: BSP had unsupported endianness (expected %s, was %s)\n", + __FUNCTION__, + VALVE_LITTLE_ENDIAN ? "little" : "big", + VALVE_LITTLE_ENDIAN ? "big" : "little"); + return false; + } + } + else if (hdr.version < MINBSPVERSION || hdr.version > BSPVERSION) + { + Warning("%s: unsupported BSP file version %d (expected in range %d-%d)\n", + __FUNCTION__, hdr.version, MINBSPVERSION, BSPVERSION); + return false; + } + + return true; + } + + constexpr static auto LumpHdrOffs(GameLumpId_t id) + { + auto lumpBegin = _hacky_datamap_offsetof(dheader_t, lumps); + return narrow_cast(lumpBegin + id * sizeof(lump_t)); + } + + [[nodiscard]] static bool ReadLumpHdr(GameLumpId_t id, FileHandle_t f, lump_t* out) + { + constexpr auto lumpSize = sizeof(std::remove_pointer_t); + static_assert(lumpSize > 0); + + Assert(filesystem); + Assert(f); + + const auto seekOffs = LumpHdrOffs(id); + if (seekOffs < 0) + { + Assert(false); + return false; + } + else if (seekOffs > filesystem->Size(f) + lumpSize) + { + Assert(false); + return false; + } + + filesystem->Seek(f, LumpHdrOffs(id), FILESYSTEM_SEEK_HEAD); + + const auto totalRead = filesystem->Read(out, lumpSize, f); + const bool ok = (totalRead == lumpSize); + Assert(ok); + return ok; + } + + template + [[nodiscard]] static bool ReadLump(GameLumpId_t id, FileHandle_t f, T* out, + int startLump = 0, int numLumpsToRead = 1, const lump_t* lump = {}) + { + if (numLumpsToRead == 0) + { + return true; // if they want to read nothing, that's fine I suppose + } + else + { + Assert(filesystem); + } + + if (numLumpsToRead < 0) + { + Warning("%s: Cannot read negative amount of lumps (%d)\n", __FUNCTION__, numLumpsToRead); + return false; + } + if (startLump < 0) + { + Warning("%s: Lump start position cannot be negative\n", __FUNCTION__); + return false; + } + if (!f) + { + Warning("%s: Invalid file handle\n", __FUNCTION__); + return false; + } + + lump_t hdr; + if (!ReadLumpHdr(id, f, &hdr)) + { + Warning("%s: Failed to read BSP header for lump %d\n", __FUNCTION__, id); + return false; + } + if (hdr.fileofs < 0) + { + Warning("%s: Lump hdr reported negative target lump offset %d\n", __FUNCTION__, hdr.fileofs); + return false; + } + if (hdr.filelen < 0) + { + Warning("%s: File hdr reported negative target lump length %d\n", __FUNCTION__, hdr.filelen); + return false; + } + + const auto fileTargetLumpOffset = hdr.fileofs + startLump * sizeof(T); + if (fileTargetLumpOffset <= 0) + { + Warning("%s: Expected a positive target lump offset for lump %d but got %zu\n", __FUNCTION__, id, fileTargetLumpOffset); + return false; + } + else if (fileTargetLumpOffset > std::numeric_limits::max()) + { + Warning("%s: Reading starting to read lumpid(s) of type %d from index %d would require lump offset of %zu which is >max: %d\n", + __FUNCTION__, id, fileTargetLumpOffset, std::numeric_limits::max()); + return false; + } + + const auto numLumpsAvailable = hdr.filelen / sizeof(T); + Assert(numLumpsAvailable >= 0); + + if (numLumpsToRead > numLumpsAvailable) + { + Warning("%s: Requested %d lump(s) of type %d but only %zu such lump(s) available\n", + __FUNCTION__, numLumpsToRead, id, numLumpsAvailable); + return false; + } + + const auto fileSize = filesystem->Size(f); + const auto sizeToRead = sizeof(T) * numLumpsToRead; + if (sizeToRead > std::numeric_limits::max()) + { + Warning("%s: Reading %d lumps of lumpid %d would require %zu bytes which >max: %d\n", + __FUNCTION__, numLumpsToRead, id, sizeToRead, std::numeric_limits::max()); + return false; + } + + const auto requiredSize = fileTargetLumpOffset + sizeToRead; + Assert(fileSize > 0); + Assert(requiredSize > 0); + if (fileSize < requiredSize) + { + Warning("%s: File size (%d bytes) not large enough to process %d lump(s) offset for lumpid %d (%zu bytes)\n", + __FUNCTION__, fileSize, numLumpsToRead, id, requiredSize); + return false; + } + + filesystem->Seek(f, narrow_cast(fileTargetLumpOffset), FILESYSTEM_SEEK_HEAD); + + const auto bytesReadTotal = filesystem->Read(out, narrow_cast(sizeToRead), f); + if (bytesReadTotal != sizeToRead) + { + Warning("%s: Expected to read %d bytes from %d lump(s) of type %d, but read %d bytes\n", + __FUNCTION__, sizeToRead, numLumpsToRead, id, bytesReadTotal); + return false; + } + + return true; + } + + template + [[nodiscard]] static bool ReadLump(GameLumpId_t id, FileHandle_t f, T& out, + int startLump = 0, const lump_t* lump = {}) + { + return ReadLump(id, f, &out, startLump, 1, lump); + } + + [[nodiscard]] static bool ReadBrushes(FileHandle_t f, CUtlVector& out) + { + lump_t brushHdr; + if (!ReadLumpHdr(LUMP_BRUSHES, f, &brushHdr)) + { + Warning("%s: failed to read brush lump hdr\n", __FUNCTION__); + return false; + } + const auto mapBrushCount = narrow_cast(brushHdr.filelen / sizeof(dbrush_t)); + if (mapBrushCount <= 0) + { + Warning("%s: could not find any brushes for bsp\n", __FUNCTION__); + return false; + } + else if (mapBrushCount > MAX_MAP_BRUSHES) + { + Warning("%s: map reported to contain %d brushes but max is %d\n", + __FUNCTION__, mapBrushCount, MAX_MAP_BRUSHES); + return false; + } + + out.EnsureCapacity(mapBrushCount); + dbrush_t brush; + for (int i = 0; i < mapBrushCount; ++i) + { + if (!ReadLump(LUMP_BRUSHES, f, brush, i, &brushHdr)) + { + Warning("%s: failed to read brush %d lump contents\n", __FUNCTION__, i); + return false; + } + + if (!(brush.contents & CONTENTS_LADDER)) + { + continue; + } + + if (brush.numsides < 4) + { + Warning("%s ladder brush %d reported %d numsides which is insufficient to construct a polyhedron\n", + __FUNCTION__, i, brush.numsides); + continue; + } + + if (brush.numsides > MAX_MAP_BRUSHSIDES) + { + Warning("%s ladder brush %d reported %d numsides which is more than max supported: %d\n", + __FUNCTION__, i, brush.numsides, MAX_MAP_BRUSHSIDES); + continue; + } + + out.AddToTail(brush); + } + + return true; + } + + [[nodiscard]] static bool ReadCSG(FileHandle_t f, + CUtlVector>& out) + { + CUtlVector brushes; + if (!ReadBrushes(f, brushes)) + { + return false; + } + + CUtlVector ladderSides; + CUtlVector ladderPlanes; + + out.EnsureCapacity(brushes.Count()); + Assert(out.Count() == 0); + for (const auto& brush : brushes) + { + ladderSides.SetCount(brush.numsides); + if (!Neo::ReadLump(LUMP_BRUSHSIDES, f, ladderSides.Base(), brush.firstside, brush.numsides)) + { + Warning("%s: failed to read brush sides lump contents %d - %d\n", __FUNCTION__, brush.firstside, brush.firstside + brush.numsides); + return false; + } + + ladderPlanes.Purge(); + for (int j = 0; j < ladderSides.Count(); ++j) + { + auto* storeAddr = ladderPlanes.AddToTailGetPtr(); + Assert(storeAddr); + if (!Neo::ReadLump(LUMP_PLANES, f, storeAddr, ladderSides[j].planenum)) + { + Warning("%s: failed to read plane lump contents for ladder side %d of plane num %d\n", __FUNCTION__, + j, ladderSides[j].planenum); + return false; + } + Assert(storeAddr->normal.IsValid()); + Assert(IsFinite(storeAddr->dist)); + } + Assert(ladderPlanes.Count() > 0); + + auto* nextPlanes = out.AddToTailGetPtr(); + nextPlanes->EnsureCapacity(ladderPlanes.Count()); + Assert(nextPlanes->Count() == 0); + for (const auto& p : ladderPlanes) + { + Vector4D alignedPlane{ p.normal.x, p.normal.y, p.normal.z, p.dist }; + Assert(alignedPlane.IsValid()); + nextPlanes->AddToTail(alignedPlane); + } + Assert(nextPlanes->Count() == ladderPlanes.Count()); + } + Assert(out.Count() == brushes.Count()); + + return true; + } + + [[nodiscard]] static bool LadderFromPolyhedron(const CPolyhedron* polyhedron) + { + Assert(polyhedron); + + Vector polyMins, polyMaxs, center; + for (int i = 0; i < polyhedron->iPolygonCount; ++i) + { + const auto& polygon = polyhedron->pPolygons[i]; + DevMsg("---\n\t%d POLYGON: %d\n", i, polygon.iIndexCount); + + ClearBounds(polyMins, polyMaxs); + + for (int j = 0; j < polygon.iIndexCount; ++j) + { + auto index = polygon.iFirstIndex + j; + const auto& idxLineRef = polyhedron->pIndices[index]; + const auto& pointIndices = polyhedron->pLines[idxLineRef.iLineIndex].iPointIndices; + + const Vector& linePos1 = polyhedron->pVertices[pointIndices[0]]; + const Vector& linePos2 = polyhedron->pVertices[pointIndices[1]]; + Assert(linePos1.IsValid()); + Assert(linePos2.IsValid()); + Assert(linePos1 != linePos2); + DevMsg("\t\tVERT: %f %f %f -> %f %f %f\n", + linePos1.x, linePos1.y, linePos1.z, + linePos2.x, linePos2.y, linePos2.z); + + AddPointToBounds(linePos1, polyMins, polyMaxs); + AddPointToBounds(linePos2, polyMins, polyMaxs); + + static int r = 0; + r = (r + 25) % 255; + Color color{ r,255,255,255 }; + UTIL_AddDebugLine(linePos1, linePos2, + true, false, color); + } + DevMsg("\t\tPOLY NORMAL: %f %f %f\n", polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z); + + center = VectorLerp(polyMins, polyMaxs, 0.5); + UTIL_AddDebugLine(center, center + (polygon.polyNormal * GenerationStepSize), true, false); + } + + return true; // TODO + } +} + +bool CNavMesh::BuildBrushLaddersFromBsp() +{ + bool ok = true; + + FileHandle_t f = {}; + CUtlVector> planesOfLadderBrushes; + + Assert(gpGlobals); + if (gpGlobals->bMapLoadFailed) + { + Warning("%s: map load failed\n", __FUNCTION__); + goto fail; + } + + char mapPath[MAX_PATH]; + Assert(!!gpGlobals->mapname && *gpGlobals->mapname.ToCStr()); + Assert(V_strlen(gpGlobals->mapname.ToCStr()) < MAX_MAP_NAME); + V_sprintf_safe(mapPath, "maps/%s.bsp", gpGlobals->mapname.ToCStr()); + if (!(mapPath || *mapPath)) + { + Warning("%s: failed to get map name\n", __FUNCTION__); + goto fail; + } + + Assert(filesystem); + if (!filesystem->FileExists(mapPath)) + { + Warning("%s: failed to locate map from filesystem: \"%s\"\n", + __FUNCTION__, mapPath); + goto fail; + } + + f = filesystem->Open(mapPath, "rb"); + if (!f) + { + Warning("%s: failed to open map file handle\n", __FUNCTION__); + goto fail; + } + + if (!Neo::ValidateBspHeader(f)) + { + goto fail; + } + + if (!Neo::ReadCSG(f, planesOfLadderBrushes)) + { + goto fail; + } + + if (planesOfLadderBrushes.Count() == 0) // no ladders? + { + goto cleanup; + } + + for (const auto& planesOfLadderBrush : planesOfLadderBrushes) + { + auto* polyhedron = GeneratePolyhedronFromPlanes( + planesOfLadderBrush.Base()->Base(), + planesOfLadderBrush.Count(), + ON_EPSILON, true); + + if (!polyhedron) + { + Assert(false); + goto fail; + } + + const bool ladderGenOk = Neo::LadderFromPolyhedron(polyhedron); + + polyhedron->Release(); + + if (!ladderGenOk) + { + Warning("%s: failed to generate ladder from polyhedron\n", __FUNCTION__); + + if (polyhedron->iVertexCount <= 0) + { + Warning("\tAdditionally, generated polyhedron reports %d sides but expected >0\n", + polyhedron->iVertexCount); + Assert(false); + continue; + } + + CUtlVectorpVertices)> vertices; + vertices.EnsureCapacity(polyhedron->iVertexCount); + for (int i = 0; i < polyhedron->iVertexCount; ++i) + { + auto** pVert = vertices.AddToTailGetPtr(); + *pVert = polyhedron->pVertices + i; + Assert(*pVert); + } + Assert(vertices.Count() == polyhedron->iVertexCount); + + CUtlString errMsg; + errMsg.Format("\t%d verts: ", polyhedron->iVertexCount); + for (const auto& vert : vertices) + { + errMsg.Format("%s [%f %f %f],", errMsg.String(), vert->x, vert->y, vert->z); + } + errMsg.SetLength(errMsg.Length() - 1); // remove final comma from the above loop + Warning("%s\n", errMsg.String()); + } + } + +cleanup: + if (f) + { + filesystem->Close(f); + } + return ok; +fail: + ok = false; + goto cleanup; +} +#endif + //-------------------------------------------------------------------------------------------------------------- /** * Strip the "analyzed" data out of all navigation areas diff --git a/src/game/server/nav_mesh.h b/src/game/server/nav_mesh.h index e778b03cd1..59a96db016 100644 --- a/src/game/server/nav_mesh.h +++ b/src/game/server/nav_mesh.h @@ -1194,6 +1194,10 @@ class CNavMesh : public CGameEventListener NavLadderVector m_ladders; // list of ladder navigation representations void BuildLadders( void ); void DestroyLadders( void ); +#ifdef NEO + // Build ladders from the BSP brush lump ladders (rather than func_ladder) + [[nodiscard]] bool BuildBrushLaddersFromBsp(); +#endif bool SampleStep( void ); // sample the walkable areas of the map void CreateNavAreasFromNodes( void ); // cover all of the sampled nodes with nav areas diff --git a/src/game/server/ndebugoverlay.cpp b/src/game/server/ndebugoverlay.cpp index fa152ba3fc..87a72f0d6a 100644 --- a/src/game/server/ndebugoverlay.cpp +++ b/src/game/server/ndebugoverlay.cpp @@ -66,7 +66,11 @@ OverlayLine_t* GetDebugOverlayLine(void) // Input : If testLOS is true, color is based on line of sight test // Output : //----------------------------------------------------------------------------- +#ifdef NEO +void UTIL_AddDebugLine(const Vector &startPos, const Vector &endPos, bool noDepthTest, bool testLOS, const Color& color) +#else void UTIL_AddDebugLine(const Vector &startPos, const Vector &endPos, bool noDepthTest, bool testLOS) +#endif { OverlayLine_t* debugLine = GetDebugOverlayLine(); @@ -87,10 +91,15 @@ void UTIL_AddDebugLine(const Vector &startPos, const Vector &endPos, bool noDept return; } } - +#ifdef NEO + debugLine->r = color.r(); + debugLine->g = color.g(); + debugLine->b = color.b(); +#else debugLine->r = 255; debugLine->g = 255; debugLine->b = 255; +#endif } //----------------------------------------------------------------------------- diff --git a/src/game/server/ndebugoverlay.h b/src/game/server/ndebugoverlay.h index 7b0a64a9d3..2a68d7e15c 100644 --- a/src/game/server/ndebugoverlay.h +++ b/src/game/server/ndebugoverlay.h @@ -25,7 +25,11 @@ struct OverlayLine_t bool draw; }; +#ifdef NEO +extern void UTIL_AddDebugLine( const Vector &startPos, const Vector &endPos, bool noDepthTest, bool testLOS, const Color& color={255,255,255,255}); +#else extern void UTIL_AddDebugLine( const Vector &startPos, const Vector &endPos, bool noDepthTest, bool testLOS ); +#endif extern void UTIL_DrawPositioningOverlay( float flCrossDistance ); extern void UTIL_DrawOverlayLines( void ); From d225a45ec5f969cf1ac38cf2ea6c9a39b25b1729 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 13 Mar 2026 19:39:03 +0200 Subject: [PATCH 03/16] default-disable debug short-circuit --- src/game/server/nav_generate.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/game/server/nav_generate.cpp b/src/game/server/nav_generate.cpp index df9e256abb..979ead85d4 100644 --- a/src/game/server/nav_generate.cpp +++ b/src/game/server/nav_generate.cpp @@ -25,6 +25,9 @@ // NOTE: This has to be the last file included! #include "tier0/memdbgon.h" +#ifdef NEO +#define LADDER_BRUSH_GEN_DEBUG false // Don't enable this unless you need to debug the ladder brush autogen +#endif enum { MAX_BLOCKED_AREAS = 256 }; static unsigned int blockedID[ MAX_BLOCKED_AREAS ]; @@ -3425,6 +3428,8 @@ void CNavMesh::AddWalkableSeeds( void ) */ void CNavMesh::BeginGeneration( bool incremental ) { +#ifdef NEO +#if LADDER_BRUSH_GEN_DEBUG // Fast debug path while skipping the other nav stuff. Don't enable this expect for ladder debug! if (!BuildBrushLaddersFromBsp()) { Warning("Generating brush ladders...FAIL\n"); @@ -3434,6 +3439,8 @@ void CNavMesh::BeginGeneration( bool incremental ) Msg( "Generating brush ladders...DONE\n" ); } return; +#endif +#endif IGameEvent *event = gameeventmanager->CreateEvent( "nav_generate" ); if ( event ) @@ -4028,7 +4035,8 @@ bool CNavMesh::UpdateGeneration( float maxTime ) m_isAnalyzed = true; } -#if 0 //def NEO +#ifdef NEO +#if !(LADDER_BRUSH_GEN_DEBUG) // Post-gen because we want automatic merge with the final navmesh if (!BuildBrushLaddersFromBsp()) { @@ -4038,6 +4046,7 @@ bool CNavMesh::UpdateGeneration( float maxTime ) { Msg( "Generating brush ladders...DONE\n" ); } +#endif #endif // generation complete! From 7ce192bbb95173ab1c36c9b2b1b2f055ac3ab18d Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 13 Mar 2026 19:39:53 +0200 Subject: [PATCH 04/16] refactor --- src/game/server/nav_mesh.cpp | 72 ++++++++++++++++++------------------ src/game/server/nav_mesh.h | 2 + 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index 15d220ba44..42dc8f1fa4 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -3322,51 +3322,53 @@ namespace Neo return true; } +} + +[[nodiscard]] bool CNavMesh::LadderFromPolyhedron(const CPolyhedron* polyhedron) +{ + Assert(polyhedron); - [[nodiscard]] static bool LadderFromPolyhedron(const CPolyhedron* polyhedron) + Vector polyMins, polyMaxs, center; + for (int i = 0; i < polyhedron->iPolygonCount; ++i) { - Assert(polyhedron); + const auto& polygon = polyhedron->pPolygons[i]; + DevMsg("---\n\t%d POLYGON: %d\n", i, polygon.iIndexCount); - Vector polyMins, polyMaxs, center; - for (int i = 0; i < polyhedron->iPolygonCount; ++i) + ClearBounds(polyMins, polyMaxs); + + for (int j = 0; j < polygon.iIndexCount; ++j) { - const auto& polygon = polyhedron->pPolygons[i]; - DevMsg("---\n\t%d POLYGON: %d\n", i, polygon.iIndexCount); + auto index = polygon.iFirstIndex + j; + const auto& idxLineRef = polyhedron->pIndices[index]; + const auto& pointIndices = polyhedron->pLines[idxLineRef.iLineIndex].iPointIndices; - ClearBounds(polyMins, polyMaxs); + const Vector& linePos1 = polyhedron->pVertices[pointIndices[0]]; + const Vector& linePos2 = polyhedron->pVertices[pointIndices[1]]; + Assert(linePos1.IsValid()); + Assert(linePos2.IsValid()); + Assert(linePos1 != linePos2); + DevMsg("\t\tVERT: %f %f %f -> %f %f %f\n", + linePos1.x, linePos1.y, linePos1.z, + linePos2.x, linePos2.y, linePos2.z); - for (int j = 0; j < polygon.iIndexCount; ++j) - { - auto index = polygon.iFirstIndex + j; - const auto& idxLineRef = polyhedron->pIndices[index]; - const auto& pointIndices = polyhedron->pLines[idxLineRef.iLineIndex].iPointIndices; - - const Vector& linePos1 = polyhedron->pVertices[pointIndices[0]]; - const Vector& linePos2 = polyhedron->pVertices[pointIndices[1]]; - Assert(linePos1.IsValid()); - Assert(linePos2.IsValid()); - Assert(linePos1 != linePos2); - DevMsg("\t\tVERT: %f %f %f -> %f %f %f\n", - linePos1.x, linePos1.y, linePos1.z, - linePos2.x, linePos2.y, linePos2.z); - - AddPointToBounds(linePos1, polyMins, polyMaxs); - AddPointToBounds(linePos2, polyMins, polyMaxs); - - static int r = 0; - r = (r + 25) % 255; - Color color{ r,255,255,255 }; - UTIL_AddDebugLine(linePos1, linePos2, - true, false, color); - } - DevMsg("\t\tPOLY NORMAL: %f %f %f\n", polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z); + AddPointToBounds(linePos1, polyMins, polyMaxs); + AddPointToBounds(linePos2, polyMins, polyMaxs); - center = VectorLerp(polyMins, polyMaxs, 0.5); - UTIL_AddDebugLine(center, center + (polygon.polyNormal * GenerationStepSize), true, false); + static int r = 0; + r = (r + 25) % 255; + Color color{ r,255,255,255 }; + UTIL_AddDebugLine(linePos1, linePos2, + true, false, color); } + DevMsg("\t\tPOLY NORMAL: %f %f %f\n", polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z); + + center = VectorLerp(polyMins, polyMaxs, 0.5); + UTIL_AddDebugLine(center, center + (polygon.polyNormal * GenerationStepSize), true, false); return true; // TODO } + + return true; } bool CNavMesh::BuildBrushLaddersFromBsp() @@ -3436,7 +3438,7 @@ bool CNavMesh::BuildBrushLaddersFromBsp() goto fail; } - const bool ladderGenOk = Neo::LadderFromPolyhedron(polyhedron); + const bool ladderGenOk = LadderFromPolyhedron(polyhedron); polyhedron->Release(); diff --git a/src/game/server/nav_mesh.h b/src/game/server/nav_mesh.h index 59a96db016..36b8a3bfe8 100644 --- a/src/game/server/nav_mesh.h +++ b/src/game/server/nav_mesh.h @@ -1197,6 +1197,8 @@ class CNavMesh : public CGameEventListener #ifdef NEO // Build ladders from the BSP brush lump ladders (rather than func_ladder) [[nodiscard]] bool BuildBrushLaddersFromBsp(); +private: + [[nodiscard]] bool LadderFromPolyhedron(const CPolyhedron* polyhedron); #endif bool SampleStep( void ); // sample the walkable areas of the map From 64f9609cc27a1d097011cfd96b0964786afb7f8e Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 13 Mar 2026 19:40:05 +0200 Subject: [PATCH 05/16] wip: auto-generate brush ladders --- src/game/server/nav_mesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index 42dc8f1fa4..1a6c115f91 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -3365,7 +3365,7 @@ namespace Neo center = VectorLerp(polyMins, polyMaxs, 0.5); UTIL_AddDebugLine(center, center + (polygon.polyNormal * GenerationStepSize), true, false); - return true; // TODO + CreateLadder(polyMins, polyMaxs, HumanHeight); } return true; From cfbd985d9e7c04d936730bfcae5f2540a4c585f4 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 13 Mar 2026 20:42:40 +0200 Subject: [PATCH 06/16] Fix format specifier typos --- src/game/server/nav_mesh.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index 1a6c115f91..4cff9f67dd 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -3158,8 +3158,8 @@ namespace Neo } else if (fileTargetLumpOffset > std::numeric_limits::max()) { - Warning("%s: Reading starting to read lumpid(s) of type %d from index %d would require lump offset of %zu which is >max: %d\n", - __FUNCTION__, id, fileTargetLumpOffset, std::numeric_limits::max()); + Warning("%s: Starting to read lumpid(s) of type %d from index %d would require lump offset of %zu which is >max: %d\n", + __FUNCTION__, id, startLump, fileTargetLumpOffset, std::numeric_limits::max()); return false; } @@ -3197,7 +3197,7 @@ namespace Neo const auto bytesReadTotal = filesystem->Read(out, narrow_cast(sizeToRead), f); if (bytesReadTotal != sizeToRead) { - Warning("%s: Expected to read %d bytes from %d lump(s) of type %d, but read %d bytes\n", + Warning("%s: Expected to read %zu bytes from %d lump(s) of type %d, but read %d bytes\n", __FUNCTION__, sizeToRead, numLumpsToRead, id, bytesReadTotal); return false; } From 7ec8e870bbdeaa4da016a125700394ae478d9702 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 14 Mar 2026 07:57:39 +0200 Subject: [PATCH 07/16] Refactor --- src/game/server/nav_generate.cpp | 46 ++++++++++++++------------- src/game/server/nav_mesh.cpp | 54 +++++++++++++++++++++++++------- src/game/server/nav_mesh.h | 3 ++ 3 files changed, 71 insertions(+), 32 deletions(-) diff --git a/src/game/server/nav_generate.cpp b/src/game/server/nav_generate.cpp index 979ead85d4..3a47f21618 100644 --- a/src/game/server/nav_generate.cpp +++ b/src/game/server/nav_generate.cpp @@ -22,12 +22,13 @@ #include "func_simpleladder.h" #endif +#ifdef NEO +#include "nav_mesh.h" +#endif + // NOTE: This has to be the last file included! #include "tier0/memdbgon.h" -#ifdef NEO -#define LADDER_BRUSH_GEN_DEBUG false // Don't enable this unless you need to debug the ladder brush autogen -#endif enum { MAX_BLOCKED_AREAS = 256 }; static unsigned int blockedID[ MAX_BLOCKED_AREAS ]; @@ -3429,17 +3430,18 @@ void CNavMesh::AddWalkableSeeds( void ) void CNavMesh::BeginGeneration( bool incremental ) { #ifdef NEO -#if LADDER_BRUSH_GEN_DEBUG // Fast debug path while skipping the other nav stuff. Don't enable this expect for ladder debug! - if (!BuildBrushLaddersFromBsp()) + if (nav_generate_debug_brushladders.GetBool()) { - Warning("Generating brush ladders...FAIL\n"); - } - else - { - Msg( "Generating brush ladders...DONE\n" ); + if (!BuildBrushLaddersFromBsp()) + { + Warning("Generating brush ladders...FAIL\n"); + } + else + { + Msg( "Generating brush ladders...DONE\n" ); + } + return; } - return; -#endif #endif IGameEvent *event = gameeventmanager->CreateEvent( "nav_generate" ); @@ -4036,17 +4038,19 @@ bool CNavMesh::UpdateGeneration( float maxTime ) } #ifdef NEO -#if !(LADDER_BRUSH_GEN_DEBUG) - // Post-gen because we want automatic merge with the final navmesh - if (!BuildBrushLaddersFromBsp()) - { - Warning("Generating brush ladders...FAIL\n"); - } - else + // This runs earlier during the generation for the debug==true case, so skip here. + if (!nav_generate_debug_brushladders.GetBool()) { - Msg( "Generating brush ladders...DONE\n" ); + // Post-gen because we want automatic merge with the final navmesh + if (!BuildBrushLaddersFromBsp()) + { + Warning("Generating brush ladders...FAIL\n"); + } + else + { + Msg( "Generating brush ladders...DONE\n" ); + } } -#endif #endif // generation complete! diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index 4cff9f67dd..b63d5ebdd1 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -41,8 +41,15 @@ // NOTE: This has to be the last file included! #include "tier0/memdbgon.h" - +#ifdef NEO +static auto DrawLine(const Vector& from, const Vector& to, float duration = NDEBUG_PERSIST_TILL_NEXT_SERVER, + int r = 255, int g=255, int b=255) +{ + return NDebugOverlay::Line(from, to, r, g, b, true, duration); +} +#else #define DrawLine( from, to, duration, red, green, blue ) NDebugOverlay::Line( from, to, red, green, blue, true, NDEBUG_PERSIST_TILL_NEXT_SERVER ) +#endif /** @@ -62,6 +69,12 @@ ConVar nav_max_vis_delta_list_length( "nav_max_vis_delta_list_length", "64", FCV extern ConVar nav_show_potentially_visible; +#ifdef NEO +ConVar nav_generate_debug_brushladders("nav_generate_debug_brushladders", "0", FCVAR_CHEAT, + "If non-zero, will only visualize ladder brush generation during nav_generate and do nothing else. " + "The visualization will remain visible for the amount of seconds specified by cvar value.", + true, false, false, 0); +#endif bool FindGroundForNode( Vector *pos, Vector *normal ); @@ -3328,6 +3341,11 @@ namespace Neo { Assert(polyhedron); + if (nav_generate_debug_brushladders.GetBool()) + { + Assert(debugoverlay); + } + Vector polyMins, polyMaxs, center; for (int i = 0; i < polyhedron->iPolygonCount; ++i) { @@ -3347,23 +3365,37 @@ namespace Neo Assert(linePos1.IsValid()); Assert(linePos2.IsValid()); Assert(linePos1 != linePos2); - DevMsg("\t\tVERT: %f %f %f -> %f %f %f\n", - linePos1.x, linePos1.y, linePos1.z, - linePos2.x, linePos2.y, linePos2.z); AddPointToBounds(linePos1, polyMins, polyMaxs); AddPointToBounds(linePos2, polyMins, polyMaxs); - static int r = 0; - r = (r + 25) % 255; - Color color{ r,255,255,255 }; - UTIL_AddDebugLine(linePos1, linePos2, - true, false, color); + if (nav_generate_debug_brushladders.GetBool()) + { + DevMsg("\t\tLINE: %f %f %f -> %f %f %f\n", + linePos1.x, linePos1.y, linePos1.z, + linePos2.x, linePos2.y, linePos2.z); + + static int r = 0; + r = (r + 25) % 255; + DrawLine(linePos1, linePos2, + nav_generate_debug_brushladders.GetFloat(), r, 255, 255); + } } - DevMsg("\t\tPOLY NORMAL: %f %f %f\n", polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z); center = VectorLerp(polyMins, polyMaxs, 0.5); - UTIL_AddDebugLine(center, center + (polygon.polyNormal * GenerationStepSize), true, false); + + if (nav_generate_debug_brushladders.GetBool()) + { + DevMsg("\t\tPOLY NORMAL: %f %f %f\n", + polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z); + const Color c = COLOR_BLUE; + DrawLine(center, center + (polygon.polyNormal * GenerationStepSize), + nav_generate_debug_brushladders.GetFloat(), c.r(), c.g(), c.b()); + + continue; + } + + // TODO: filter CreateLadder(polyMins, polyMaxs, HumanHeight); } diff --git a/src/game/server/nav_mesh.h b/src/game/server/nav_mesh.h index 36b8a3bfe8..85c8ee4d18 100644 --- a/src/game/server/nav_mesh.h +++ b/src/game/server/nav_mesh.h @@ -36,6 +36,9 @@ extern ConVar nav_edit; extern ConVar nav_quicksave; extern ConVar nav_show_approach_points; extern ConVar nav_show_danger; +#ifdef NEO +extern ConVar nav_generate_debug_brushladders; +#endif //-------------------------------------------------------------------------------------------------------- class NavAreaCollector From 97b704e9c3d8f5c32d1484b5f189bf047f847be7 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 14 Mar 2026 09:46:45 +0200 Subject: [PATCH 08/16] Improve debug --- src/game/server/nav_mesh.cpp | 83 ++++++++++++++++++++++++++++++++++-- src/game/shared/shareddefs.h | 3 ++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index b63d5ebdd1..c0060a8e35 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -3347,9 +3347,24 @@ namespace Neo } Vector polyMins, polyMaxs, center; + trace_t tr; for (int i = 0; i < polyhedron->iPolygonCount; ++i) { const auto& polygon = polyhedron->pPolygons[i]; +#ifdef DBGFLAG_ASSERT + { + const Vector dbgVec{ polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z }; + Assert(dbgVec.IsValid()); + Assert(!dbgVec.IsZero()); + AssertFloatEquals(dbgVec.Length(), 1.f, 0.01f); // unit vector + } +#endif + // Can't build up/down facing ladder mounting points, skip + if (!polygon.polyNormal.x && !polygon.polyNormal.y) + { + continue; + } + DevMsg("---\n\t%d POLYGON: %d\n", i, polygon.iIndexCount); ClearBounds(polyMins, polyMaxs); @@ -3381,21 +3396,83 @@ namespace Neo nav_generate_debug_brushladders.GetFloat(), r, 255, 255); } } - center = VectorLerp(polyMins, polyMaxs, 0.5); + enum LadderPolyFilterReason { + ThisIsFine = 0, // No problems detected, do not filter this poly. + TooSlim, // At least one of the dimensions of this 2D poly is unacceptably slim for bot navigation. + TooCramped, // Insufficient nav clearance for bots to meaningfully mount here. + } filter = ThisIsFine; + + // Ignore ladder polygons that are super slim, because it's likely either a side + // or a bevel of the actual intended mounting point. + // While such surfaces *could* sometimes be used by humans, they require a lot of + // finesse to meaningfully traverse, so it's better for the bots to prefer the obvious + // large mapper-intended primary climb surfaces. Of course, the mapper could opt + // to manually markup their custom slim ladders as usual outside the auto-gen. + constexpr float smallestLadderDimToConsider = 4; + int nSmaller = 0; + nSmaller += (polyMaxs.x - polyMins.x < smallestLadderDimToConsider); + nSmaller += (polyMaxs.y - polyMins.y < smallestLadderDimToConsider); + nSmaller += (polyMaxs.z - polyMins.z < smallestLadderDimToConsider); + + // Because the poly mins/maxs are modeling a 2D surface, it is expected for + // one of the dimensions to be 0 or near 0. But if 2 or more are below + // smallestLadderDimToConsider, then we do want to filter. + if (nSmaller > 1) + { + filter = TooSlim; + } + + if (filter == ThisIsFine) + { + // Cull ladder sides facing the wall etc. + // Need clearance of at least GenerationStepSize in front of the ladder to mount. + UTIL_TraceLine( + center + (polygon.polyNormal * ON_EPSILON), + center + (polygon.polyNormal * GenerationStepSize), + MASK_PLAYERSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr); + if (tr.DidHit()) + { + filter = TooCramped; + } + } + if (nav_generate_debug_brushladders.GetBool()) { + Color c; + switch (filter) + { + case ThisIsFine: + c = COLOR_LIME; + break; + case TooSlim: + c = COLOR_DARK_ORANGE; + break; + case TooCramped: + c = COLOR_NEON_PINK; + break; + default: + c = COLOR_BLACK; + Assert(false); + break; + } DevMsg("\t\tPOLY NORMAL: %f %f %f\n", polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z); - const Color c = COLOR_BLUE; + if (filter != ThisIsFine) + { + DevMsg("\t\t\tREJECTED! enum: %d\n", filter); + } DrawLine(center, center + (polygon.polyNormal * GenerationStepSize), nav_generate_debug_brushladders.GetFloat(), c.r(), c.g(), c.b()); + } + if (filter) + { continue; } - // TODO: filter + continue; // TODO: debug CreateLadder(polyMins, polyMaxs, HumanHeight); } diff --git a/src/game/shared/shareddefs.h b/src/game/shared/shareddefs.h index b290d6486e..e0ad452dd3 100644 --- a/src/game/shared/shareddefs.h +++ b/src/game/shared/shareddefs.h @@ -628,6 +628,9 @@ typedef enum #define COLOR_TRANSPARENT Color(0, 0, 0, 0) #define COLOR_DARK Color(55, 55, 55, 255) #define COLOR_FADED_DARK Color(55, 55, 55, 176) +#define COLOR_NEON_PINK Color(255, 16, 240) +#define COLOR_DARK_ORANGE Color(255, 140, 0) +#define COLOR_LIME Color(0, 255, 0) #endif // All NPCs need this data From 532842b737f6958b7ef2539ded4b63decced4f46 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 14 Mar 2026 09:46:57 +0200 Subject: [PATCH 09/16] Make GenerationStepSize constexpr --- src/game/server/nav.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/server/nav.h b/src/game/server/nav.h index 78dcd77254..c9113738c4 100644 --- a/src/game/server/nav.h +++ b/src/game/server/nav.h @@ -21,7 +21,7 @@ */ #ifdef NEO -const float GenerationStepSize = 20.0f; // NEO: some hallways/openings were missing connections with larger value +constexpr float GenerationStepSize = 20.0f; // NEO: some hallways/openings were missing connections with larger value #else const float GenerationStepSize = 25.0f; // (30) was 20, but bots can't fit always fit #endif From e56d247d3a942843066081b1e3f460fe618532e2 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 14 Mar 2026 09:51:29 +0200 Subject: [PATCH 10/16] refactor --- src/game/server/nav_mesh.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index c0060a8e35..a20c09e93d 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -3459,22 +3459,20 @@ namespace Neo } DevMsg("\t\tPOLY NORMAL: %f %f %f\n", polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z); - if (filter != ThisIsFine) - { - DevMsg("\t\t\tREJECTED! enum: %d\n", filter); - } DrawLine(center, center + (polygon.polyNormal * GenerationStepSize), nav_generate_debug_brushladders.GetFloat(), c.r(), c.g(), c.b()); } - if (filter) + if (filter != ThisIsFine) { + if (nav_generate_debug_brushladders.GetBool()) + { + DevMsg("\t\tREJECTED! enum: %d\n", filter); + } continue; } - continue; // TODO: debug - - CreateLadder(polyMins, polyMaxs, HumanHeight); + //CreateLadder(polyMins, polyMaxs, HumanHeight); } return true; From 5946d496f07a84c4eb1d5bbce0402964459ed403 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 14 Mar 2026 09:52:42 +0200 Subject: [PATCH 11/16] CreateLadder for !nav_generate_debug_brushladders runs --- src/game/server/nav_mesh.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index a20c09e93d..4a0e7da59a 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -3472,7 +3472,11 @@ namespace Neo continue; } - //CreateLadder(polyMins, polyMaxs, HumanHeight); + // Create for real only if we're not debugging this logic + if (!nav_generate_debug_brushladders.GetBool()) + { + CreateLadder(polyMins, polyMaxs, HumanHeight); + } } return true; From f2fc750d7b2741f6cecc40bfd37b25d6cac85782 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 14 Mar 2026 15:35:47 +0200 Subject: [PATCH 12/16] wip: Fix ladder generation for slanted angles --- src/game/server/nav_mesh.cpp | 44 ++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index 4a0e7da59a..10a75daee5 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -3346,6 +3346,11 @@ namespace Neo Assert(debugoverlay); } + decltype(m_surfaceNormal) backup_m_surfaceNormal; + decltype(m_editCursorPos) backup_m_editCursorPos; + decltype(m_editMode) backup_m_editMode; + decltype(m_climbableSurface) backup_m_climbableSurface; + Vector polyMins, polyMaxs, center; trace_t tr; for (int i = 0; i < polyhedron->iPolygonCount; ++i) @@ -3473,9 +3478,44 @@ namespace Neo } // Create for real only if we're not debugging this logic - if (!nav_generate_debug_brushladders.GetBool()) + if (nav_generate_debug_brushladders.GetBool()) + { + continue; + } + + // NEO JANK (Rain): manipulate edit mode state to fake a ladder build. + // Should probs just make our own version of the function to clean this up. { - CreateLadder(polyMins, polyMaxs, HumanHeight); + // Backup state + { + backup_m_surfaceNormal = m_surfaceNormal; + backup_m_editCursorPos = m_editCursorPos; + backup_m_editMode = m_editMode; + backup_m_climbableSurface = m_climbableSurface; + } + // Overwrite the state in preparetion for CommandNavBuildLadder + { + m_editCursorPos = center; + m_surfaceNormal = Vector{ + polygon.polyNormal.x, + polygon.polyNormal.y, + polygon.polyNormal.z }; + m_editMode = EditModeType::NORMAL; + m_climbableSurface = true; + } + // The state-dependent ladder build call + { + CommandNavBuildLadder(); + } + // Finally, restore state + { + m_editMode = backup_m_editMode; + m_editCursorPos = backup_m_editCursorPos; + m_surfaceNormal = backup_m_surfaceNormal; + m_climbableSurface = backup_m_climbableSurface; + } + + //CreateLadder( topEdge, bottomEdge, leftEdge.DistTo( rightEdge ), m_ladderNormal.AsVector2D(), 0.0f ); } } From abb6bddf5fb7d3955dcc80010f754b0055c08137 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 14 Mar 2026 17:31:58 +0200 Subject: [PATCH 13/16] refactor --- src/game/server/nav_mesh.cpp | 53 ++++++++++-------------------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index 10a75daee5..ec816b19c2 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -3346,12 +3346,12 @@ namespace Neo Assert(debugoverlay); } - decltype(m_surfaceNormal) backup_m_surfaceNormal; - decltype(m_editCursorPos) backup_m_editCursorPos; - decltype(m_editMode) backup_m_editMode; - decltype(m_climbableSurface) backup_m_climbableSurface; + // To be used for the center position of each polygon of the polyhedron. + // By reference, because the final CommandNavBuildLadder call which actually builds the ladder + // depends on this state, and we can spare the extra Vector by just assigning to it directly. + Vector& center = m_editCursorPos; - Vector polyMins, polyMaxs, center; + Vector polyMins, polyMaxs; trace_t tr; for (int i = 0; i < polyhedron->iPolygonCount; ++i) { @@ -3370,7 +3370,10 @@ namespace Neo continue; } - DevMsg("---\n\t%d POLYGON: %d\n", i, polygon.iIndexCount); + if (nav_generate_debug_brushladders.GetBool()) + { + DevMsg("---\n\t%d POLYGON: %d\n", i, polygon.iIndexCount); + } ClearBounds(polyMins, polyMaxs); @@ -3483,40 +3486,12 @@ namespace Neo continue; } - // NEO JANK (Rain): manipulate edit mode state to fake a ladder build. - // Should probs just make our own version of the function to clean this up. - { - // Backup state - { - backup_m_surfaceNormal = m_surfaceNormal; - backup_m_editCursorPos = m_editCursorPos; - backup_m_editMode = m_editMode; - backup_m_climbableSurface = m_climbableSurface; - } - // Overwrite the state in preparetion for CommandNavBuildLadder - { - m_editCursorPos = center; - m_surfaceNormal = Vector{ - polygon.polyNormal.x, - polygon.polyNormal.y, - polygon.polyNormal.z }; - m_editMode = EditModeType::NORMAL; - m_climbableSurface = true; - } - // The state-dependent ladder build call - { - CommandNavBuildLadder(); - } - // Finally, restore state - { - m_editMode = backup_m_editMode; - m_editCursorPos = backup_m_editCursorPos; - m_surfaceNormal = backup_m_surfaceNormal; - m_climbableSurface = backup_m_climbableSurface; - } + // Set up state as required by the CommandNavBuildLadder call below + m_surfaceNormal = Vector{ polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z }; + m_editMode = EditModeType::NORMAL; + m_climbableSurface = true; - //CreateLadder( topEdge, bottomEdge, leftEdge.DistTo( rightEdge ), m_ladderNormal.AsVector2D(), 0.0f ); - } + CommandNavBuildLadder(); } return true; From bd8ebc50fa7f8a96d5cd59dca2e8b999738a926b Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 14 Mar 2026 18:48:07 +0200 Subject: [PATCH 14/16] wip --- src/game/server/nav_mesh.cpp | 40 ++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/game/server/nav_mesh.cpp b/src/game/server/nav_mesh.cpp index ec816b19c2..ce33ea6484 100644 --- a/src/game/server/nav_mesh.cpp +++ b/src/game/server/nav_mesh.cpp @@ -3349,26 +3349,38 @@ namespace Neo // To be used for the center position of each polygon of the polyhedron. // By reference, because the final CommandNavBuildLadder call which actually builds the ladder // depends on this state, and we can spare the extra Vector by just assigning to it directly. - Vector& center = m_editCursorPos; + Vector& polyCenter = m_editCursorPos; + Vector& polyNormal = m_surfaceNormal; Vector polyMins, polyMaxs; trace_t tr; for (int i = 0; i < polyhedron->iPolygonCount; ++i) { const auto& polygon = polyhedron->pPolygons[i]; + polyNormal = { polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z }; #ifdef DBGFLAG_ASSERT { - const Vector dbgVec{ polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z }; - Assert(dbgVec.IsValid()); - Assert(!dbgVec.IsZero()); - AssertFloatEquals(dbgVec.Length(), 1.f, 0.01f); // unit vector + Assert(polyNormal.IsValid()); + Assert(!polyNormal.IsZero()); + AssertFloatEquals(polyNormal.Length(), 1.f, 0.01f); // unit vector } #endif // Can't build up/down facing ladder mounting points, skip - if (!polygon.polyNormal.x && !polygon.polyNormal.y) + if (!polyNormal.x && !polyNormal.y) { continue; } + // Also reject sides that the nav system considers too flat for ladders + if (polyNormal.z) + { + extern ConVar nav_slope_limit; + Assert(nav_slope_limit.GetFloat() >= 0); + Assert(nav_slope_limit.GetFloat() <= 1); + if (abs(polyNormal.z) > nav_slope_limit.GetFloat()) + { + continue; + } + } if (nav_generate_debug_brushladders.GetBool()) { @@ -3404,7 +3416,7 @@ namespace Neo nav_generate_debug_brushladders.GetFloat(), r, 255, 255); } } - center = VectorLerp(polyMins, polyMaxs, 0.5); + polyCenter = VectorLerp(polyMins, polyMaxs, 0.5); enum LadderPolyFilterReason { ThisIsFine = 0, // No problems detected, do not filter this poly. @@ -3423,7 +3435,6 @@ namespace Neo nSmaller += (polyMaxs.x - polyMins.x < smallestLadderDimToConsider); nSmaller += (polyMaxs.y - polyMins.y < smallestLadderDimToConsider); nSmaller += (polyMaxs.z - polyMins.z < smallestLadderDimToConsider); - // Because the poly mins/maxs are modeling a 2D surface, it is expected for // one of the dimensions to be 0 or near 0. But if 2 or more are below // smallestLadderDimToConsider, then we do want to filter. @@ -3432,14 +3443,14 @@ namespace Neo filter = TooSlim; } - if (filter == ThisIsFine) + else if (filter == ThisIsFine) { // Cull ladder sides facing the wall etc. // Need clearance of at least GenerationStepSize in front of the ladder to mount. UTIL_TraceLine( - center + (polygon.polyNormal * ON_EPSILON), - center + (polygon.polyNormal * GenerationStepSize), - MASK_PLAYERSOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &tr); + polyCenter + (polyNormal * ON_EPSILON), + polyCenter + (polyNormal * GenerationStepSize), + GetGenerationTraceMask(), nullptr, COLLISION_GROUP_NONE, &tr); if (tr.DidHit()) { filter = TooCramped; @@ -3466,8 +3477,8 @@ namespace Neo break; } DevMsg("\t\tPOLY NORMAL: %f %f %f\n", - polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z); - DrawLine(center, center + (polygon.polyNormal * GenerationStepSize), + polyNormal.x, polyNormal.y, polyNormal.z); + DrawLine(polyCenter, polyCenter + (polyNormal * GenerationStepSize), nav_generate_debug_brushladders.GetFloat(), c.r(), c.g(), c.b()); } @@ -3487,7 +3498,6 @@ namespace Neo } // Set up state as required by the CommandNavBuildLadder call below - m_surfaceNormal = Vector{ polygon.polyNormal.x, polygon.polyNormal.y, polygon.polyNormal.z }; m_editMode = EditModeType::NORMAL; m_climbableSurface = true; From ca3b2e9add2f486674698ef14f1bcadf949074d0 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 14 Mar 2026 19:32:23 +0200 Subject: [PATCH 15/16] cleanup --- src/game/server/nav_generate.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/game/server/nav_generate.cpp b/src/game/server/nav_generate.cpp index 3a47f21618..5f5d2e3370 100644 --- a/src/game/server/nav_generate.cpp +++ b/src/game/server/nav_generate.cpp @@ -22,10 +22,6 @@ #include "func_simpleladder.h" #endif -#ifdef NEO -#include "nav_mesh.h" -#endif - // NOTE: This has to be the last file included! #include "tier0/memdbgon.h" From df5d449086e637a31dcd6049fb35d6febaeeb6ea Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 14 Mar 2026 19:49:47 +0200 Subject: [PATCH 16/16] Fix nav_generate bot kick command --- src/game/server/nav_generate.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/game/server/nav_generate.cpp b/src/game/server/nav_generate.cpp index 5f5d2e3370..fee6291096 100644 --- a/src/game/server/nav_generate.cpp +++ b/src/game/server/nav_generate.cpp @@ -3446,6 +3446,9 @@ void CNavMesh::BeginGeneration( bool incremental ) gameeventmanager->FireEvent( event ); } +#ifdef NEO + engine->ServerCommand( "neo_bot_kick all\n" ); +#else #ifdef TERROR engine->ServerCommand( "director_stop\nnb_delete_all\n" ); if ( !incremental && !engine->IsDedicatedServer() ) @@ -3459,6 +3462,7 @@ void CNavMesh::BeginGeneration( bool incremental ) #else engine->ServerCommand( "bot_kick\n" ); #endif +#endif // NEO // Right now, incrementally-generated areas won't connect to existing areas automatically. // Since this means hand-editing will be necessary, don't do a full analyze.