mirror of
https://github.com/Mauler125/r5sdk.git
synced 2025-02-09 19:15:03 +01:00
Recast: implement per-traverse anim type disjoint set linking
Previously we handles each table the same. The updated algorithm now has a disjoint set for each traverse anim type, and selectively determines which poly's are reachable based on the link's traverse type connecting them. All left to be done here is making a correct lookup table, which is currently defined as s_traverseAnimTraverseFlags.
This commit is contained in:
parent
91d36cf007
commit
c0ff0e6be9
@ -837,11 +837,11 @@ bool Editor::createTraverseLinks()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Editor::createStaticPathingData()
|
||||
bool Editor::createStaticPathingData(const dtTraverseTableCreateParams* params)
|
||||
{
|
||||
if (!m_navMesh) return false;
|
||||
if (!params->nav) return false;
|
||||
|
||||
if (!dtCreateDisjointPolyGroups(m_navMesh, m_djs))
|
||||
if (!dtCreateDisjointPolyGroups(params))
|
||||
{
|
||||
m_ctx->log(RC_LOG_ERROR, "createStaticPathingData: Failed to build disjoint poly groups.");
|
||||
return false;
|
||||
@ -856,17 +856,19 @@ bool Editor::createStaticPathingData()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Editor::updateStaticPathingData()
|
||||
bool Editor::updateStaticPathingData(const dtTraverseTableCreateParams* params)
|
||||
{
|
||||
if (!m_navMesh) return false;
|
||||
if (!params->nav) return false;
|
||||
|
||||
if (!dtUpdateDisjointPolyGroups(m_navMesh, m_djs))
|
||||
const int numTraverseTables = NavMesh_GetTraverseTableCountForNavMeshType(m_selectedNavMeshType);
|
||||
|
||||
if (!dtUpdateDisjointPolyGroups(params))
|
||||
{
|
||||
m_ctx->log(RC_LOG_ERROR, "updateStaticPathingData: Failed to update disjoint poly groups.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!dtCreateTraverseTableData(m_navMesh, m_djs, NavMesh_GetTraverseTableCountForNavMeshType(m_selectedNavMeshType)))
|
||||
if (!dtCreateTraverseTableData(params))
|
||||
{
|
||||
m_ctx->log(RC_LOG_ERROR, "updateStaticPathingData: Failed to build traverse table data.");
|
||||
return false;
|
||||
@ -875,10 +877,52 @@ bool Editor::updateStaticPathingData()
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: this lookup table isn't correct, needs to be fixed.
|
||||
static const int s_traverseAnimTraverseFlags[TraverseAnimType_e::ANIMTYPE_COUNT] = {
|
||||
0x0000013F, // ANIMTYPE_HUMAN
|
||||
0x0000013F, // ANIMTYPE_SPECTRE
|
||||
0x00000000, // ANIMTYPE_STALKER
|
||||
0x0033FFFF, // ANIMTYPE_FRAG_DRONE
|
||||
0x00000000, // ANIMTYPE_PILOT
|
||||
0x00000000, // ANIMTYPE_PROWLER
|
||||
0x00000000, // ANIMTYPE_SUPER_SPECTRE
|
||||
0x00000000, // ANIMTYPE_TITAN
|
||||
0x00000000, // ANIMTYPE_GOLIATH
|
||||
};
|
||||
|
||||
bool animTypeSupportsTraverseLink(const dtTraverseTableCreateParams* params, const dtLink* link, const int tableIndex)
|
||||
{
|
||||
// TODO: always link off-mesh connected polygon islands together?
|
||||
// Research needed.
|
||||
if (link->reverseLink == DT_NULL_TRAVERSE_REVERSE_LINK)
|
||||
return true;
|
||||
|
||||
const NavMeshType_e navMeshType = (NavMeshType_e)params->navMeshType;
|
||||
|
||||
// Only the _small NavMesh has more than 1 table.
|
||||
const int traverseAnimType = navMeshType == NAVMESH_SMALL
|
||||
? tableIndex
|
||||
: NavMesh_GetFirstTraverseAnimTypeForType(navMeshType);
|
||||
|
||||
return rdBitCellBit(link->traverseType) & s_traverseAnimTraverseFlags[traverseAnimType];
|
||||
}
|
||||
|
||||
void Editor::createTraverseTableParams(dtTraverseTableCreateParams* params)
|
||||
{
|
||||
params->nav = m_navMesh;
|
||||
params->sets = m_djs;
|
||||
params->tableCount = NavMesh_GetTraverseTableCountForNavMeshType(m_selectedNavMeshType);
|
||||
params->navMeshType = m_selectedNavMeshType;
|
||||
params->canTraverse = animTypeSupportsTraverseLink;
|
||||
}
|
||||
|
||||
void Editor::buildStaticPathingData()
|
||||
{
|
||||
createStaticPathingData();
|
||||
updateStaticPathingData();
|
||||
dtTraverseTableCreateParams params;
|
||||
createTraverseTableParams(¶ms);
|
||||
|
||||
createStaticPathingData(¶ms);
|
||||
updateStaticPathingData(¶ms);
|
||||
}
|
||||
|
||||
void Editor::updateToolStates(const float dt)
|
||||
|
@ -530,17 +530,17 @@ bool Editor_SoloMesh::handleBuild()
|
||||
}
|
||||
}
|
||||
|
||||
dtDisjointSet data;
|
||||
//dtDisjointSet data;
|
||||
|
||||
if (!dtCreateDisjointPolyGroups(m_navMesh, data))
|
||||
{
|
||||
m_ctx->log(RC_LOG_ERROR, "buildNavigation: Failed to build disjoint poly groups.");
|
||||
}
|
||||
//if (!dtCreateDisjointPolyGroups(m_navMesh, data))
|
||||
//{
|
||||
// m_ctx->log(RC_LOG_ERROR, "buildNavigation: Failed to build disjoint poly groups.");
|
||||
//}
|
||||
|
||||
if (!dtCreateTraverseTableData(m_navMesh, data, traverseTableCount))
|
||||
{
|
||||
m_ctx->log(RC_LOG_ERROR, "buildNavigation: Failed to build traversal table data.");
|
||||
}
|
||||
//if (!dtCreateTraverseTableData(m_navMesh, data, traverseTableCount))
|
||||
//{
|
||||
// m_ctx->log(RC_LOG_ERROR, "buildNavigation: Failed to build traversal table data.");
|
||||
//}
|
||||
|
||||
m_ctx->stopTimer(RC_TIMER_TOTAL);
|
||||
|
||||
|
@ -222,7 +222,11 @@ void NavMeshPruneTool::handleMenu()
|
||||
if (ImGui::Button("Prune Unselected"))
|
||||
{
|
||||
disableUnvisitedPolys(nav, m_flags);
|
||||
m_editor->updateStaticPathingData();
|
||||
dtTraverseTableCreateParams params;
|
||||
|
||||
m_editor->createTraverseTableParams(¶ms);
|
||||
m_editor->updateStaticPathingData(¶ms);
|
||||
|
||||
delete m_flags;
|
||||
m_flags = 0;
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ protected:
|
||||
EditorToolState* m_toolStates[MAX_TOOLS];
|
||||
|
||||
BuildContext* m_ctx;
|
||||
dtDisjointSet m_djs;
|
||||
dtDisjointSet m_djs[DT_MAX_TRAVERSE_TABLES];
|
||||
|
||||
EditorDebugDraw m_dd;
|
||||
unsigned int m_navMeshDrawFlags;
|
||||
@ -250,10 +250,13 @@ public:
|
||||
|
||||
void connectTileTraverseLinks(dtMeshTile* const baseTile, const bool linkToNeighbor); // Make private.
|
||||
bool createTraverseLinks();
|
||||
|
||||
void createTraverseTableParams(dtTraverseTableCreateParams* params);
|
||||
|
||||
void buildStaticPathingData();
|
||||
|
||||
bool createStaticPathingData();
|
||||
bool updateStaticPathingData();
|
||||
bool createStaticPathingData(const dtTraverseTableCreateParams* params);
|
||||
bool updateStaticPathingData(const dtTraverseTableCreateParams* params);
|
||||
|
||||
private:
|
||||
// Explicitly disabled copy constructor and copy assignment operator.
|
||||
|
@ -120,6 +120,19 @@ public:
|
||||
init(size);
|
||||
}
|
||||
|
||||
void copy(dtDisjointSet& other)
|
||||
{
|
||||
other.rank.resize(rank.size());
|
||||
|
||||
for (int i = 0; i < other.rank.size(); i++)
|
||||
other.rank[i] = rank[i];
|
||||
|
||||
other.parent.resize(parent.size());
|
||||
|
||||
for (int i = 0; i < other.parent.size(); i++)
|
||||
other.parent[i] = parent[i];
|
||||
}
|
||||
|
||||
void init(const int size)
|
||||
{
|
||||
rank.resize(size);
|
||||
@ -175,26 +188,39 @@ private:
|
||||
mutable rdIntArray parent;
|
||||
};
|
||||
|
||||
/// Builds navigation mesh disjoint poly groups from the provided navmesh.
|
||||
struct dtLink;
|
||||
|
||||
/// Parameters used to build traverse links.
|
||||
/// @ingroup detour
|
||||
/// @param[in] nav The navigation mesh to use.
|
||||
/// @param[Out] disjoint The disjoint set data.
|
||||
struct dtTraverseTableCreateParams
|
||||
{
|
||||
dtNavMesh* nav; ///< The navmesh.
|
||||
dtDisjointSet* sets; ///< The disjoint polygroup sets.
|
||||
int tableCount; ///< The number of traverse tables this navmesh should contain.
|
||||
int navMeshType; ///< The navmesh type [_small, _extra_large].
|
||||
|
||||
///< The user installed callback which is used to determine if an animType
|
||||
/// an use this traverse link.
|
||||
bool (*canTraverse)(const dtTraverseTableCreateParams* params, const dtLink* link, const int tableIndex);
|
||||
};
|
||||
|
||||
/// Builds navigation mesh disjoint poly groups from the provided parameters.
|
||||
/// @ingroup detour
|
||||
/// @param[in] params The build parameters.
|
||||
/// @return True if the disjoint set data was successfully created.
|
||||
bool dtCreateDisjointPolyGroups(dtNavMesh* nav, dtDisjointSet& disjoint);
|
||||
bool dtCreateDisjointPolyGroups(const dtTraverseTableCreateParams* params);
|
||||
|
||||
/// Updates navigation mesh disjoint poly groups from the provided navmesh.
|
||||
/// Updates navigation mesh disjoint poly groups from the provided parameters.
|
||||
/// @ingroup detour
|
||||
/// @param[in] nav The navigation mesh to use.
|
||||
/// @param[Out] disjoint The disjoint set data.
|
||||
/// @param[in] params The build parameters.
|
||||
/// @return True if the disjoint set data was successfully updated.
|
||||
bool dtUpdateDisjointPolyGroups(dtNavMesh* nav, dtDisjointSet& disjoint);
|
||||
bool dtUpdateDisjointPolyGroups(const dtTraverseTableCreateParams* params);
|
||||
|
||||
/// Builds navigation mesh static traverse table from the provided navmesh.
|
||||
/// Builds navigation mesh static traverse table from the provided parameters.
|
||||
/// @ingroup detour
|
||||
/// @param[in] nav The navigation mesh to use.
|
||||
/// @param[in] disjoint The disjoint set data.
|
||||
/// @param[in] params The build parameters.
|
||||
/// @return True if the static traverse table was successfully created.
|
||||
bool dtCreateTraverseTableData(dtNavMesh* nav, const dtDisjointSet& disjoint, const int tableCount);
|
||||
bool dtCreateTraverseTableData(const dtTraverseTableCreateParams* params);
|
||||
|
||||
/// Builds navigation mesh tile data from the provided tile creation data.
|
||||
/// @ingroup detour
|
||||
|
@ -248,14 +248,17 @@ static void setPolyGroupsTraversalReachability(int* const tableData, const int n
|
||||
tableData[index] &= ~value;
|
||||
}
|
||||
|
||||
bool dtCreateDisjointPolyGroups(dtNavMesh* nav, dtDisjointSet& disjoint)
|
||||
bool dtCreateDisjointPolyGroups(const dtTraverseTableCreateParams* params)
|
||||
{
|
||||
dtNavMesh* nav = params->nav;
|
||||
dtDisjointSet& set = params->sets[0];
|
||||
|
||||
rdAssert(nav);
|
||||
|
||||
// Reserve the first poly groups
|
||||
// 0 = DT_NULL_POLY_GROUP.
|
||||
// 1 = DT_UNLINKED_POLY_GROUP.
|
||||
disjoint.init(DT_FIRST_USABLE_POLY_GROUP);
|
||||
set.init(DT_FIRST_USABLE_POLY_GROUP);
|
||||
|
||||
// Clear all labels.
|
||||
for (int i = 0; i < nav->getMaxTiles(); ++i)
|
||||
@ -317,14 +320,14 @@ bool dtCreateDisjointPolyGroups(dtNavMesh* nav, dtDisjointSet& disjoint)
|
||||
const bool noLinkedGroups = linkedGroups.empty();
|
||||
|
||||
if (noLinkedGroups)
|
||||
poly.groupId = (unsigned short)disjoint.insertNew();
|
||||
poly.groupId = (unsigned short)set.insertNew();
|
||||
else
|
||||
{
|
||||
const unsigned short rootGroup = *linkedGroups.begin();
|
||||
poly.groupId = rootGroup;
|
||||
|
||||
for (const int linkedGroup : linkedGroups)
|
||||
disjoint.setUnion(rootGroup, linkedGroup);
|
||||
set.setUnion(rootGroup, linkedGroup);
|
||||
}
|
||||
|
||||
if (!noLinkedGroups)
|
||||
@ -343,66 +346,20 @@ bool dtCreateDisjointPolyGroups(dtNavMesh* nav, dtDisjointSet& disjoint)
|
||||
dtPoly& poly = tile->polys[j];
|
||||
if (poly.groupId != DT_UNLINKED_POLY_GROUP)
|
||||
{
|
||||
const int id = disjoint.find(poly.groupId);
|
||||
const int id = set.find(poly.groupId);
|
||||
poly.groupId = (unsigned short)id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav->setPolyGroupcount(disjoint.getSetCount());
|
||||
nav->setPolyGroupcount(set.getSetCount());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool dtUpdateDisjointPolyGroups(dtNavMesh* nav, dtDisjointSet& disjoint)
|
||||
static void unionTraverseLinkedPolyGroups(const dtTraverseTableCreateParams* params, const int tableIndex)
|
||||
{
|
||||
// Third pass to mark all unlinked poly's.
|
||||
for (int i = 0; i < nav->getMaxTiles(); ++i)
|
||||
{
|
||||
dtMeshTile* tile = nav->getTile(i);
|
||||
if (!tile || !tile->header || !tile->dataSize) continue;
|
||||
const int pcount = tile->header->polyCount;
|
||||
for (int j = 0; j < pcount; j++)
|
||||
{
|
||||
dtPoly& poly = tile->polys[j];
|
||||
|
||||
// This poly isn't connected to anything, mark it so the game
|
||||
// won't consider this poly in path generation.
|
||||
if (poly.firstLink == DT_NULL_LINK)
|
||||
poly.groupId = DT_UNLINKED_POLY_GROUP;
|
||||
}
|
||||
}
|
||||
|
||||
// Gather all unique polygroups and map them to a contiguous range.
|
||||
std::map<unsigned short, unsigned short> groupMap;
|
||||
disjoint.init(DT_FIRST_USABLE_POLY_GROUP);
|
||||
|
||||
for (int i = 0; i < nav->getMaxTiles(); ++i)
|
||||
{
|
||||
dtMeshTile* tile = nav->getTile(i);
|
||||
if (!tile || !tile->header || !tile->dataSize) continue;
|
||||
const int pcount = tile->header->polyCount;
|
||||
for (int j = 0; j < pcount; j++)
|
||||
{
|
||||
dtPoly& poly = tile->polys[j];
|
||||
unsigned short oldId = poly.groupId;
|
||||
if (oldId != DT_UNLINKED_POLY_GROUP && groupMap.find(oldId) == groupMap.end())
|
||||
groupMap[oldId] = (unsigned short)disjoint.insertNew();
|
||||
}
|
||||
}
|
||||
|
||||
// Fourth pass to apply the new mapping to all polys.
|
||||
for (int i = 0; i < nav->getMaxTiles(); ++i)
|
||||
{
|
||||
dtMeshTile* tile = nav->getTile(i);
|
||||
if (!tile || !tile->header || !tile->dataSize) continue;
|
||||
const int pcount = tile->header->polyCount;
|
||||
for (int j = 0; j < pcount; j++)
|
||||
{
|
||||
dtPoly& poly = tile->polys[j];
|
||||
if (poly.groupId != DT_UNLINKED_POLY_GROUP)
|
||||
poly.groupId = groupMap[poly.groupId];
|
||||
}
|
||||
}
|
||||
dtNavMesh* nav = params->nav;
|
||||
dtDisjointSet& set = params->sets[tableIndex];
|
||||
|
||||
// Fifth pass to handle off-mesh connections.
|
||||
// note(amos): this has to happen after the first and second pass as these
|
||||
@ -431,7 +388,7 @@ bool dtUpdateDisjointPolyGroups(dtNavMesh* nav, dtDisjointSet& disjoint)
|
||||
|
||||
while (plink != DT_NULL_LINK)
|
||||
{
|
||||
const dtLink l = tile->links[plink];
|
||||
const dtLink& l = tile->links[plink];
|
||||
const dtMeshTile* t;
|
||||
const dtPoly* p;
|
||||
nav->getTileAndPolyByRefUnsafe(l.ref, &t, &p);
|
||||
@ -440,8 +397,8 @@ bool dtUpdateDisjointPolyGroups(dtNavMesh* nav, dtDisjointSet& disjoint)
|
||||
{
|
||||
if (firstGroupId == DT_NULL_POLY_GROUP)
|
||||
firstGroupId = p->groupId;
|
||||
else
|
||||
disjoint.setUnion(firstGroupId, p->groupId);
|
||||
else if (params->canTraverse(params, &l, tableIndex))
|
||||
set.setUnion(firstGroupId, p->groupId);
|
||||
}
|
||||
|
||||
plink = l.next;
|
||||
@ -486,21 +443,89 @@ bool dtUpdateDisjointPolyGroups(dtNavMesh* nav, dtDisjointSet& disjoint)
|
||||
rdAssert(landPoly->getType() != DT_POLYTYPE_OFFMESH_CONNECTION);
|
||||
rdAssert(landPoly->groupId != DT_UNLINKED_POLY_GROUP);
|
||||
|
||||
if (poly.groupId != landPoly->groupId)
|
||||
disjoint.setUnion(poly.groupId, landPoly->groupId);
|
||||
if (poly.groupId != landPoly->groupId && params->canTraverse(params, link, tableIndex))
|
||||
set.setUnion(poly.groupId, landPoly->groupId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav->setPolyGroupcount(disjoint.getSetCount());
|
||||
bool dtUpdateDisjointPolyGroups(const dtTraverseTableCreateParams* params)
|
||||
{
|
||||
dtNavMesh* nav = params->nav;
|
||||
dtDisjointSet& set = params->sets[0];
|
||||
|
||||
// Third pass to mark all unlinked poly's.
|
||||
for (int i = 0; i < nav->getMaxTiles(); ++i)
|
||||
{
|
||||
dtMeshTile* tile = nav->getTile(i);
|
||||
if (!tile || !tile->header || !tile->dataSize) continue;
|
||||
const int pcount = tile->header->polyCount;
|
||||
for (int j = 0; j < pcount; j++)
|
||||
{
|
||||
dtPoly& poly = tile->polys[j];
|
||||
|
||||
// This poly isn't connected to anything, mark it so the game
|
||||
// won't consider this poly in path generation.
|
||||
if (poly.firstLink == DT_NULL_LINK)
|
||||
poly.groupId = DT_UNLINKED_POLY_GROUP;
|
||||
}
|
||||
}
|
||||
|
||||
// Gather all unique polygroups and map them to a contiguous range.
|
||||
std::map<unsigned short, unsigned short> groupMap;
|
||||
set.init(DT_FIRST_USABLE_POLY_GROUP);
|
||||
|
||||
for (int i = 0; i < nav->getMaxTiles(); ++i)
|
||||
{
|
||||
dtMeshTile* tile = nav->getTile(i);
|
||||
if (!tile || !tile->header || !tile->dataSize) continue;
|
||||
const int pcount = tile->header->polyCount;
|
||||
for (int j = 0; j < pcount; j++)
|
||||
{
|
||||
dtPoly& poly = tile->polys[j];
|
||||
unsigned short oldId = poly.groupId;
|
||||
if (oldId != DT_UNLINKED_POLY_GROUP && groupMap.find(oldId) == groupMap.end())
|
||||
groupMap[oldId] = (unsigned short)set.insertNew();
|
||||
}
|
||||
}
|
||||
|
||||
// Fourth pass to apply the new mapping to all polys.
|
||||
for (int i = 0; i < nav->getMaxTiles(); ++i)
|
||||
{
|
||||
dtMeshTile* tile = nav->getTile(i);
|
||||
if (!tile || !tile->header || !tile->dataSize) continue;
|
||||
const int pcount = tile->header->polyCount;
|
||||
for (int j = 0; j < pcount; j++)
|
||||
{
|
||||
dtPoly& poly = tile->polys[j];
|
||||
if (poly.groupId != DT_UNLINKED_POLY_GROUP)
|
||||
poly.groupId = groupMap[poly.groupId];
|
||||
}
|
||||
}
|
||||
|
||||
// Copy base disjoint set results to sets for each traverse table.
|
||||
for (int i = 0; i < params->tableCount; i++)
|
||||
{
|
||||
dtDisjointSet& targetSet = params->sets[i];
|
||||
|
||||
if (i > 0) // Don't copy the base into itself.
|
||||
set.copy(targetSet);
|
||||
|
||||
unionTraverseLinkedPolyGroups(params, i);
|
||||
}
|
||||
|
||||
nav->setPolyGroupcount(set.getSetCount());
|
||||
return true;
|
||||
}
|
||||
|
||||
// todo(amos): remove param 'tableCount' and make struct 'dtTraverseTableCreateParams'
|
||||
bool dtCreateTraverseTableData(dtNavMesh* nav, const dtDisjointSet& disjoint, const int tableCount)
|
||||
bool dtCreateTraverseTableData(const dtTraverseTableCreateParams* params)
|
||||
{
|
||||
dtNavMesh* nav = params->nav;
|
||||
|
||||
const int polyGroupCount = nav->getPolyGroupCount();
|
||||
const int tableSize = dtCalcTraverseTableSize(polyGroupCount);
|
||||
const int tableCount = params->tableCount;
|
||||
|
||||
nav->freeTraverseTables();
|
||||
|
||||
@ -510,8 +535,6 @@ bool dtCreateTraverseTableData(dtNavMesh* nav, const dtDisjointSet& disjoint, co
|
||||
nav->setTraverseTableSize(tableSize);
|
||||
nav->setTraverseTableCount(tableCount);
|
||||
|
||||
// TODO: figure out which jump type belongs to which traverse anim type
|
||||
// and determine reachability from here...
|
||||
for (int i = 0; i < tableCount; i++)
|
||||
{
|
||||
int* const traverseTable = (int*)rdAlloc(sizeof(int)*tableSize, RD_ALLOC_PERM);
|
||||
@ -522,12 +545,14 @@ bool dtCreateTraverseTableData(dtNavMesh* nav, const dtDisjointSet& disjoint, co
|
||||
nav->setTraverseTable(i, traverseTable);
|
||||
memset(traverseTable, 0, sizeof(int)*tableSize);
|
||||
|
||||
const dtDisjointSet& set = params->sets[i];
|
||||
|
||||
for (unsigned short j = 0; j < polyGroupCount; j++)
|
||||
{
|
||||
for (unsigned short k = 0; k < polyGroupCount; k++)
|
||||
{
|
||||
// Only reachable if its the same polygroup or if they are linked!
|
||||
const bool isReachable = j == k || disjoint.find(j) == disjoint.find(k);
|
||||
const bool isReachable = j == k || set.find(j) == set.find(k);
|
||||
setPolyGroupsTraversalReachability(traverseTable, polyGroupCount, j, k, isReachable);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user