mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2025-12-10 17:42:29 +01:00
Introduce TileElementsView (#13975)
* Simplify TileElement type conversation * Introduce TileElementsView * Move TileElementsView code into TileElementsView.h * Cleanup code and move into OpenRCT2 namespace * Use reference instead of pointer * Fix include * Make GCC happy * Move the cast functions into base * Use the cast function instead of reinterpret_cast * Add TileElementsView tests * Fix iterating on TileElementBase, return pointer not reference
This commit is contained in:
@@ -453,6 +453,7 @@
|
||||
<ClInclude Include="world\SpriteBase.h" />
|
||||
<ClInclude Include="world\Surface.h" />
|
||||
<ClInclude Include="world\TileElement.h" />
|
||||
<ClInclude Include="world\TileElementsView.h" />
|
||||
<ClInclude Include="world\TileInspector.h" />
|
||||
<ClInclude Include="world\Wall.h" />
|
||||
<ClInclude Include="world\Water.h" />
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#include "Scenery.h"
|
||||
#include "SmallScenery.h"
|
||||
#include "Surface.h"
|
||||
#include "TileElementsView.h"
|
||||
#include "TileInspector.h"
|
||||
#include "Wall.h"
|
||||
|
||||
@@ -215,67 +216,35 @@ void map_set_tile_element(const TileCoordsXY& tilePos, TileElement* elements)
|
||||
|
||||
SurfaceElement* map_get_surface_element_at(const CoordsXY& coords)
|
||||
{
|
||||
TileElement* tileElement = map_get_first_element_at(coords);
|
||||
auto view = TileElementsView<SurfaceElement>(coords);
|
||||
|
||||
if (tileElement == nullptr)
|
||||
return nullptr;
|
||||
|
||||
// Find the first surface element
|
||||
while (tileElement->GetType() != TILE_ELEMENT_TYPE_SURFACE)
|
||||
{
|
||||
if (tileElement->IsLastForTile())
|
||||
return nullptr;
|
||||
|
||||
tileElement++;
|
||||
}
|
||||
|
||||
return tileElement->AsSurface();
|
||||
return *view.begin();
|
||||
}
|
||||
|
||||
PathElement* map_get_path_element_at(const TileCoordsXYZ& loc)
|
||||
{
|
||||
TileElement* tileElement = map_get_first_element_at(loc.ToCoordsXY());
|
||||
|
||||
if (tileElement == nullptr)
|
||||
return nullptr;
|
||||
|
||||
// Find the path element at known z
|
||||
do
|
||||
for (auto* element : TileElementsView<PathElement>(loc.ToCoordsXY()))
|
||||
{
|
||||
if (tileElement->IsGhost())
|
||||
if (element->IsGhost())
|
||||
continue;
|
||||
if (tileElement->GetType() != TILE_ELEMENT_TYPE_PATH)
|
||||
if (element->base_height != loc.z)
|
||||
continue;
|
||||
if (tileElement->base_height != loc.z)
|
||||
continue;
|
||||
|
||||
return tileElement->AsPath();
|
||||
} while (!(tileElement++)->IsLastForTile());
|
||||
|
||||
return element;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BannerElement* map_get_banner_element_at(const CoordsXYZ& bannerPos, uint8_t position)
|
||||
{
|
||||
auto bannerTilePos = TileCoordsXYZ{ bannerPos };
|
||||
TileElement* tileElement = map_get_first_element_at(bannerPos);
|
||||
|
||||
if (tileElement == nullptr)
|
||||
return nullptr;
|
||||
|
||||
// Find the banner element at known z and position
|
||||
do
|
||||
const auto bannerTilePos = TileCoordsXYZ{ bannerPos };
|
||||
for (auto* element : TileElementsView<BannerElement>(bannerPos))
|
||||
{
|
||||
if (tileElement->GetType() != TILE_ELEMENT_TYPE_BANNER)
|
||||
if (element->base_height != bannerTilePos.z)
|
||||
continue;
|
||||
if (tileElement->base_height != bannerTilePos.z)
|
||||
if (element->GetPosition() != position)
|
||||
continue;
|
||||
if (tileElement->AsBanner()->GetPosition() != position)
|
||||
continue;
|
||||
|
||||
return tileElement->AsBanner();
|
||||
} while (!(tileElement++)->IsLastForTile());
|
||||
|
||||
return element;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,80 @@ struct TileElementBase
|
||||
|
||||
uint8_t GetOwner() const;
|
||||
void SetOwner(uint8_t newOwner);
|
||||
|
||||
template<typename TType> const TType* as() const
|
||||
{
|
||||
return static_cast<TileElementType>(GetType()) == TType::ElementType ? reinterpret_cast<const TType*>(this) : nullptr;
|
||||
}
|
||||
template<typename TType> TType* as()
|
||||
{
|
||||
return static_cast<TileElementType>(GetType()) == TType::ElementType ? reinterpret_cast<TType*>(this) : nullptr;
|
||||
}
|
||||
|
||||
const SurfaceElement* AsSurface() const
|
||||
{
|
||||
return as<SurfaceElement>();
|
||||
}
|
||||
SurfaceElement* AsSurface()
|
||||
{
|
||||
return as<SurfaceElement>();
|
||||
}
|
||||
const PathElement* AsPath() const
|
||||
{
|
||||
return as<PathElement>();
|
||||
}
|
||||
PathElement* AsPath()
|
||||
{
|
||||
return as<PathElement>();
|
||||
}
|
||||
const TrackElement* AsTrack() const
|
||||
{
|
||||
return as<TrackElement>();
|
||||
}
|
||||
TrackElement* AsTrack()
|
||||
{
|
||||
return as<TrackElement>();
|
||||
}
|
||||
const SmallSceneryElement* AsSmallScenery() const
|
||||
{
|
||||
return as<SmallSceneryElement>();
|
||||
}
|
||||
SmallSceneryElement* AsSmallScenery()
|
||||
{
|
||||
return as<SmallSceneryElement>();
|
||||
}
|
||||
const LargeSceneryElement* AsLargeScenery() const
|
||||
{
|
||||
return as<LargeSceneryElement>();
|
||||
}
|
||||
LargeSceneryElement* AsLargeScenery()
|
||||
{
|
||||
return as<LargeSceneryElement>();
|
||||
}
|
||||
const WallElement* AsWall() const
|
||||
{
|
||||
return as<WallElement>();
|
||||
}
|
||||
WallElement* AsWall()
|
||||
{
|
||||
return as<WallElement>();
|
||||
}
|
||||
const EntranceElement* AsEntrance() const
|
||||
{
|
||||
return as<EntranceElement>();
|
||||
}
|
||||
EntranceElement* AsEntrance()
|
||||
{
|
||||
return as<EntranceElement>();
|
||||
}
|
||||
const BannerElement* AsBanner() const
|
||||
{
|
||||
return as<BannerElement>();
|
||||
}
|
||||
BannerElement* AsBanner()
|
||||
{
|
||||
return as<BannerElement>();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -110,81 +184,6 @@ struct TileElement : public TileElementBase
|
||||
uint8_t pad_05[3];
|
||||
uint8_t pad_08[8];
|
||||
|
||||
template<typename TType, TileElementType TClass> const TType* as() const
|
||||
{
|
||||
return static_cast<TileElementType>(GetType()) == TClass ? reinterpret_cast<const TType*>(this) : nullptr;
|
||||
}
|
||||
template<typename TType, TileElementType TClass> TType* as()
|
||||
{
|
||||
return static_cast<TileElementType>(GetType()) == TClass ? reinterpret_cast<TType*>(this) : nullptr;
|
||||
}
|
||||
|
||||
public:
|
||||
const SurfaceElement* AsSurface() const
|
||||
{
|
||||
return as<SurfaceElement, TileElementType::Surface>();
|
||||
}
|
||||
SurfaceElement* AsSurface()
|
||||
{
|
||||
return as<SurfaceElement, TileElementType::Surface>();
|
||||
}
|
||||
const PathElement* AsPath() const
|
||||
{
|
||||
return as<PathElement, TileElementType::Path>();
|
||||
}
|
||||
PathElement* AsPath()
|
||||
{
|
||||
return as<PathElement, TileElementType::Path>();
|
||||
}
|
||||
const TrackElement* AsTrack() const
|
||||
{
|
||||
return as<TrackElement, TileElementType::Track>();
|
||||
}
|
||||
TrackElement* AsTrack()
|
||||
{
|
||||
return as<TrackElement, TileElementType::Track>();
|
||||
}
|
||||
const SmallSceneryElement* AsSmallScenery() const
|
||||
{
|
||||
return as<SmallSceneryElement, TileElementType::SmallScenery>();
|
||||
}
|
||||
SmallSceneryElement* AsSmallScenery()
|
||||
{
|
||||
return as<SmallSceneryElement, TileElementType::SmallScenery>();
|
||||
}
|
||||
const LargeSceneryElement* AsLargeScenery() const
|
||||
{
|
||||
return as<LargeSceneryElement, TileElementType::LargeScenery>();
|
||||
}
|
||||
LargeSceneryElement* AsLargeScenery()
|
||||
{
|
||||
return as<LargeSceneryElement, TileElementType::LargeScenery>();
|
||||
}
|
||||
const WallElement* AsWall() const
|
||||
{
|
||||
return as<WallElement, TileElementType::Wall>();
|
||||
}
|
||||
WallElement* AsWall()
|
||||
{
|
||||
return as<WallElement, TileElementType::Wall>();
|
||||
}
|
||||
const EntranceElement* AsEntrance() const
|
||||
{
|
||||
return as<EntranceElement, TileElementType::Entrance>();
|
||||
}
|
||||
EntranceElement* AsEntrance()
|
||||
{
|
||||
return as<EntranceElement, TileElementType::Entrance>();
|
||||
}
|
||||
const BannerElement* AsBanner() const
|
||||
{
|
||||
return as<BannerElement, TileElementType::Banner>();
|
||||
}
|
||||
BannerElement* AsBanner()
|
||||
{
|
||||
return as<BannerElement, TileElementType::Banner>();
|
||||
}
|
||||
|
||||
void ClearAs(uint8_t newType);
|
||||
|
||||
ride_id_t GetRideIndex() const;
|
||||
@@ -197,6 +196,8 @@ assert_struct_size(TileElement, 16);
|
||||
|
||||
struct SurfaceElement : TileElementBase
|
||||
{
|
||||
static constexpr TileElementType ElementType = TileElementType::Surface;
|
||||
|
||||
private:
|
||||
uint8_t Slope;
|
||||
uint8_t WaterHeight;
|
||||
@@ -242,6 +243,8 @@ assert_struct_size(SurfaceElement, 16);
|
||||
|
||||
struct PathElement : TileElementBase
|
||||
{
|
||||
static constexpr TileElementType ElementType = TileElementType::Path;
|
||||
|
||||
private:
|
||||
PathSurfaceIndex SurfaceIndex; // 5
|
||||
#pragma clang diagnostic push
|
||||
@@ -329,6 +332,8 @@ assert_struct_size(PathElement, 16);
|
||||
|
||||
struct TrackElement : TileElementBase
|
||||
{
|
||||
static constexpr TileElementType ElementType = TileElementType::Track;
|
||||
|
||||
private:
|
||||
track_type_t TrackType;
|
||||
union
|
||||
@@ -428,6 +433,8 @@ assert_struct_size(TrackElement, 16);
|
||||
|
||||
struct SmallSceneryElement : TileElementBase
|
||||
{
|
||||
static constexpr TileElementType ElementType = TileElementType::SmallScenery;
|
||||
|
||||
private:
|
||||
ObjectEntryIndex entryIndex; // 5
|
||||
uint8_t age; // 7
|
||||
@@ -459,6 +466,8 @@ assert_struct_size(SmallSceneryElement, 16);
|
||||
|
||||
struct LargeSceneryElement : TileElementBase
|
||||
{
|
||||
static constexpr TileElementType ElementType = TileElementType::LargeScenery;
|
||||
|
||||
private:
|
||||
ObjectEntryIndex EntryIndex;
|
||||
::BannerIndex BannerIndex;
|
||||
@@ -494,6 +503,8 @@ assert_struct_size(LargeSceneryElement, 16);
|
||||
|
||||
struct WallElement : TileElementBase
|
||||
{
|
||||
static constexpr TileElementType ElementType = TileElementType::Wall;
|
||||
|
||||
private:
|
||||
ObjectEntryIndex entryIndex; // 05
|
||||
colour_t colour_1; // 07
|
||||
@@ -537,6 +548,8 @@ assert_struct_size(WallElement, 16);
|
||||
|
||||
struct EntranceElement : TileElementBase
|
||||
{
|
||||
static constexpr TileElementType ElementType = TileElementType::Entrance;
|
||||
|
||||
private:
|
||||
uint8_t entranceType; // 5
|
||||
uint8_t SequenceIndex; // 6. Only uses the lower nibble.
|
||||
@@ -568,6 +581,8 @@ assert_struct_size(EntranceElement, 16);
|
||||
|
||||
struct BannerElement : TileElementBase
|
||||
{
|
||||
static constexpr TileElementType ElementType = TileElementType::Banner;
|
||||
|
||||
private:
|
||||
BannerIndex index; // 5
|
||||
uint8_t position; // 7
|
||||
@@ -594,6 +609,8 @@ assert_struct_size(BannerElement, 16);
|
||||
|
||||
struct CorruptElement : TileElementBase
|
||||
{
|
||||
static constexpr TileElementType ElementType = TileElementType::Corrupt;
|
||||
|
||||
uint8_t pad[3];
|
||||
uint8_t pad_08[8];
|
||||
};
|
||||
|
||||
130
src/openrct2/world/TileElementsView.h
Normal file
130
src/openrct2/world/TileElementsView.h
Normal file
@@ -0,0 +1,130 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2021 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "Location.hpp"
|
||||
#include "Map.h"
|
||||
#include "TileElement.h"
|
||||
|
||||
#include <iterator>
|
||||
|
||||
namespace OpenRCT2
|
||||
{
|
||||
namespace Detail
|
||||
{
|
||||
template<typename T, typename T2> T* NextMatchingTile(T2* element)
|
||||
{
|
||||
if (element == nullptr)
|
||||
return nullptr;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
auto* res = element->template as<T>();
|
||||
if (res != nullptr)
|
||||
return res;
|
||||
|
||||
if (element->IsLastForTile())
|
||||
{
|
||||
break;
|
||||
}
|
||||
element++;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace Detail
|
||||
|
||||
template<typename T = TileElement> class TileElementsView
|
||||
{
|
||||
const CoordsXY _loc;
|
||||
|
||||
public:
|
||||
struct Iterator
|
||||
{
|
||||
T* element = nullptr;
|
||||
|
||||
Iterator& operator++()
|
||||
{
|
||||
if (element == nullptr)
|
||||
return *this;
|
||||
|
||||
if (element->IsLastForTile())
|
||||
{
|
||||
element = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
element++;
|
||||
if constexpr (!std::is_same_v<T, TileElement>)
|
||||
{
|
||||
element = Detail::NextMatchingTile<T>(element);
|
||||
}
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
Iterator operator++(int)
|
||||
{
|
||||
Iterator res = *this;
|
||||
++(*this);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool operator==(Iterator other) const
|
||||
{
|
||||
return element == other.element;
|
||||
}
|
||||
|
||||
bool operator!=(Iterator other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
T* operator*()
|
||||
{
|
||||
return element;
|
||||
}
|
||||
|
||||
const T* operator*() const
|
||||
{
|
||||
return element;
|
||||
}
|
||||
|
||||
// iterator traits
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using value_type = T;
|
||||
using pointer = const T*;
|
||||
using reference = const T&;
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
};
|
||||
|
||||
TileElementsView(const CoordsXY& loc)
|
||||
: _loc(loc)
|
||||
{
|
||||
}
|
||||
|
||||
Iterator begin() noexcept
|
||||
{
|
||||
T* element = reinterpret_cast<T*>(map_get_first_element_at(_loc));
|
||||
|
||||
if constexpr (!std::is_same_v<T, TileElement>)
|
||||
{
|
||||
element = Detail::NextMatchingTile<T>(element);
|
||||
}
|
||||
|
||||
return Iterator{ element };
|
||||
}
|
||||
|
||||
Iterator end() noexcept
|
||||
{
|
||||
return Iterator{ nullptr };
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace OpenRCT2
|
||||
180
test/tests/TileElementsView.cpp
Normal file
180
test/tests/TileElementsView.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
/*****************************************************************************
|
||||
* Copyright (c) 2014-2021 OpenRCT2 developers
|
||||
*
|
||||
* For a complete list of all authors, please refer to contributors.md
|
||||
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
||||
*
|
||||
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "TestData.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <openrct2/Context.h>
|
||||
#include <openrct2/Game.h>
|
||||
#include <openrct2/OpenRCT2.h>
|
||||
#include <openrct2/ParkImporter.h>
|
||||
#include <openrct2/world/Footpath.h>
|
||||
#include <openrct2/world/Map.h>
|
||||
#include <openrct2/world/TileElementsView.h>
|
||||
|
||||
using namespace OpenRCT2;
|
||||
|
||||
class TileElementsViewTests : public testing::Test
|
||||
{
|
||||
protected:
|
||||
static void SetUpTestCase()
|
||||
{
|
||||
std::string parkPath = TestData::GetParkPath("bpb.sv6");
|
||||
gOpenRCT2Headless = true;
|
||||
gOpenRCT2NoGraphics = true;
|
||||
_context = CreateContext();
|
||||
bool initialised = _context->Initialise();
|
||||
ASSERT_TRUE(initialised);
|
||||
|
||||
load_from_sv6(parkPath.c_str());
|
||||
game_load_init();
|
||||
|
||||
// Changed in some tests. Store to restore its value
|
||||
_gScreenFlags = gScreenFlags;
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
static void TearDownTestCase()
|
||||
{
|
||||
if (_context)
|
||||
_context.reset();
|
||||
|
||||
gScreenFlags = _gScreenFlags;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::shared_ptr<IContext> _context;
|
||||
static uint8_t _gScreenFlags;
|
||||
};
|
||||
|
||||
std::shared_ptr<IContext> TileElementsViewTests::_context;
|
||||
uint8_t TileElementsViewTests::_gScreenFlags;
|
||||
|
||||
template<typename T> std::vector<T*> BuildListManual(const CoordsXY& pos)
|
||||
{
|
||||
std::vector<TileElement*> res;
|
||||
|
||||
TileElement* element = map_get_first_element_at(pos);
|
||||
if (element == nullptr)
|
||||
return res;
|
||||
|
||||
do
|
||||
{
|
||||
if constexpr (!std::is_same_v<T, TileElement>)
|
||||
{
|
||||
auto* res = element->as<T>();
|
||||
if (res)
|
||||
res.push_back(res);
|
||||
}
|
||||
else
|
||||
{
|
||||
res.push_back(element);
|
||||
}
|
||||
|
||||
} while (!(element++)->IsLastForTile());
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename T> std::vector<T*> BuildListByView(const CoordsXY& pos)
|
||||
{
|
||||
std::vector<TileElement*> res;
|
||||
|
||||
for (auto* element : TileElementsView<T>(pos))
|
||||
{
|
||||
res.push_back(element);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename T> bool CompareLists(const CoordsXY& pos)
|
||||
{
|
||||
auto listManual = BuildListManual<TileElement>(pos);
|
||||
auto listView = BuildListByView<TileElement>(pos);
|
||||
|
||||
EXPECT_EQ(listManual.size(), listView.size());
|
||||
if (listManual.size() != listView.size())
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < listManual.size(); ++i)
|
||||
{
|
||||
EXPECT_EQ(listManual[i], listView[i]) << "[i] = " << i;
|
||||
|
||||
if (listManual[i] != listView[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T> void CheckMapTiles()
|
||||
{
|
||||
for (int x = 0; x < MAXIMUM_MAP_SIZE_TECHNICAL; ++x)
|
||||
{
|
||||
for (int y = 0; y < MAXIMUM_MAP_SIZE_TECHNICAL; ++y)
|
||||
{
|
||||
auto pos = TileCoordsXY(x, y).ToCoordsXY();
|
||||
|
||||
bool matches = CompareLists<T>(pos);
|
||||
EXPECT_TRUE(matches) << "x = " << x << ", y = " << y;
|
||||
|
||||
if (!matches)
|
||||
{
|
||||
FAIL();
|
||||
}
|
||||
}
|
||||
}
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST_F(TileElementsViewTests, QueryTypeGeneric)
|
||||
{
|
||||
CheckMapTiles<TileElement>();
|
||||
}
|
||||
|
||||
TEST_F(TileElementsViewTests, QueryTypePathElements)
|
||||
{
|
||||
CheckMapTiles<PathElement>();
|
||||
}
|
||||
|
||||
TEST_F(TileElementsViewTests, QueryTypeSurfaceElements)
|
||||
{
|
||||
CheckMapTiles<SurfaceElement>();
|
||||
}
|
||||
|
||||
TEST_F(TileElementsViewTests, QueryTypeTrackElements)
|
||||
{
|
||||
CheckMapTiles<TrackElement>();
|
||||
}
|
||||
|
||||
TEST_F(TileElementsViewTests, QueryTypeSmallSceneryElements)
|
||||
{
|
||||
CheckMapTiles<SmallSceneryElement>();
|
||||
}
|
||||
|
||||
TEST_F(TileElementsViewTests, QueryTypeLargeSceneryElements)
|
||||
{
|
||||
CheckMapTiles<LargeSceneryElement>();
|
||||
}
|
||||
|
||||
TEST_F(TileElementsViewTests, QueryTypeWallElements)
|
||||
{
|
||||
CheckMapTiles<WallElement>();
|
||||
}
|
||||
|
||||
TEST_F(TileElementsViewTests, QueryTypeEntranceElements)
|
||||
{
|
||||
CheckMapTiles<EntranceElement>();
|
||||
}
|
||||
|
||||
TEST_F(TileElementsViewTests, QueryTypeBannerElements)
|
||||
{
|
||||
CheckMapTiles<BannerElement>();
|
||||
}
|
||||
@@ -78,6 +78,7 @@
|
||||
<ClCompile Include="tests.cpp" />
|
||||
<ClCompile Include="StringTest.cpp" />
|
||||
<ClCompile Include="TileElements.cpp" />
|
||||
<ClCompile Include="TileElementsView.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="testdata\sprites\badManifest.json" />
|
||||
|
||||
Reference in New Issue
Block a user