Skip to content

Commit dff8fa1

Browse files
committed
Detect disconnected tile groups and report cell refs from there
Cell refs placed at cooridnates far from the rest are potential content issues.
1 parent 1763928 commit dff8fa1

File tree

1 file changed

+142
-28
lines changed

1 file changed

+142
-28
lines changed

apps/navmeshtool/worldspacedata.cpp

Lines changed: 142 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333

3434
#include <algorithm>
3535
#include <memory>
36-
#include <ranges>
3736
#include <sstream>
3837
#include <stdexcept>
3938
#include <string>
@@ -52,6 +51,7 @@ namespace NavMeshTool
5251
using DetourNavigator::HeightfieldSurface;
5352
using DetourNavigator::ObjectId;
5453
using DetourNavigator::ObjectTransform;
54+
using DetourNavigator::TilesPositionsRange;
5555

5656
struct CellRef
5757
{
@@ -72,6 +72,25 @@ namespace NavMeshTool
7272
}
7373
};
7474

75+
struct AddedCellRef
76+
{
77+
std::string mCell;
78+
CellRef mCellRef;
79+
TilesPositionsRange mRange;
80+
};
81+
82+
struct WriteArray
83+
{
84+
const float (&mValue)[3];
85+
86+
friend inline std::ostream& operator<<(std::ostream& stream, const WriteArray& value)
87+
{
88+
for (std::size_t i = 0; i < 2; ++i)
89+
stream << value.mValue[i] << ", ";
90+
return stream << value.mValue[2];
91+
}
92+
};
93+
7594
ESM::RecNameInts getType(const EsmLoader::EsmData& esmData, const ESM::RefId& refId)
7695
{
7796
const auto it = std::lower_bound(
@@ -120,7 +139,7 @@ namespace NavMeshTool
120139

121140
Log(Debug::Debug) << "Prepared " << cellRefs.size() << " unique cell refs";
122141

123-
for (CellRef& cellRef : cellRefs)
142+
for (const CellRef& cellRef : cellRefs)
124143
{
125144
VFS::Path::Normalized model(getModel(esmData, cellRef.mRefId, cellRef.mType));
126145
if (model.empty())
@@ -154,7 +173,7 @@ namespace NavMeshTool
154173
case ESM::REC_CONT:
155174
case ESM::REC_DOOR:
156175
case ESM::REC_STAT:
157-
f(BulletObject(std::move(shapeInstance), cellRef.mPos, cellRef.mScale));
176+
f(BulletObject(std::move(shapeInstance), cellRef.mPos, cellRef.mScale), cellRef);
158177
break;
159178
default:
160179
break;
@@ -233,6 +252,88 @@ namespace NavMeshTool
233252
<< " fileHash=" << Misc::StringUtils::toHex(shape.getInstance()->mFileHash);
234253
return stream.str();
235254
}
255+
256+
void detectDisconnectedTileGroups(ESM::RefId worldspace,
257+
const std::map<osg::Vec2i, DetourNavigator::ChangeType>& changedTiles,
258+
std::span<const AddedCellRef> cellRefs)
259+
{
260+
if (changedTiles.empty())
261+
return;
262+
263+
std::deque<osg::Vec2i> queue;
264+
std::map<osg::Vec2i, std::size_t> positionToComponent;
265+
std::size_t componentIndex = 0;
266+
267+
for (const auto& [initial, changeType] : changedTiles)
268+
{
269+
if (positionToComponent.contains(initial))
270+
continue;
271+
272+
queue.push_back(initial);
273+
positionToComponent.emplace(initial, componentIndex);
274+
275+
while (!queue.empty())
276+
{
277+
const osg::Vec2i position = queue.front();
278+
queue.pop_front();
279+
280+
for (int x = position.x() - 1; x <= position.x() + 1; ++x)
281+
{
282+
for (int y = position.y() - 1; y <= position.y() + 1; ++y)
283+
{
284+
const osg::Vec2i candidate(x, y);
285+
286+
if (candidate == position)
287+
continue;
288+
289+
if (!changedTiles.contains(candidate))
290+
continue;
291+
292+
const auto it = positionToComponent.find(candidate);
293+
294+
if (it != positionToComponent.end())
295+
continue;
296+
297+
queue.push_back(candidate);
298+
positionToComponent.emplace_hint(it, candidate, componentIndex);
299+
}
300+
}
301+
}
302+
303+
++componentIndex;
304+
}
305+
306+
if (componentIndex <= 1)
307+
return;
308+
309+
Log(Debug::Warning) << "Found " << componentIndex << " disconnected tile groups";
310+
311+
std::vector<std::size_t> cellRefsPerComponent(componentIndex);
312+
313+
for (const AddedCellRef& v : cellRefs)
314+
++cellRefsPerComponent[positionToComponent.at(v.mRange.mBegin)];
315+
316+
const std::size_t largestComponent
317+
= std::max_element(cellRefsPerComponent.begin(), cellRefsPerComponent.end())
318+
- cellRefsPerComponent.begin();
319+
320+
for (const AddedCellRef& v : cellRefs)
321+
{
322+
const std::size_t component = positionToComponent.at(v.mRange.mBegin);
323+
324+
if (component == largestComponent)
325+
continue;
326+
327+
Log(Debug::Warning) << "CellRef belongs to not largest disconnected tile group:"
328+
<< " worldspace=" << worldspace << " cell=\"" << v.mCell << "\""
329+
<< " ref_num=" << v.mCellRef.mRefNum << " ref_id=" << v.mCellRef.mRefId
330+
<< " scale=" << v.mCellRef.mScale << " pos=" << WriteArray{ v.mCellRef.mPos.pos }
331+
<< " rot=" << WriteArray{ v.mCellRef.mPos.rot }
332+
<< " begin_tile=" << v.mRange.mBegin.x() << ", " << v.mRange.mBegin.y()
333+
<< " end_tile=" << v.mRange.mEnd.x() << ", " << v.mRange.mEnd.y()
334+
<< " component=" << component;
335+
}
336+
}
236337
}
237338

238339
WorldspaceData::WorldspaceData(ESM::RefId worldspace, const DetourNavigator::RecastSettings& settings)
@@ -301,6 +402,7 @@ namespace NavMeshTool
301402
manager.setWorldspace(worldspace, guard.get());
302403

303404
std::size_t objectsCounter = 0;
405+
std::vector<AddedCellRef> addedCellRefs;
304406

305407
for (std::size_t i = 0; i < cells.size(); ++i)
306408
{
@@ -333,36 +435,44 @@ namespace NavMeshTool
333435
manager.addWater(cellPosition, std::numeric_limits<int>::max(), cell.mWater, guard.get());
334436
}
335437

336-
forEachObject(cell, esmData, vfs, bulletShapeManager, readers, [&](BulletObject object) {
337-
if (object.getShapeInstance()->mVisualCollisionType != Resource::VisualCollisionType::None)
338-
return;
438+
forEachObject(
439+
cell, esmData, vfs, bulletShapeManager, readers, [&](BulletObject object, const CellRef& cellRef) {
440+
if (object.getShapeInstance()->mVisualCollisionType != Resource::VisualCollisionType::None)
441+
return;
339442

340-
const btTransform& transform = object.getCollisionObject().getWorldTransform();
341-
const btAABB aabb = BulletHelpers::getAabb(*object.getCollisionObject().getCollisionShape(), transform);
342-
mergeOrAssign(aabb, data.mAabb, data.mAabbInitialized);
343-
if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get())
344-
data.mAabb.merge(BulletHelpers::getAabb(*avoid, transform));
443+
const btTransform& transform = object.getCollisionObject().getWorldTransform();
444+
const btAABB aabb
445+
= BulletHelpers::getAabb(*object.getCollisionObject().getCollisionShape(), transform);
446+
mergeOrAssign(aabb, data.mAabb, data.mAabbInitialized);
447+
if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get())
448+
data.mAabb.merge(BulletHelpers::getAabb(*avoid, transform));
345449

346-
const ObjectId objectId(++objectsCounter);
347-
const CollisionShape shape(object.getShapeInstance(), *object.getCollisionObject().getCollisionShape(),
348-
object.getObjectTransform());
450+
const ObjectId objectId(++objectsCounter);
451+
const CollisionShape shape(object.getShapeInstance(),
452+
*object.getCollisionObject().getCollisionShape(), object.getObjectTransform());
349453

350-
if (!manager.addObject(objectId, shape, transform, DetourNavigator::AreaType_ground, guard.get()))
351-
throw std::logic_error(
352-
makeAddObjectErrorMessage(objectId, DetourNavigator::AreaType_ground, shape));
353-
354-
if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get())
355-
{
356-
const ObjectId avoidObjectId(++objectsCounter);
357-
const CollisionShape avoidShape(object.getShapeInstance(), *avoid, object.getObjectTransform());
358-
if (!manager.addObject(
359-
avoidObjectId, avoidShape, transform, DetourNavigator::AreaType_null, guard.get()))
454+
if (!manager.addObject(objectId, shape, transform, DetourNavigator::AreaType_ground, guard.get()))
360455
throw std::logic_error(
361-
makeAddObjectErrorMessage(avoidObjectId, DetourNavigator::AreaType_null, avoidShape));
362-
}
456+
makeAddObjectErrorMessage(objectId, DetourNavigator::AreaType_ground, shape));
363457

364-
data.mObjects.emplace_back(std::move(object));
365-
});
458+
addedCellRefs.push_back(AddedCellRef{
459+
.mCell = cell.getDescription(),
460+
.mCellRef = cellRef,
461+
.mRange = makeTilesPositionsRange(shape.getShape(), transform, settings.mRecast),
462+
});
463+
464+
if (const btCollisionShape* avoid = object.getShapeInstance()->mAvoidCollisionShape.get())
465+
{
466+
const ObjectId avoidObjectId(++objectsCounter);
467+
const CollisionShape avoidShape(object.getShapeInstance(), *avoid, object.getObjectTransform());
468+
if (!manager.addObject(
469+
avoidObjectId, avoidShape, transform, DetourNavigator::AreaType_null, guard.get()))
470+
throw std::logic_error(
471+
makeAddObjectErrorMessage(avoidObjectId, DetourNavigator::AreaType_null, avoidShape));
472+
}
473+
474+
data.mObjects.emplace_back(std::move(object));
475+
});
366476

367477
if (writeBinaryLog)
368478
serializeToStderr(ProcessedCells{ static_cast<std::uint64_t>(i + 1) });
@@ -373,6 +483,10 @@ namespace NavMeshTool
373483
}
374484

375485
const std::map<osg::Vec2i, DetourNavigator::ChangeType> changedTiles = manager.takeChangedTiles(guard.get());
486+
487+
if (worldspace != ESM::Cell::sDefaultWorldspaceId)
488+
detectDisconnectedTileGroups(worldspace, changedTiles, addedCellRefs);
489+
376490
data.mTiles.reserve(changedTiles.size());
377491
std::ranges::transform(changedTiles, std::back_inserter(data.mTiles), [](const auto& v) { return v.first; });
378492

0 commit comments

Comments
 (0)