From 556ab7e70d8ef72281a2d7084485331aa63fa75c Mon Sep 17 00:00:00 2001 From: Blounard Date: Thu, 5 Mar 2026 22:37:30 +0100 Subject: [PATCH 1/5] Non Pathable Fix Fixed a bug where non pathable quad could get assigned a far away checkpoint node with crossing path. Now instead of using min XYZ distance, it uses min neighbour distance. Also added some warnings + filters for overlapping path, and end quads not reached. --- src/path.cpp | 187 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 132 insertions(+), 55 deletions(-) diff --git a/src/path.cpp b/src/path.cpp index 0c92b62..dfdf2b0 100644 --- a/src/path.cpp +++ b/src/path.cpp @@ -137,17 +137,90 @@ std::vector Path::GeneratePath(size_t pathStartIndex, std::vector> neighbours(quadblocks.size()); + for (size_t i = 0; i < quadblocks.size(); i++) + { + if (!quadblocks[i].GetCheckpointStatus()) { continue; } + for (size_t j = i + 1; j < quadblocks.size(); j++) + { + if (!quadblocks[j].GetCheckpointStatus()) { continue; } + if (quadblocks[i].Neighbours(quadblocks[j])) + { + neighbours[i].push_back(j); + neighbours[j].push_back(i); + } + } + } + + // Build Connected Component : All quad of this path, starting from start. + std::vector connectedComponent(quadblocks.size(), false); + std::vector visitedQuadblocks(quadblocks.size(), false); + std::vector toVisit = m_quadIndexesStart; + + for (const size_t index : m_quadIndexesStart) + { + visitedQuadblocks[index] = true; + } + for (const size_t index : m_quadIndexesIgnore) + { + visitedQuadblocks[index] = true; + } + for (size_t i = 0; i < quadblocks.size(); i++) + { + if (!quadblocks[i].GetCheckpointStatus()) + { + visitedQuadblocks[i] = true; + } + } + bool overlap = false; + while (!toVisit.empty()) + { + size_t currQuadID = toVisit.back(); + toVisit.pop_back(); + connectedComponent[currQuadID] = true; + for (const size_t neighbourID : neighbours[currQuadID]) + { + if (!visitedQuadblocks[neighbourID]) + { + if (quadblocks[neighbourID].GetCheckpoint() != -1) + { + printf("WARNING: %s (green) from path %zu is neighbour with %s (red) from a previous path\n", quadblocks[currQuadID].GetName().c_str(), m_index, quadblocks[neighbourID].GetName().c_str()); + overlap = true; + quadblocks[neighbourID].SetFilter(true); + quadblocks[neighbourID].SetFilterColor(Color(static_cast(255), static_cast(0), static_cast(0))); + quadblocks[currQuadID].SetFilter(true); + quadblocks[currQuadID].SetFilterColor(Color(static_cast(0), static_cast(255), static_cast(0))); + } + else + { + visitedQuadblocks[neighbourID] = true; + toVisit.push_back(neighbourID); + } + } + } + } + bool endQuadFound = true; + for (const size_t index : m_quadIndexesEnd) + { + if (!connectedComponent[index]) + { + endQuadFound = false; + printf("WARNING : %s not found in path %zu from the beginning quads\n", quadblocks[index].GetName().c_str(), m_index); + } + } + // First pass : Build the main path size_t visitedCount = 0; std::vector startEndIndexes; - std::vector visitedQuadblocks(quadblocks.size(), false); + visitedQuadblocks.assign(quadblocks.size(), false); GetStartEndIndexes(startEndIndexes); for (const size_t index : startEndIndexes) { visitedQuadblocks[index] = true; visitedCount++; } - for (size_t i = 0; i < quadblocks.size(); i++) { if (!(quadblocks[i].GetCheckpointStatus() && quadblocks[i].GetCheckpointPathable())) @@ -159,21 +232,19 @@ std::vector Path::GeneratePath(size_t pathStartIndex, std::vector currQuadblocks = m_quadIndexesStart; std::vector> quadIndexesPerChunk; - - // First pass : only look at the pathable quads while (true) { std::vector nextQuadblocks; if (visitedCount < quadblocks.size()) { - for (const size_t index : currQuadblocks) + for (const size_t currID : currQuadblocks) { - for (size_t i = 0; i < quadblocks.size(); i++) + for (size_t neighboorID : neighbours[currID]) { - if (!visitedQuadblocks[i] && quadblocks[index].Neighbours(quadblocks[i])) + if (!visitedQuadblocks[neighboorID]) { - nextQuadblocks.push_back(i); - visitedQuadblocks[i] = true; + nextQuadblocks.push_back(neighboorID); + visitedQuadblocks[neighboorID] = true; visitedCount++; } } @@ -236,65 +307,71 @@ std::vector Path::GeneratePath(size_t pathStartIndex, std::vector toVisit = m_quadIndexesStart; - while (!toVisit.empty()) + // Second pass : Look at all checkpoints quads, and set checkpoint ID of non pathable ones + // The checkpoint ID is set with the ID of the pathable quad the nearest in term of neighbours distance, then XYZ distance for tiebreak + for (size_t currQuadID = 0; currQuadID < quadblocks.size(); currQuadID++) { - size_t currQuadID = toVisit.back(); - toVisit.pop_back(); - Quadblock& currQuad = quadblocks[currQuadID]; - if (currQuad.GetCheckpointStatus() && !currQuad.GetCheckpointPathable()) + if (connectedComponent[currQuadID] && !quadblocks[currQuadID].GetCheckpointPathable()) { - // Assign to currQuad the closest checkpoint (only if currQuad isn't pathable) - float closestDist = std::numeric_limits::max(); - Vec3 quadCenter = currQuad.GetCenter(); - for (const Checkpoint& ckpt : checkpoints) + Quadblock& currQuad = quadblocks[currQuadID]; + visitedQuadblocks.assign(quadblocks.size(), false); + visitedQuadblocks[currQuadID] = true; + std::vector currentChunk = { currQuadID }; + while (!currentChunk.empty()) { - float dist = (ckpt.GetPos() - quadCenter).Length(); - if (dist < closestDist) + std::vector nextChunck; + std::vector candidateCheckpoints; + for (const size_t chunkQuadID : currentChunk) { - closestDist = dist; - currQuad.SetCheckpoint(ckpt.GetIndex()); + for (const size_t neighborID : neighbours[chunkQuadID]) + { + if (!visitedQuadblocks[neighborID]) + { + visitedQuadblocks[neighborID] = true; + if (quadblocks[neighborID].GetCheckpointPathable() && quadblocks[neighborID].GetCheckpoint() != -1) + { + candidateCheckpoints.push_back(quadblocks[neighborID].GetCheckpoint()); + } + else + { + nextChunck.push_back(neighborID); + } + } + } } - } - } - - //Find neighboor to visit (all neighboor, not only pathable ones) - for (size_t i = 0; i < quadblocks.size(); i++) - { - if (!visitedQuadblocks[i] && currQuad.Neighbours(quadblocks[i])) - { - toVisit.push_back(i); - visitedQuadblocks[i] = true; + if (!candidateCheckpoints.empty()) + { + float closestDist = std::numeric_limits::max(); + Vec3 quadCenter = currQuad.GetCenter(); + for (int ckpt_id : candidateCheckpoints) + { + Checkpoint& ckpt = checkpoints[ckpt_id - static_cast(pathStartIndex)]; + float dist = (ckpt.GetPos() - quadCenter).Length(); + if (dist < closestDist) + { + closestDist = dist; + currQuad.SetCheckpoint(ckpt_id); + } + } + break; + } + currentChunk = nextChunck; } } } - // Make sure to give start checkpoints start indexes (was overwritten in 2nd pass) + m_start = pathStartIndex; + m_end = m_start + checkpoints.size() - 1; + + // Make sure to give start/end checkpoints start/end indexes (was overwritten in 2nd pass) for (const size_t index : m_quadIndexesStart) { quadblocks[index].SetCheckpoint(static_cast(pathStartIndex)); } - - - - m_start = pathStartIndex; - m_end = m_start + checkpoints.size() - 1; + for (const size_t index : m_quadIndexesEnd) + { + quadblocks[index].SetCheckpoint(static_cast(m_end)); + } const size_t ckptCount = checkpoints.size(); for (size_t i = 0; i < ckptCount; i++) From afce3795ca36a86978abdd6f2c32fb622d9899d5 Mon Sep 17 00:00:00 2001 From: Blounard Date: Thu, 5 Mar 2026 22:38:36 +0100 Subject: [PATCH 2/5] split path connections added down and up connections for left and right paths also added the resetfilter, forgot in the previous commit --- src/level.cpp | 15 +++++++++++++++ src/path.cpp | 11 +++++++++++ src/path.h | 2 ++ 3 files changed, 28 insertions(+) diff --git a/src/level.cpp b/src/level.cpp index 5be77a8..25bfc64 100644 --- a/src/level.cpp +++ b/src/level.cpp @@ -202,6 +202,7 @@ bool Level::GenerateCheckpoints() for (const Path& path : m_checkpointPaths) { if (!path.IsReady()) { return false; } } + ResetFilter(); size_t checkpointIndex = 0; std::vector linkNodeIndexes; std::vector> pathCheckpoints; @@ -246,6 +247,20 @@ bool Level::GenerateCheckpoints() } } + for (Path& path : m_checkpointPaths) + { + const Checkpoint& middleStart = m_checkpoints[path.GetStart()]; + const Checkpoint& middleEnd = m_checkpoints[path.GetEnd()]; + + Path* sides[2] = { path.GetLeft(), path.GetRight() }; + for (Path* side : sides) + { + if (!side) { continue; } + m_checkpoints[side->GetStart()].UpdateDown(middleStart.GetDown()); + m_checkpoints[side->GetEnd()].UpdateUp(middleEnd.GetUp()); + } + } + // Cap the number of checkpoints to 255 const size_t MAX_CHECKPOINTS = 255; if (m_checkpoints.size() > MAX_CHECKPOINTS) diff --git a/src/path.cpp b/src/path.cpp index dfdf2b0..ca87176 100644 --- a/src/path.cpp +++ b/src/path.cpp @@ -88,6 +88,17 @@ size_t Path::GetEnd() const return m_end; } +Path* Path::GetLeft() +{ + return m_left; +} + +Path* Path::GetRight() +{ + return m_right; +} + + std::vector& Path::GetStartIndexes() { return m_quadIndexesStart; diff --git a/src/path.h b/src/path.h index d87a0bd..e5a283a 100644 --- a/src/path.h +++ b/src/path.h @@ -17,6 +17,8 @@ class Path size_t GetIndex() const; size_t GetStart() const; size_t GetEnd() const; + Path* GetLeft(); + Path* GetRight(); std::vector& GetStartIndexes(); std::vector& GetEndIndexes(); std::vector& GetIgnoreIndexes(); From ec1deb7030d547dc1d430df763120a334f566cb7 Mon Sep 17 00:00:00 2001 From: Blounard Date: Thu, 12 Mar 2026 15:32:33 +0100 Subject: [PATCH 3/5] Crashfix Fixed a crash when trying to delete a Path when there is no path --- src/levelui.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/levelui.cpp b/src/levelui.cpp index 177c684..80944bc 100644 --- a/src/levelui.cpp +++ b/src/levelui.cpp @@ -724,10 +724,12 @@ void Level::RenderUI(Renderer& renderer) m_checkpointPaths.push_back(Path(m_checkpointPaths.size())); } ImGui::SameLine(); + ImGui::BeginDisabled(m_checkpointPaths.empty()); if (ImGui::Button("Delete Path")) { m_checkpointPaths.pop_back(); } + ImGui::EndDisabled(); bool ready = !m_checkpointPaths.empty(); for (const Path& path : m_checkpointPaths) From 4bf39a77d3049e6b2cf947422de20e737c948a3f Mon Sep 17 00:00:00 2001 From: Blounard Date: Thu, 12 Mar 2026 16:29:45 +0100 Subject: [PATCH 4/5] Warning message When generating path, and there is an overlap detected, a warning message is displayed under the generate button. --- python_bindings/cte_bindings.cpp | 2 +- src/level.cpp | 5 +++-- src/levelui.cpp | 16 +++++++++++++++- src/path.cpp | 7 +++---- src/path.h | 2 +- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/python_bindings/cte_bindings.cpp b/python_bindings/cte_bindings.cpp index dc13f31..bcc3ead 100644 --- a/python_bindings/cte_bindings.cpp +++ b/python_bindings/cte_bindings.cpp @@ -446,7 +446,7 @@ void init_crashteameditor(py::module_& m) .def("is_ready", &Path::IsReady) .def("set_index", &Path::SetIndex) .def("update_dist", &Path::UpdateDist, py::arg("dist"), py::arg("ref_point"), py::arg("checkpoints")) - .def("generate_path", &Path::GeneratePath, py::arg("path_start_index"), py::arg("quadblocks")) + .def("generate_path", &Path::GeneratePath, py::arg("path_start_index"), py::arg("quadblocks"), py::arg("overlap")) .def_property("color", [](const Path& p) { return p.GetColor(); }, [](Path& p, const Color& color) { p.SetColor(color); }) diff --git a/src/level.cpp b/src/level.cpp index 25bfc64..7a49256 100644 --- a/src/level.cpp +++ b/src/level.cpp @@ -206,9 +206,10 @@ bool Level::GenerateCheckpoints() size_t checkpointIndex = 0; std::vector linkNodeIndexes; std::vector> pathCheckpoints; + bool overlap = false; for (Path& path : m_checkpointPaths) { - pathCheckpoints.push_back(path.GeneratePath(checkpointIndex, m_quadblocks)); + pathCheckpoints.push_back(path.GeneratePath(checkpointIndex, m_quadblocks, overlap)); checkpointIndex += pathCheckpoints.back().size(); linkNodeIndexes.push_back(path.GetStart()); linkNodeIndexes.push_back(path.GetEnd()); @@ -405,7 +406,7 @@ bool Level::GenerateCheckpoints() } UpdateRenderCheckpointData(); - return true; + return !overlap; } diff --git a/src/levelui.cpp b/src/levelui.cpp index 80944bc..6207aea 100644 --- a/src/levelui.cpp +++ b/src/levelui.cpp @@ -738,9 +738,23 @@ void Level::RenderUI(Renderer& renderer) } ImGui::BeginDisabled(!ready); static ButtonUI generateButton; + static bool showWarning = false; + static std::chrono::time_point warningStart; if (generateButton.Show("Generate", "Checkpoints successfully generated.", false)) { - GenerateCheckpoints(); + if (!GenerateCheckpoints()) + { + showWarning = true; + warningStart = std::chrono::steady_clock::now(); + } + } + if (showWarning) + { + auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - warningStart).count(); + if (elapsed < 5) + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Warning: some paths are overlapping.\nCheck console for details."); + else + showWarning = false; } ImGui::EndDisabled(); ImGui::TreePop(); diff --git a/src/path.cpp b/src/path.cpp index ca87176..6a0e5b3 100644 --- a/src/path.cpp +++ b/src/path.cpp @@ -141,7 +141,7 @@ void Path::UpdateDist(float dist, const Vec3& refPoint, std::vector& if (m_right) { m_right->UpdateDist(dist, checkpoints[m_end].GetPos(), checkpoints); } } -std::vector Path::GeneratePath(size_t pathStartIndex, std::vector& quadblocks) +std::vector Path::GeneratePath(size_t pathStartIndex, std::vector& quadblocks, bool& overlap) { /* Begin from the start point, find all neighbour quadblocks. @@ -185,7 +185,6 @@ std::vector Path::GeneratePath(size_t pathStartIndex, std::vector Path::GeneratePath(size_t pathStartIndex, std::vector leftCheckpoints, rightCheckpoints; if (m_left) { - leftCheckpoints = m_left->GeneratePath(pathStartIndex, quadblocks); + leftCheckpoints = m_left->GeneratePath(pathStartIndex, quadblocks, overlap); checkpoints.back().UpdateLeft(leftCheckpoints.back().GetIndex()); checkpoints.front().UpdateLeft(leftCheckpoints.front().GetIndex()); leftCheckpoints.back().UpdateRight(checkpoints.back().GetIndex()); @@ -407,7 +406,7 @@ std::vector Path::GeneratePath(size_t pathStartIndex, std::vectorGeneratePath(pathStartIndex, quadblocks); + rightCheckpoints = m_right->GeneratePath(pathStartIndex, quadblocks, overlap); checkpoints.back().UpdateRight(rightCheckpoints.back().GetIndex()); checkpoints.front().UpdateRight(rightCheckpoints.front().GetIndex()); rightCheckpoints.back().UpdateLeft(checkpoints.back().GetIndex()); diff --git a/src/path.h b/src/path.h index e5a283a..45e04eb 100644 --- a/src/path.h +++ b/src/path.h @@ -27,7 +27,7 @@ class Path void SetColor(const Color& color); void SetIndex(size_t index); void UpdateDist(float dist, const Vec3& refPoint, std::vector& checkpoints); - std::vector GeneratePath(size_t pathStartIndex, std::vector& quadblocks); + std::vector GeneratePath(size_t pathStartIndex, std::vector& quadblocks, bool& overlap); void RenderUI(const std::string& title, const std::vector& quadblocks, const std::string& searchQuery, bool& insertAbove, bool& removePath, const std::vector& selectedIndexes, bool mainPath); void ToJson(nlohmann::json& json, const std::vector& quadblocks) const; void FromJson(const nlohmann::json& json, const std::vector& quadblocks); From 2d5384b2777ce6d2a23061e327ee35ba145dbfbe Mon Sep 17 00:00:00 2001 From: Blounard Date: Thu, 12 Mar 2026 16:41:36 +0100 Subject: [PATCH 5/5] small fix Reset checkpoints from quad before generating path checkepoints. This is because the behavior now depends if some quads have already been assigned a checkpoint value, so this ensure determinism --- src/level.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/level.cpp b/src/level.cpp index 7a49256..29f74cd 100644 --- a/src/level.cpp +++ b/src/level.cpp @@ -203,6 +203,10 @@ bool Level::GenerateCheckpoints() for (const Path& path : m_checkpointPaths) { if (!path.IsReady()) { return false; } } ResetFilter(); + for (size_t i = 0; i < m_quadblocks.size(); i++) + { + m_quadblocks[i].SetCheckpoint(-1); + } size_t checkpointIndex = 0; std::vector linkNodeIndexes; std::vector> pathCheckpoints;