mirror of
https://github.com/Laupetin/OpenAssetTools.git
synced 2026-06-06 08:42:35 +00:00
refactor: remove extra classes from BSP.h and move vars to their respective files
This commit is contained in:
@@ -180,65 +180,10 @@ namespace BSP
|
|||||||
std::vector<BSPModel> models;
|
std::vector<BSPModel> models;
|
||||||
};
|
};
|
||||||
|
|
||||||
// BSPGameConstants:
|
enum BSPDefaultLights
|
||||||
// These values are hardcoded ingame and will break the map if they are changed
|
|
||||||
namespace BSPGameConstants
|
|
||||||
{
|
{
|
||||||
constexpr unsigned int MAX_COLLISION_VERTS = UINT16_MAX;
|
EMPTY_LIGHT_INDEX = 0,
|
||||||
|
SUN_LIGHT_INDEX = 1,
|
||||||
constexpr size_t MAX_AABB_TREE_CHILDREN = 128;
|
BSP_DEFAULT_LIGHT_COUNT = 2
|
||||||
|
};
|
||||||
enum BSPDefaultLights
|
|
||||||
{
|
|
||||||
EMPTY_LIGHT_INDEX = 0,
|
|
||||||
SUN_LIGHT_INDEX = 1,
|
|
||||||
BSP_DEFAULT_LIGHT_COUNT = 2
|
|
||||||
};
|
|
||||||
} // namespace BSPGameConstants
|
|
||||||
|
|
||||||
// BSPLinkingConstants:
|
|
||||||
// These values are BSP linking constants that are required for the link to be successful
|
|
||||||
namespace BSPLinkingConstants
|
|
||||||
{
|
|
||||||
constexpr const char* MISSING_IMAGE_NAME = ",mc/lambert1";
|
|
||||||
constexpr const char* COLOR_ONLY_IMAGE_NAME = ",mc/lambert1";
|
|
||||||
} // namespace BSPLinkingConstants
|
|
||||||
|
|
||||||
// BSPEditableConstants:
|
|
||||||
// These values are BSP constants that can be edited and may not break the linker/game if changed
|
|
||||||
namespace BSPEditableConstants
|
|
||||||
{
|
|
||||||
// Default xmodel values
|
|
||||||
// Unused as there is no support for xmodels right now
|
|
||||||
constexpr float DEFAULT_SMODEL_CULL_DIST = 10000.0f;
|
|
||||||
constexpr int DEFAULT_SMODEL_FLAGS = STATIC_MODEL_FLAG_NO_SHADOW;
|
|
||||||
constexpr int DEFAULT_SMODEL_LIGHT = 1;
|
|
||||||
constexpr int DEFAULT_SMODEL_REFLECTION_PROBE = 0;
|
|
||||||
|
|
||||||
// Default surface values
|
|
||||||
constexpr int DEFAULT_SURFACE_LIGHT = 1;
|
|
||||||
constexpr int DEFAULT_SURFACE_LIGHTMAP = 0;
|
|
||||||
constexpr int DEFAULT_SURFACE_REFLECTION_PROBE = 0;
|
|
||||||
constexpr int DEFAULT_SURFACE_FLAGS = 0;
|
|
||||||
|
|
||||||
// material flags determine the features of a surface
|
|
||||||
// unsure which flag type changes what right now
|
|
||||||
// -1 results in: no running, water splashes all the time, low friction, slanted angles make you slide very fast
|
|
||||||
// 1 results in: normal surface features, grenades work, seems normal
|
|
||||||
constexpr int MATERIAL_SURFACE_FLAGS = 1;
|
|
||||||
constexpr int MATERIAL_CONTENT_FLAGS = 1;
|
|
||||||
|
|
||||||
// terrain/world flags: does not change the type of terrain or what features they have
|
|
||||||
// from testing, as long at it isn't 0 things will work correctly
|
|
||||||
constexpr int LEAF_TERRAIN_CONTENTS = 1;
|
|
||||||
constexpr int WORLD_TERRAIN_CONTENTS = 1;
|
|
||||||
|
|
||||||
// lightgrid (global) lighting colour
|
|
||||||
// since lightgrids are not well understood, this colour is used for the R, G and B values right now
|
|
||||||
constexpr unsigned char LIGHTGRID_COLOUR = 128;
|
|
||||||
|
|
||||||
// Sunlight values
|
|
||||||
constexpr vec4_t SUNLIGHT_COLOR = {0.75f, 0.75f, 0.75f, 1.0f};
|
|
||||||
constexpr vec3_t SUNLIGHT_DIRECTION = {0.0f, 0.0f, 0.0f};
|
|
||||||
}; // namespace BSPEditableConstants
|
|
||||||
} // namespace BSP
|
} // namespace BSP
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
#include "../BSPUtil.h"
|
#include "../BSPUtil.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct uniqueMatData
|
||||||
|
{
|
||||||
|
size_t materialIndex;
|
||||||
|
std::vector<int> partitionIndexes;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr size_t MAX_AABB_TREE_CHILDREN = 128;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
namespace BSP
|
namespace BSP
|
||||||
{
|
{
|
||||||
ClipMapLinker::ClipMapLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context)
|
ClipMapLinker::ClipMapLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context)
|
||||||
@@ -202,179 +213,6 @@ namespace BSP
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void calculatePartitionAABB(clipMap_t* clipMap, CollisionPartition* partition, vec3_t& out_mins, vec3_t& out_maxs);
|
|
||||||
void addAABBTreeFromPartitions(
|
|
||||||
clipMap_t* clipMap, std::vector<int>& partitions, size_t* out_parentCount, size_t* out_parentStartIndex, int* out_treeContents);
|
|
||||||
|
|
||||||
void ClipMapLinker::loadSubModelCollision(clipMap_t* clipMap, BSPData* bsp)
|
|
||||||
{
|
|
||||||
// Submodels are used for the world and map ent collision (triggers, bomb zones, etc)
|
|
||||||
clipMap->numSubModels = static_cast<unsigned int>(bsp->models.size() + 1);
|
|
||||||
clipMap->cmodels = m_memory.Alloc<cmodel_t>(clipMap->numSubModels);
|
|
||||||
|
|
||||||
// first model is always the world model
|
|
||||||
for (unsigned int vertIdx = 0; vertIdx < clipMap->vertCount; vertIdx++)
|
|
||||||
{
|
|
||||||
vec3_t vertex = clipMap->verts[vertIdx];
|
|
||||||
if (vertIdx == 0)
|
|
||||||
{
|
|
||||||
clipMap->cmodels[0].mins = vertex;
|
|
||||||
clipMap->cmodels[0].maxs = vertex;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
BSPUtil::updateAABBWithPoint(vertex, clipMap->cmodels[0].mins, clipMap->cmodels[0].maxs);
|
|
||||||
}
|
|
||||||
clipMap->cmodels[0].radius = BSPUtil::distBetweenPoints(clipMap->cmodels[0].mins, clipMap->cmodels[0].maxs) / 2;
|
|
||||||
// The world sub model has no data apart from the bounds
|
|
||||||
clipMap->cmodels[0].leaf.firstCollAabbIndex = 0;
|
|
||||||
clipMap->cmodels[0].leaf.collAabbCount = 0;
|
|
||||||
clipMap->cmodels[0].leaf.brushContents = 0;
|
|
||||||
clipMap->cmodels[0].leaf.terrainContents = 0;
|
|
||||||
clipMap->cmodels[0].leaf.mins.x = 0.0f;
|
|
||||||
clipMap->cmodels[0].leaf.mins.y = 0.0f;
|
|
||||||
clipMap->cmodels[0].leaf.mins.z = 0.0f;
|
|
||||||
clipMap->cmodels[0].leaf.maxs.x = 0.0f;
|
|
||||||
clipMap->cmodels[0].leaf.maxs.y = 0.0f;
|
|
||||||
clipMap->cmodels[0].leaf.maxs.z = 0.0f;
|
|
||||||
clipMap->cmodels[0].leaf.leafBrushNode = 0;
|
|
||||||
clipMap->cmodels[0].leaf.cluster = 0;
|
|
||||||
clipMap->cmodels[0].info = nullptr;
|
|
||||||
|
|
||||||
for (size_t modelIdx = 0; modelIdx < bsp->models.size(); modelIdx++)
|
|
||||||
{
|
|
||||||
auto clipModel = &clipMap->cmodels[modelIdx + 1];
|
|
||||||
auto& bspModel = bsp->models.at(modelIdx);
|
|
||||||
|
|
||||||
if (bspModel.isGfxModel)
|
|
||||||
{
|
|
||||||
memset(clipModel, 0, sizeof(cmodel_t));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bspModel.surfaceCount != 0)
|
|
||||||
{
|
|
||||||
std::vector<int> partitionIndexes;
|
|
||||||
for (size_t surfIdx = 0; surfIdx < bspModel.surfaceCount; surfIdx++)
|
|
||||||
{
|
|
||||||
ColSurface& surf = collisionSurfaceVec.at(bsp->colWorld.staticSurfaces.size() + bspModel.surfaceIndex + surfIdx);
|
|
||||||
for (size_t partitionIdx = 0; partitionIdx < surf.partitionCount; partitionIdx++)
|
|
||||||
{
|
|
||||||
size_t clipMapPartitionIndex = surf.partitionStartIndex + partitionIdx;
|
|
||||||
partitionIndexes.emplace_back(clipMapPartitionIndex);
|
|
||||||
vec3_t mins;
|
|
||||||
vec3_t maxs;
|
|
||||||
calculatePartitionAABB(clipMap, &clipMap->partitions[clipMapPartitionIndex], mins, maxs);
|
|
||||||
if (surfIdx == 0 && partitionIdx == 0)
|
|
||||||
{
|
|
||||||
clipModel->mins = mins;
|
|
||||||
clipModel->maxs = maxs;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
BSPUtil::updateAABB(mins, maxs, clipModel->mins, clipModel->maxs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int terrainContents = 0;
|
|
||||||
size_t firstCollAabbIndex = 0;
|
|
||||||
size_t collAabbCount = 0;
|
|
||||||
addAABBTreeFromPartitions(clipMap, partitionIndexes, &collAabbCount, &firstCollAabbIndex, &terrainContents);
|
|
||||||
clipModel->leaf.terrainContents = terrainContents;
|
|
||||||
clipModel->leaf.firstCollAabbIndex = static_cast<uint16_t>(firstCollAabbIndex);
|
|
||||||
clipModel->leaf.collAabbCount = static_cast<uint16_t>(collAabbCount);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
clipModel->leaf.terrainContents = 0;
|
|
||||||
clipModel->leaf.firstCollAabbIndex = 0;
|
|
||||||
clipModel->leaf.collAabbCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bspModel.hasBrush)
|
|
||||||
{
|
|
||||||
|
|
||||||
BSPBoxBrush& bspBrush = bsp->colWorld.scriptBoxBrushes.at(bspModel.brushIndex);
|
|
||||||
vec3_t mins;
|
|
||||||
vec3_t maxs;
|
|
||||||
for (size_t vertIdx = 0; vertIdx < bspBrush.vertexCount; vertIdx++)
|
|
||||||
{
|
|
||||||
vec3_t* vertex = &clipMap->info.brushVerts[bspBrush.vertexIndex + vertIdx];
|
|
||||||
if (vertIdx == 0)
|
|
||||||
{
|
|
||||||
mins = *vertex;
|
|
||||||
maxs = *vertex;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
BSPUtil::updateAABBWithPoint(*vertex, mins, maxs);
|
|
||||||
}
|
|
||||||
|
|
||||||
clipModel->leaf.mins = mins;
|
|
||||||
clipModel->leaf.maxs = maxs;
|
|
||||||
clipModel->leaf.brushContents = bspBrush.contentFlags;
|
|
||||||
clipModel->leaf.leafBrushNode = static_cast<int>(brushNodeVec.size());
|
|
||||||
assert(clipModel->leaf.leafBrushNode != 0);
|
|
||||||
|
|
||||||
cLeafBrushNode_s brushNode;
|
|
||||||
brushNode.axis = 0;
|
|
||||||
brushNode.contents = bspBrush.contentFlags;
|
|
||||||
brushNode.leafBrushCount = 1;
|
|
||||||
brushNode.data.leaf.brushes = m_memory.Alloc<LeafBrush>(1);
|
|
||||||
brushNode.data.leaf.brushes[0] = static_cast<unsigned short>(brushVec.size());
|
|
||||||
brushNodeVec.emplace_back(brushNode);
|
|
||||||
|
|
||||||
cbrush_array_t brush;
|
|
||||||
memset(&brush, 0, sizeof(cbrush_array_t)); // if not sides or verts are given, the mins/maxs are used instead
|
|
||||||
brush.numverts = static_cast<unsigned int>(bspBrush.vertexCount);
|
|
||||||
brush.verts = &clipMap->info.brushVerts[bspBrush.vertexIndex];
|
|
||||||
brush.contents = bspBrush.contentFlags;
|
|
||||||
brush.mins = mins;
|
|
||||||
brush.maxs = maxs;
|
|
||||||
brush.axial_cflags[0][0] = bspBrush.contentFlags;
|
|
||||||
brush.axial_cflags[0][1] = bspBrush.contentFlags;
|
|
||||||
brush.axial_cflags[0][2] = bspBrush.contentFlags;
|
|
||||||
brush.axial_cflags[1][0] = bspBrush.contentFlags;
|
|
||||||
brush.axial_cflags[1][1] = bspBrush.contentFlags;
|
|
||||||
brush.axial_cflags[1][2] = bspBrush.contentFlags;
|
|
||||||
brush.axial_sflags[0][0] = bspBrush.surfaceFlags;
|
|
||||||
brush.axial_sflags[0][1] = bspBrush.surfaceFlags;
|
|
||||||
brush.axial_sflags[0][2] = bspBrush.surfaceFlags;
|
|
||||||
brush.axial_sflags[1][0] = bspBrush.surfaceFlags;
|
|
||||||
brush.axial_sflags[1][1] = bspBrush.surfaceFlags;
|
|
||||||
brush.axial_sflags[1][2] = bspBrush.surfaceFlags;
|
|
||||||
brushVec.emplace_back(brush);
|
|
||||||
|
|
||||||
if (bspModel.surfaceCount != 0)
|
|
||||||
BSPUtil::updateAABB(mins, maxs, clipModel->mins, clipModel->maxs);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
clipModel->mins = mins;
|
|
||||||
clipModel->maxs = maxs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
clipModel->leaf.brushContents = 0;
|
|
||||||
clipModel->leaf.mins.x = 0.0f;
|
|
||||||
clipModel->leaf.mins.y = 0.0f;
|
|
||||||
clipModel->leaf.mins.z = 0.0f;
|
|
||||||
clipModel->leaf.maxs.x = 0.0f;
|
|
||||||
clipModel->leaf.maxs.y = 0.0f;
|
|
||||||
clipModel->leaf.maxs.z = 0.0f;
|
|
||||||
clipModel->leaf.leafBrushNode = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bspModel.surfaceCount == 0 && !bspModel.hasBrush)
|
|
||||||
{
|
|
||||||
clipModel->mins = {0.0f, 0.0f, 0.0f};
|
|
||||||
clipModel->maxs = {0.0f, 0.0f, 0.0f};
|
|
||||||
clipModel->radius = 0.0f;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
clipModel->radius = BSPUtil::distBetweenPoints(clipModel->mins, clipModel->maxs) / 2;
|
|
||||||
clipModel->leaf.cluster = 0;
|
|
||||||
clipModel->info = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// out_mins and out_maxs are initialised in the function
|
// out_mins and out_maxs are initialised in the function
|
||||||
void calculatePartitionAABB(clipMap_t* clipMap, CollisionPartition* partition, vec3_t& out_mins, vec3_t& out_maxs)
|
void calculatePartitionAABB(clipMap_t* clipMap, CollisionPartition* partition, vec3_t& out_mins, vec3_t& out_maxs)
|
||||||
{
|
{
|
||||||
@@ -394,12 +232,6 @@ namespace BSP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct uniqueMatData
|
|
||||||
{
|
|
||||||
size_t materialIndex;
|
|
||||||
std::vector<int> partitionIndexes;
|
|
||||||
};
|
|
||||||
|
|
||||||
void ClipMapLinker::addAABBTreeFromPartitions(
|
void ClipMapLinker::addAABBTreeFromPartitions(
|
||||||
clipMap_t* clipMap, std::vector<int>& partitions, size_t* out_parentCount, size_t* out_parentStartIndex, int* out_treeContents)
|
clipMap_t* clipMap, std::vector<int>& partitions, size_t* out_parentCount, size_t* out_parentStartIndex, int* out_treeContents)
|
||||||
{
|
{
|
||||||
@@ -438,8 +270,8 @@ namespace BSP
|
|||||||
for (auto& matData : uniqueMaterials)
|
for (auto& matData : uniqueMaterials)
|
||||||
{
|
{
|
||||||
size_t objCount = matData.partitionIndexes.size();
|
size_t objCount = matData.partitionIndexes.size();
|
||||||
size_t result = objCount / BSPGameConstants::MAX_AABB_TREE_CHILDREN;
|
size_t result = objCount / MAX_AABB_TREE_CHILDREN;
|
||||||
size_t remainder = objCount % BSPGameConstants::MAX_AABB_TREE_CHILDREN;
|
size_t remainder = objCount % MAX_AABB_TREE_CHILDREN;
|
||||||
if (remainder > 0)
|
if (remainder > 0)
|
||||||
result++;
|
result++;
|
||||||
totalParentCount += result;
|
totalParentCount += result;
|
||||||
@@ -454,8 +286,8 @@ namespace BSP
|
|||||||
for (auto& matData : uniqueMaterials)
|
for (auto& matData : uniqueMaterials)
|
||||||
{
|
{
|
||||||
size_t matPartCount = matData.partitionIndexes.size();
|
size_t matPartCount = matData.partitionIndexes.size();
|
||||||
size_t parentCount = matPartCount / BSPGameConstants::MAX_AABB_TREE_CHILDREN;
|
size_t parentCount = matPartCount / MAX_AABB_TREE_CHILDREN;
|
||||||
size_t remainder = matPartCount % BSPGameConstants::MAX_AABB_TREE_CHILDREN;
|
size_t remainder = matPartCount % MAX_AABB_TREE_CHILDREN;
|
||||||
if (remainder > 0)
|
if (remainder > 0)
|
||||||
parentCount++;
|
parentCount++;
|
||||||
|
|
||||||
@@ -463,11 +295,11 @@ namespace BSP
|
|||||||
size_t addedObjectCount = 0;
|
size_t addedObjectCount = 0;
|
||||||
for (size_t parentIdx = 0; parentIdx < parentCount; parentIdx++)
|
for (size_t parentIdx = 0; parentIdx < parentCount; parentIdx++)
|
||||||
{
|
{
|
||||||
size_t currChildObjectCount = BSPGameConstants::MAX_AABB_TREE_CHILDREN;
|
size_t currChildObjectCount = MAX_AABB_TREE_CHILDREN;
|
||||||
if (unaddedObjectCount <= BSPGameConstants::MAX_AABB_TREE_CHILDREN)
|
if (unaddedObjectCount <= MAX_AABB_TREE_CHILDREN)
|
||||||
currChildObjectCount = unaddedObjectCount;
|
currChildObjectCount = unaddedObjectCount;
|
||||||
else
|
else
|
||||||
unaddedObjectCount -= BSPGameConstants::MAX_AABB_TREE_CHILDREN;
|
unaddedObjectCount -= MAX_AABB_TREE_CHILDREN;
|
||||||
|
|
||||||
vec3_t parentMins;
|
vec3_t parentMins;
|
||||||
vec3_t parentMaxs;
|
vec3_t parentMaxs;
|
||||||
@@ -680,10 +512,9 @@ namespace BSP
|
|||||||
{
|
{
|
||||||
// due to tris using uint16_t as the type for indexing the vert array,
|
// due to tris using uint16_t as the type for indexing the vert array,
|
||||||
// any vertex count over the uint16_t max means the vertices above the uint16_t max can't be indexed
|
// any vertex count over the uint16_t max means the vertices above the uint16_t max can't be indexed
|
||||||
if (static_cast<unsigned int>(bsp->colWorld.vertices.size()) > BSPGameConstants::MAX_COLLISION_VERTS)
|
if (static_cast<unsigned int>(bsp->colWorld.vertices.size()) > UINT16_MAX)
|
||||||
{
|
{
|
||||||
con::error(
|
con::error("ERROR: collision vertex count {} exceeds the maximum number: {}!\n", bsp->colWorld.vertices.size(), UINT16_MAX);
|
||||||
"ERROR: collision vertex count {} exceeds the maximum number: {}!\n", bsp->colWorld.vertices.size(), BSPGameConstants::MAX_COLLISION_VERTS);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,6 +657,175 @@ namespace BSP
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClipMapLinker::loadSubModelCollision(clipMap_t* clipMap, BSPData* bsp)
|
||||||
|
{
|
||||||
|
// Submodels are used for the world and map ent collision (triggers, bomb zones, etc)
|
||||||
|
clipMap->numSubModels = static_cast<unsigned int>(bsp->models.size() + 1);
|
||||||
|
clipMap->cmodels = m_memory.Alloc<cmodel_t>(clipMap->numSubModels);
|
||||||
|
|
||||||
|
// first model is always the world model
|
||||||
|
for (unsigned int vertIdx = 0; vertIdx < clipMap->vertCount; vertIdx++)
|
||||||
|
{
|
||||||
|
vec3_t vertex = clipMap->verts[vertIdx];
|
||||||
|
if (vertIdx == 0)
|
||||||
|
{
|
||||||
|
clipMap->cmodels[0].mins = vertex;
|
||||||
|
clipMap->cmodels[0].maxs = vertex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
BSPUtil::updateAABBWithPoint(vertex, clipMap->cmodels[0].mins, clipMap->cmodels[0].maxs);
|
||||||
|
}
|
||||||
|
clipMap->cmodels[0].radius = BSPUtil::distBetweenPoints(clipMap->cmodels[0].mins, clipMap->cmodels[0].maxs) / 2;
|
||||||
|
// The world sub model has no data apart from the bounds
|
||||||
|
clipMap->cmodels[0].leaf.firstCollAabbIndex = 0;
|
||||||
|
clipMap->cmodels[0].leaf.collAabbCount = 0;
|
||||||
|
clipMap->cmodels[0].leaf.brushContents = 0;
|
||||||
|
clipMap->cmodels[0].leaf.terrainContents = 0;
|
||||||
|
clipMap->cmodels[0].leaf.mins.x = 0.0f;
|
||||||
|
clipMap->cmodels[0].leaf.mins.y = 0.0f;
|
||||||
|
clipMap->cmodels[0].leaf.mins.z = 0.0f;
|
||||||
|
clipMap->cmodels[0].leaf.maxs.x = 0.0f;
|
||||||
|
clipMap->cmodels[0].leaf.maxs.y = 0.0f;
|
||||||
|
clipMap->cmodels[0].leaf.maxs.z = 0.0f;
|
||||||
|
clipMap->cmodels[0].leaf.leafBrushNode = 0;
|
||||||
|
clipMap->cmodels[0].leaf.cluster = 0;
|
||||||
|
clipMap->cmodels[0].info = nullptr;
|
||||||
|
|
||||||
|
for (size_t modelIdx = 0; modelIdx < bsp->models.size(); modelIdx++)
|
||||||
|
{
|
||||||
|
auto clipModel = &clipMap->cmodels[modelIdx + 1];
|
||||||
|
auto& bspModel = bsp->models.at(modelIdx);
|
||||||
|
|
||||||
|
if (bspModel.isGfxModel)
|
||||||
|
{
|
||||||
|
memset(clipModel, 0, sizeof(cmodel_t));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bspModel.surfaceCount != 0)
|
||||||
|
{
|
||||||
|
std::vector<int> partitionIndexes;
|
||||||
|
for (size_t surfIdx = 0; surfIdx < bspModel.surfaceCount; surfIdx++)
|
||||||
|
{
|
||||||
|
ColSurface& surf = collisionSurfaceVec.at(bsp->colWorld.staticSurfaces.size() + bspModel.surfaceIndex + surfIdx);
|
||||||
|
for (size_t partitionIdx = 0; partitionIdx < surf.partitionCount; partitionIdx++)
|
||||||
|
{
|
||||||
|
size_t clipMapPartitionIndex = surf.partitionStartIndex + partitionIdx;
|
||||||
|
partitionIndexes.emplace_back(clipMapPartitionIndex);
|
||||||
|
vec3_t mins;
|
||||||
|
vec3_t maxs;
|
||||||
|
calculatePartitionAABB(clipMap, &clipMap->partitions[clipMapPartitionIndex], mins, maxs);
|
||||||
|
if (surfIdx == 0 && partitionIdx == 0)
|
||||||
|
{
|
||||||
|
clipModel->mins = mins;
|
||||||
|
clipModel->maxs = maxs;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
BSPUtil::updateAABB(mins, maxs, clipModel->mins, clipModel->maxs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int terrainContents = 0;
|
||||||
|
size_t firstCollAabbIndex = 0;
|
||||||
|
size_t collAabbCount = 0;
|
||||||
|
addAABBTreeFromPartitions(clipMap, partitionIndexes, &collAabbCount, &firstCollAabbIndex, &terrainContents);
|
||||||
|
clipModel->leaf.terrainContents = terrainContents;
|
||||||
|
clipModel->leaf.firstCollAabbIndex = static_cast<uint16_t>(firstCollAabbIndex);
|
||||||
|
clipModel->leaf.collAabbCount = static_cast<uint16_t>(collAabbCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clipModel->leaf.terrainContents = 0;
|
||||||
|
clipModel->leaf.firstCollAabbIndex = 0;
|
||||||
|
clipModel->leaf.collAabbCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bspModel.hasBrush)
|
||||||
|
{
|
||||||
|
|
||||||
|
BSPBoxBrush& bspBrush = bsp->colWorld.scriptBoxBrushes.at(bspModel.brushIndex);
|
||||||
|
vec3_t mins;
|
||||||
|
vec3_t maxs;
|
||||||
|
for (size_t vertIdx = 0; vertIdx < bspBrush.vertexCount; vertIdx++)
|
||||||
|
{
|
||||||
|
vec3_t* vertex = &clipMap->info.brushVerts[bspBrush.vertexIndex + vertIdx];
|
||||||
|
if (vertIdx == 0)
|
||||||
|
{
|
||||||
|
mins = *vertex;
|
||||||
|
maxs = *vertex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
BSPUtil::updateAABBWithPoint(*vertex, mins, maxs);
|
||||||
|
}
|
||||||
|
|
||||||
|
clipModel->leaf.mins = mins;
|
||||||
|
clipModel->leaf.maxs = maxs;
|
||||||
|
clipModel->leaf.brushContents = bspBrush.contentFlags;
|
||||||
|
clipModel->leaf.leafBrushNode = static_cast<int>(brushNodeVec.size());
|
||||||
|
assert(clipModel->leaf.leafBrushNode != 0);
|
||||||
|
|
||||||
|
cLeafBrushNode_s brushNode;
|
||||||
|
brushNode.axis = 0;
|
||||||
|
brushNode.contents = bspBrush.contentFlags;
|
||||||
|
brushNode.leafBrushCount = 1;
|
||||||
|
brushNode.data.leaf.brushes = m_memory.Alloc<LeafBrush>(1);
|
||||||
|
brushNode.data.leaf.brushes[0] = static_cast<unsigned short>(brushVec.size());
|
||||||
|
brushNodeVec.emplace_back(brushNode);
|
||||||
|
|
||||||
|
cbrush_array_t brush;
|
||||||
|
memset(&brush, 0, sizeof(cbrush_array_t)); // if not sides or verts are given, the mins/maxs are used instead
|
||||||
|
brush.numverts = static_cast<unsigned int>(bspBrush.vertexCount);
|
||||||
|
brush.verts = &clipMap->info.brushVerts[bspBrush.vertexIndex];
|
||||||
|
brush.contents = bspBrush.contentFlags;
|
||||||
|
brush.mins = mins;
|
||||||
|
brush.maxs = maxs;
|
||||||
|
brush.axial_cflags[0][0] = bspBrush.contentFlags;
|
||||||
|
brush.axial_cflags[0][1] = bspBrush.contentFlags;
|
||||||
|
brush.axial_cflags[0][2] = bspBrush.contentFlags;
|
||||||
|
brush.axial_cflags[1][0] = bspBrush.contentFlags;
|
||||||
|
brush.axial_cflags[1][1] = bspBrush.contentFlags;
|
||||||
|
brush.axial_cflags[1][2] = bspBrush.contentFlags;
|
||||||
|
brush.axial_sflags[0][0] = bspBrush.surfaceFlags;
|
||||||
|
brush.axial_sflags[0][1] = bspBrush.surfaceFlags;
|
||||||
|
brush.axial_sflags[0][2] = bspBrush.surfaceFlags;
|
||||||
|
brush.axial_sflags[1][0] = bspBrush.surfaceFlags;
|
||||||
|
brush.axial_sflags[1][1] = bspBrush.surfaceFlags;
|
||||||
|
brush.axial_sflags[1][2] = bspBrush.surfaceFlags;
|
||||||
|
brushVec.emplace_back(brush);
|
||||||
|
|
||||||
|
if (bspModel.surfaceCount != 0)
|
||||||
|
BSPUtil::updateAABB(mins, maxs, clipModel->mins, clipModel->maxs);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clipModel->mins = mins;
|
||||||
|
clipModel->maxs = maxs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
clipModel->leaf.brushContents = 0;
|
||||||
|
clipModel->leaf.mins.x = 0.0f;
|
||||||
|
clipModel->leaf.mins.y = 0.0f;
|
||||||
|
clipModel->leaf.mins.z = 0.0f;
|
||||||
|
clipModel->leaf.maxs.x = 0.0f;
|
||||||
|
clipModel->leaf.maxs.y = 0.0f;
|
||||||
|
clipModel->leaf.maxs.z = 0.0f;
|
||||||
|
clipModel->leaf.leafBrushNode = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bspModel.surfaceCount == 0 && !bspModel.hasBrush)
|
||||||
|
{
|
||||||
|
clipModel->mins = {0.0f, 0.0f, 0.0f};
|
||||||
|
clipModel->maxs = {0.0f, 0.0f, 0.0f};
|
||||||
|
clipModel->radius = 0.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
clipModel->radius = BSPUtil::distBetweenPoints(clipModel->mins, clipModel->maxs) / 2;
|
||||||
|
clipModel->leaf.cluster = 0;
|
||||||
|
clipModel->info = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool ClipMapLinker::loadMaterials(clipMap_t* clipMap, BSPData* bsp)
|
bool ClipMapLinker::loadMaterials(clipMap_t* clipMap, BSPData* bsp)
|
||||||
{
|
{
|
||||||
// Clipmap materials define the properties of a material (bullet penetration, no collision, water, etc)
|
// Clipmap materials define the properties of a material (bullet penetration, no collision, water, etc)
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
|
|
||||||
#include <numbers>
|
#include <numbers>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr const char* DEFAULT_LIGHTDEF_NAME = "white_light";
|
||||||
|
constexpr short LIGHT_CULLDIST = 10000;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
namespace BSP
|
namespace BSP
|
||||||
{
|
{
|
||||||
ComWorldLinker::ComWorldLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context)
|
ComWorldLinker::ComWorldLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context)
|
||||||
@@ -16,10 +22,10 @@ namespace BSP
|
|||||||
bool ComWorldLinker::createLightDefs()
|
bool ComWorldLinker::createLightDefs()
|
||||||
{
|
{
|
||||||
T6::GfxLightDef* lightDef2d = m_memory.Alloc<T6::GfxLightDef>();
|
T6::GfxLightDef* lightDef2d = m_memory.Alloc<T6::GfxLightDef>();
|
||||||
lightDef2d->name = m_memory.Dup("white_light");
|
lightDef2d->name = m_memory.Dup(DEFAULT_LIGHTDEF_NAME);
|
||||||
lightDef2d->lmapLookupStart = 0; // always 0
|
lightDef2d->lmapLookupStart = 0; // always 0
|
||||||
lightDef2d->attenuation.samplerState = 115; // always 115
|
lightDef2d->attenuation.samplerState = 115; // always 115
|
||||||
auto image2dAsset = m_context.LoadDependency<T6::AssetImage>("whitesquare");
|
auto image2dAsset = m_context.LoadDependency<T6::AssetImage>(",$white");
|
||||||
if (image2dAsset == nullptr)
|
if (image2dAsset == nullptr)
|
||||||
return false;
|
return false;
|
||||||
lightDef2d->attenuation.image = image2dAsset->Asset();
|
lightDef2d->attenuation.image = image2dAsset->Asset();
|
||||||
@@ -36,7 +42,7 @@ namespace BSP
|
|||||||
comWorld->isInUse = 1;
|
comWorld->isInUse = 1;
|
||||||
|
|
||||||
// first two lights are the empty light and the sun light.
|
// first two lights are the empty light and the sun light.
|
||||||
size_t totalLightCount = bsp->lights.size() + BSPGameConstants::BSP_DEFAULT_LIGHT_COUNT;
|
size_t totalLightCount = bsp->lights.size() + BSP_DEFAULT_LIGHT_COUNT;
|
||||||
comWorld->primaryLightCount = static_cast<unsigned int>(totalLightCount);
|
comWorld->primaryLightCount = static_cast<unsigned int>(totalLightCount);
|
||||||
comWorld->primaryLights = m_memory.Alloc<ComPrimaryLight>(totalLightCount);
|
comWorld->primaryLights = m_memory.Alloc<ComPrimaryLight>(totalLightCount);
|
||||||
|
|
||||||
@@ -49,9 +55,9 @@ namespace BSP
|
|||||||
for (size_t lightIdx = 0; lightIdx < totalLightCount; lightIdx++)
|
for (size_t lightIdx = 0; lightIdx < totalLightCount; lightIdx++)
|
||||||
{
|
{
|
||||||
ComPrimaryLight* light = &comWorld->primaryLights[lightIdx];
|
ComPrimaryLight* light = &comWorld->primaryLights[lightIdx];
|
||||||
if (lightIdx == BSPGameConstants::EMPTY_LIGHT_INDEX)
|
if (lightIdx == EMPTY_LIGHT_INDEX)
|
||||||
continue; // first (empty) light has no data
|
continue; // first (empty) light has no data
|
||||||
else if (lightIdx == BSPGameConstants::SUN_LIGHT_INDEX)
|
else if (lightIdx == SUN_LIGHT_INDEX)
|
||||||
{
|
{
|
||||||
BSPLight* bspLight = &bsp->sunlight;
|
BSPLight* bspLight = &bsp->sunlight;
|
||||||
|
|
||||||
@@ -68,7 +74,7 @@ namespace BSP
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
BSPLight* bspLight = &bsp->lights.at(lightIdx - BSPGameConstants::BSP_DEFAULT_LIGHT_COUNT);
|
BSPLight* bspLight = &bsp->lights.at(lightIdx - BSP_DEFAULT_LIGHT_COUNT);
|
||||||
|
|
||||||
light->type = GFX_LIGHT_TYPE_SPOT;
|
light->type = GFX_LIGHT_TYPE_SPOT;
|
||||||
|
|
||||||
@@ -103,8 +109,8 @@ namespace BSP
|
|||||||
light->aAbB.z = 0.75f;
|
light->aAbB.z = 0.75f;
|
||||||
light->aAbB.w = 1.0f;
|
light->aAbB.w = 1.0f;
|
||||||
|
|
||||||
light->cullDist = 10000;
|
light->cullDist = LIGHT_CULLDIST;
|
||||||
light->defName = "white_light";
|
light->defName = DEFAULT_LIGHTDEF_NAME;
|
||||||
light->rotationLimit = 1.0f; // 1.0f - doesn't rotate, -1.0f - unclamped rotation
|
light->rotationLimit = 1.0f; // 1.0f - doesn't rotate, -1.0f - unclamped rotation
|
||||||
light->translationLimit = 0.0f; // 0.0f - doesn't translate, above 0.0f - distance per game update translated
|
light->translationLimit = 0.0f; // 0.0f - doesn't translate, above 0.0f - distance per game update translated
|
||||||
light->roundness = 1.0f; // 0.0f - light is a square. 1.0f - light is a circle
|
light->roundness = 1.0f; // 0.0f - light is a square. 1.0f - light is a circle
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ namespace
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr const char* DEFAULT_IMAGE_NAME = ",mc/lambert1";
|
||||||
|
constexpr char EMPTY_LIGHTMAP_INDEX = 0;
|
||||||
|
constexpr char EMPTY_RPROBE_INDEX = 0;
|
||||||
|
constexpr float XMODEL_CULL_DIST = 10000.0f;
|
||||||
|
constexpr char DEFAULT_LIGHTGRID_COLOUR = 32;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace BSP
|
namespace BSP
|
||||||
@@ -99,12 +105,12 @@ namespace BSP
|
|||||||
if (bspMaterial.materialType == MATERIAL_TYPE_TEXTURE)
|
if (bspMaterial.materialType == MATERIAL_TYPE_TEXTURE)
|
||||||
materialName = bspMaterial.materialName;
|
materialName = bspMaterial.materialName;
|
||||||
else // MATERIAL_TYPE_COLOUR
|
else // MATERIAL_TYPE_COLOUR
|
||||||
materialName = BSPLinkingConstants::COLOR_ONLY_IMAGE_NAME;
|
materialName = DEFAULT_IMAGE_NAME;
|
||||||
|
|
||||||
auto surfMaterialAsset = m_context.LoadDependency<AssetMaterial>(materialName);
|
auto surfMaterialAsset = m_context.LoadDependency<AssetMaterial>(materialName);
|
||||||
if (surfMaterialAsset == nullptr)
|
if (surfMaterialAsset == nullptr)
|
||||||
{
|
{
|
||||||
surfMaterialAsset = m_context.LoadDependency<AssetMaterial>(BSPLinkingConstants::MISSING_IMAGE_NAME);
|
surfMaterialAsset = m_context.LoadDependency<AssetMaterial>(DEFAULT_IMAGE_NAME);
|
||||||
assert(surfMaterialAsset != nullptr);
|
assert(surfMaterialAsset != nullptr);
|
||||||
}
|
}
|
||||||
gfxSurface->material = surfMaterialAsset->Asset();
|
gfxSurface->material = surfMaterialAsset->Asset();
|
||||||
@@ -138,9 +144,9 @@ namespace BSP
|
|||||||
if ((bspMaterial.surfaceFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_NOCASTSHADOW].surfaceFlags) == 0)
|
if ((bspMaterial.surfaceFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_NOCASTSHADOW].surfaceFlags) == 0)
|
||||||
gfxSurface->flags |= GFX_SURFACE_CASTS_SHADOW;
|
gfxSurface->flags |= GFX_SURFACE_CASTS_SHADOW;
|
||||||
|
|
||||||
gfxSurface->primaryLightIndex = BSPEditableConstants::DEFAULT_SURFACE_LIGHT;
|
gfxSurface->primaryLightIndex = SUN_LIGHT_INDEX;
|
||||||
gfxSurface->lightmapIndex = BSPEditableConstants::DEFAULT_SURFACE_LIGHTMAP;
|
gfxSurface->lightmapIndex = EMPTY_LIGHTMAP_INDEX;
|
||||||
gfxSurface->reflectionProbeIndex = BSPEditableConstants::DEFAULT_SURFACE_REFLECTION_PROBE;
|
gfxSurface->reflectionProbeIndex = EMPTY_RPROBE_INDEX;
|
||||||
|
|
||||||
// unknown value
|
// unknown value
|
||||||
gfxSurface->tris.himipRadiusInvSq = 0.0f;
|
gfxSurface->tris.himipRadiusInvSq = 0.0f;
|
||||||
@@ -210,9 +216,9 @@ namespace BSP
|
|||||||
if (!bspModel.doesCastShadow)
|
if (!bspModel.doesCastShadow)
|
||||||
currModel->flags |= STATIC_MODEL_FLAG_NO_SHADOW;
|
currModel->flags |= STATIC_MODEL_FLAG_NO_SHADOW;
|
||||||
|
|
||||||
currModel->cullDist = 10000.0f;
|
currModel->cullDist = XMODEL_CULL_DIST;
|
||||||
currModel->primaryLightIndex = 1;
|
currModel->primaryLightIndex = SUN_LIGHT_INDEX;
|
||||||
currModel->reflectionProbeIndex = 0;
|
currModel->reflectionProbeIndex = EMPTY_LIGHTMAP_INDEX;
|
||||||
currModel->smid = modelIdx;
|
currModel->smid = modelIdx;
|
||||||
|
|
||||||
// unknown use / unused
|
// unknown use / unused
|
||||||
@@ -392,8 +398,8 @@ namespace BSP
|
|||||||
void GfxWorldLinker::loadGfxLights(BSPData* bsp, GfxWorld* gfxWorld)
|
void GfxWorldLinker::loadGfxLights(BSPData* bsp, GfxWorld* gfxWorld)
|
||||||
{
|
{
|
||||||
// there must be 2 or more lights, first is the static light and second is the sun light
|
// there must be 2 or more lights, first is the static light and second is the sun light
|
||||||
gfxWorld->primaryLightCount = BSPGameConstants::BSP_DEFAULT_LIGHT_COUNT + static_cast<unsigned int>(bsp->lights.size());
|
gfxWorld->primaryLightCount = BSP_DEFAULT_LIGHT_COUNT + static_cast<unsigned int>(bsp->lights.size());
|
||||||
gfxWorld->sunPrimaryLightIndex = BSPGameConstants::SUN_LIGHT_INDEX;
|
gfxWorld->sunPrimaryLightIndex = SUN_LIGHT_INDEX;
|
||||||
|
|
||||||
gfxWorld->shadowGeom = m_memory.Alloc<GfxShadowGeometry>(gfxWorld->primaryLightCount);
|
gfxWorld->shadowGeom = m_memory.Alloc<GfxShadowGeometry>(gfxWorld->primaryLightCount);
|
||||||
for (unsigned int lightIdx = 0; lightIdx < gfxWorld->primaryLightCount; lightIdx++)
|
for (unsigned int lightIdx = 0; lightIdx < gfxWorld->primaryLightCount; lightIdx++)
|
||||||
@@ -446,7 +452,7 @@ namespace BSP
|
|||||||
|
|
||||||
gfxWorld->lightGrid.rowAxis = 0; // default value
|
gfxWorld->lightGrid.rowAxis = 0; // default value
|
||||||
gfxWorld->lightGrid.colAxis = 1; // default value
|
gfxWorld->lightGrid.colAxis = 1; // default value
|
||||||
gfxWorld->lightGrid.sunPrimaryLightIndex = BSPGameConstants::SUN_LIGHT_INDEX;
|
gfxWorld->lightGrid.sunPrimaryLightIndex = SUN_LIGHT_INDEX;
|
||||||
gfxWorld->lightGrid.offset = 0.0f; // default value
|
gfxWorld->lightGrid.offset = 0.0f; // default value
|
||||||
|
|
||||||
// setting all rowDataStart indexes to 0 will always index the first row in rawRowData
|
// setting all rowDataStart indexes to 0 will always index the first row in rawRowData
|
||||||
@@ -471,12 +477,12 @@ namespace BSP
|
|||||||
for (unsigned int i = 0; i < gfxWorld->lightGrid.entryCount; i++)
|
for (unsigned int i = 0; i < gfxWorld->lightGrid.entryCount; i++)
|
||||||
{
|
{
|
||||||
entryArray[i].colorsIndex = 0; // always index first colour
|
entryArray[i].colorsIndex = 0; // always index first colour
|
||||||
entryArray[i].primaryLightIndex = BSPGameConstants::SUN_LIGHT_INDEX;
|
entryArray[i].primaryLightIndex = SUN_LIGHT_INDEX;
|
||||||
entryArray[i].visibility = 0;
|
entryArray[i].visibility = 0;
|
||||||
}
|
}
|
||||||
gfxWorld->lightGrid.entries = entryArray;
|
gfxWorld->lightGrid.entries = entryArray;
|
||||||
|
|
||||||
char color = 32;
|
char color = DEFAULT_LIGHTGRID_COLOUR;
|
||||||
float sunIntensity = bsp->sunlight.intensity;
|
float sunIntensity = bsp->sunlight.intensity;
|
||||||
if (sunIntensity < 0.0f || sunIntensity > 20000.0f)
|
if (sunIntensity < 0.0f || sunIntensity > 20000.0f)
|
||||||
con::warn("Sun intensity ({}) needs to be between 0 and 20000", sunIntensity);
|
con::warn("Sun intensity ({}) needs to be between 0 and 20000", sunIntensity);
|
||||||
@@ -534,7 +540,7 @@ namespace BSP
|
|||||||
// there is only 1 reflection probe
|
// there is only 1 reflection probe
|
||||||
gfxWorld->cells[0].reflectionProbeCount = 1;
|
gfxWorld->cells[0].reflectionProbeCount = 1;
|
||||||
gfxWorld->cells[0].reflectionProbes = m_memory.Alloc<char>(gfxWorld->cells[0].reflectionProbeCount);
|
gfxWorld->cells[0].reflectionProbes = m_memory.Alloc<char>(gfxWorld->cells[0].reflectionProbeCount);
|
||||||
gfxWorld->cells[0].reflectionProbes[0] = BSPEditableConstants::DEFAULT_SURFACE_REFLECTION_PROBE;
|
gfxWorld->cells[0].reflectionProbes[0] = EMPTY_RPROBE_INDEX;
|
||||||
|
|
||||||
// AABB trees are used to detect what should be rendered and what shouldn't
|
// AABB trees are used to detect what should be rendered and what shouldn't
|
||||||
// Just use the first AABB node to hold all models, no optimisation but all models/surfaces wil lbe drawn
|
// Just use the first AABB node to hold all models, no optimisation but all models/surfaces wil lbe drawn
|
||||||
|
|||||||
Reference in New Issue
Block a user