mirror of
https://github.com/minetest/minetest.git
synced 2024-09-12 00:26:50 +02:00
Software inventorycube (#7651)
Fixes missing/upside-down images on Android.
This commit is contained in:
parent
5f0c969485
commit
18a8fbf465
@ -719,10 +719,6 @@ ambient_occlusion_gamma (Ambient occlusion gamma) float 2.2 0.25 4.0
|
||||
# Enables animation of inventory items.
|
||||
inventory_items_animations (Inventory items animations) bool false
|
||||
|
||||
# Android systems only: Tries to create inventory textures from meshes
|
||||
# when no supported render was found.
|
||||
inventory_image_hack (Inventory image hack) bool false
|
||||
|
||||
# Fraction of the visible distance at which fog starts to be rendered
|
||||
fog_start (Fog start) float 0.4 0.0 0.99
|
||||
|
||||
|
@ -39,8 +39,6 @@ file can usually be found at /mnt/sdcard/Minetest.
|
||||
* gui_scaling: this is a user-specified scaling factor for the GUI- In case
|
||||
main menu is too big or small on your device, try changing this
|
||||
value.
|
||||
* inventory_image_hack: if your inventory items are messed up, try setting
|
||||
this to true
|
||||
|
||||
Known issues
|
||||
------------
|
||||
|
@ -841,11 +841,6 @@
|
||||
# type: bool
|
||||
# inventory_items_animations = false
|
||||
|
||||
# Android systems only: Tries to create inventory textures from meshes
|
||||
# when no supported render was found.
|
||||
# type: bool
|
||||
# inventory_image_hack = false
|
||||
|
||||
# Fraction of the visible distance at which fog starts to be rendered
|
||||
# type: float min: 0 max: 0.99
|
||||
# fog_start = 0.4
|
||||
|
@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
|
||||
#include "tile.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ICameraSceneNode.h>
|
||||
#include "util/string.h"
|
||||
#include "util/container.h"
|
||||
@ -362,12 +363,6 @@ public:
|
||||
// Shall be called from the main thread.
|
||||
void rebuildImagesAndTextures();
|
||||
|
||||
// Render a mesh to a texture.
|
||||
// Returns NULL if render-to-texture failed.
|
||||
// Shall be called from the main thread.
|
||||
video::ITexture* generateTextureFromMesh(
|
||||
const TextureFromMeshParams ¶ms);
|
||||
|
||||
video::ITexture* getNormalTexture(const std::string &name);
|
||||
video::SColor getTextureAverageColor(const std::string &name);
|
||||
video::ITexture *getShaderFlagsTexture(bool normamap_present);
|
||||
@ -775,205 +770,121 @@ void TextureSource::rebuildImagesAndTextures()
|
||||
}
|
||||
}
|
||||
|
||||
video::ITexture* TextureSource::generateTextureFromMesh(
|
||||
const TextureFromMeshParams ¶ms)
|
||||
inline static void applyShadeFactor(video::SColor &color, u32 factor)
|
||||
{
|
||||
u32 f = core::clamp<u32>(factor, 0, 256);
|
||||
color.setRed(color.getRed() * f / 256);
|
||||
color.setGreen(color.getGreen() * f / 256);
|
||||
color.setBlue(color.getBlue() * f / 256);
|
||||
}
|
||||
|
||||
static video::IImage *createInventoryCubeImage(
|
||||
video::IImage *top, video::IImage *left, video::IImage *right)
|
||||
{
|
||||
core::dimension2du size_top = top->getDimension();
|
||||
core::dimension2du size_left = left->getDimension();
|
||||
core::dimension2du size_right = right->getDimension();
|
||||
|
||||
u32 size = npot2(std::max({
|
||||
size_top.Width, size_top.Height,
|
||||
size_left.Width, size_left.Height,
|
||||
size_right.Width, size_right.Height,
|
||||
}));
|
||||
|
||||
// It must be divisible by 4, to let everything work correctly.
|
||||
// But it is a power of 2, so being at least 4 is the same.
|
||||
// And the resulting texture should't be too large as well.
|
||||
size = core::clamp<u32>(size, 4, 64);
|
||||
|
||||
// With such parameters, the cube fits exactly, touching each image line
|
||||
// from `0` to `cube_size - 1`. (Note that division is exact here).
|
||||
u32 cube_size = 9 * size;
|
||||
u32 offset = size / 2;
|
||||
|
||||
video::IVideoDriver *driver = RenderingEngine::get_video_driver();
|
||||
sanity_check(driver);
|
||||
|
||||
#ifdef __ANDROID__
|
||||
const GLubyte* renderstr = glGetString(GL_RENDERER);
|
||||
std::string renderer((char*) renderstr);
|
||||
auto lock_image = [size, driver] (video::IImage *&image) -> const u32 * {
|
||||
image->grab();
|
||||
core::dimension2du dim = image->getDimension();
|
||||
video::ECOLOR_FORMAT format = image->getColorFormat();
|
||||
if (dim.Width != size || dim.Height != size || format != video::ECF_A8R8G8B8) {
|
||||
video::IImage *scaled = driver->createImage(video::ECF_A8R8G8B8, {size, size});
|
||||
image->copyToScaling(scaled);
|
||||
image->drop();
|
||||
image = scaled;
|
||||
}
|
||||
sanity_check(image->getPitch() == 4 * size);
|
||||
return reinterpret_cast<u32 *>(image->lock());
|
||||
};
|
||||
auto free_image = [] (video::IImage *image) -> void {
|
||||
image->unlock();
|
||||
image->drop();
|
||||
};
|
||||
|
||||
// use no render to texture hack
|
||||
if (
|
||||
(renderer.find("Adreno") != std::string::npos) ||
|
||||
(renderer.find("Mali") != std::string::npos) ||
|
||||
(renderer.find("Immersion") != std::string::npos) ||
|
||||
(renderer.find("Tegra") != std::string::npos) ||
|
||||
g_settings->getBool("inventory_image_hack")
|
||||
) {
|
||||
// Get a scene manager
|
||||
scene::ISceneManager *smgr_main = RenderingEngine::get_raw_device()->getSceneManager();
|
||||
sanity_check(smgr_main);
|
||||
scene::ISceneManager *smgr = smgr_main->createNewSceneManager();
|
||||
sanity_check(smgr);
|
||||
video::IImage *result = driver->createImage(video::ECF_A8R8G8B8, {cube_size, cube_size});
|
||||
sanity_check(result->getPitch() == 4 * cube_size);
|
||||
result->fill(video::SColor(0x00000000u));
|
||||
u32 *target = reinterpret_cast<u32 *>(result->lock());
|
||||
|
||||
const float scaling = 0.2;
|
||||
// Draws single cube face
|
||||
// `shade_factor` is face brightness, in range [0.0, 1.0]
|
||||
// (xu, xv, x1; yu, yv, y1) form coordinate transformation matrix
|
||||
// `offsets` list pixels to be drawn for single source pixel
|
||||
auto draw_image = [=] (video::IImage *image, float shade_factor,
|
||||
s16 xu, s16 xv, s16 x1,
|
||||
s16 yu, s16 yv, s16 y1,
|
||||
std::initializer_list<v2s16> offsets) -> void {
|
||||
u32 brightness = core::clamp<u32>(256 * shade_factor, 0, 256);
|
||||
const u32 *source = lock_image(image);
|
||||
for (u16 v = 0; v < size; v++) {
|
||||
for (u16 u = 0; u < size; u++) {
|
||||
video::SColor pixel(*source);
|
||||
applyShadeFactor(pixel, brightness);
|
||||
s16 x = xu * u + xv * v + x1;
|
||||
s16 y = yu * u + yv * v + y1;
|
||||
for (const auto &off : offsets)
|
||||
target[(y + off.Y) * cube_size + (x + off.X) + offset] = pixel.color;
|
||||
source++;
|
||||
}
|
||||
}
|
||||
free_image(image);
|
||||
};
|
||||
|
||||
scene::IMeshSceneNode* meshnode =
|
||||
smgr->addMeshSceneNode(params.mesh, NULL,
|
||||
-1, v3f(0,0,0), v3f(0,0,0),
|
||||
v3f(1.0 * scaling,1.0 * scaling,1.0 * scaling), true);
|
||||
meshnode->setMaterialFlag(video::EMF_LIGHTING, true);
|
||||
meshnode->setMaterialFlag(video::EMF_ANTI_ALIASING, true);
|
||||
meshnode->setMaterialFlag(video::EMF_TRILINEAR_FILTER, m_setting_trilinear_filter);
|
||||
meshnode->setMaterialFlag(video::EMF_BILINEAR_FILTER, m_setting_bilinear_filter);
|
||||
meshnode->setMaterialFlag(video::EMF_ANISOTROPIC_FILTER, m_setting_anisotropic_filter);
|
||||
|
||||
scene::ICameraSceneNode* camera = smgr->addCameraSceneNode(0,
|
||||
params.camera_position, params.camera_lookat);
|
||||
// second parameter of setProjectionMatrix (isOrthogonal) is ignored
|
||||
camera->setProjectionMatrix(params.camera_projection_matrix, false);
|
||||
|
||||
smgr->setAmbientLight(params.ambient_light);
|
||||
smgr->addLightSceneNode(0,
|
||||
params.light_position,
|
||||
params.light_color,
|
||||
params.light_radius*scaling);
|
||||
|
||||
core::dimension2d<u32> screen = driver->getScreenSize();
|
||||
|
||||
// Render scene
|
||||
driver->beginScene(true, true, video::SColor(0,0,0,0));
|
||||
driver->clearZBuffer();
|
||||
smgr->drawAll();
|
||||
|
||||
core::dimension2d<u32> partsize(screen.Width * scaling,screen.Height * scaling);
|
||||
|
||||
irr::video::IImage* rawImage =
|
||||
driver->createImage(irr::video::ECF_A8R8G8B8, partsize);
|
||||
|
||||
u8* pixels = static_cast<u8*>(rawImage->lock());
|
||||
if (!pixels)
|
||||
draw_image(top, 1.000000f,
|
||||
4, -4, 4 * (size - 1),
|
||||
2, 2, 0,
|
||||
{
|
||||
rawImage->drop();
|
||||
return NULL;
|
||||
}
|
||||
{2, 0}, {3, 0}, {4, 0}, {5, 0},
|
||||
{0, 1}, {1, 1}, {2, 1}, {3, 1}, {4, 1}, {5, 1}, {6, 1}, {7, 1},
|
||||
{2, 2}, {3, 2}, {4, 2}, {5, 2},
|
||||
});
|
||||
|
||||
core::rect<s32> source(
|
||||
screen.Width /2 - (screen.Width * (scaling / 2)),
|
||||
screen.Height/2 - (screen.Height * (scaling / 2)),
|
||||
screen.Width /2 + (screen.Width * (scaling / 2)),
|
||||
screen.Height/2 + (screen.Height * (scaling / 2))
|
||||
);
|
||||
|
||||
glReadPixels(source.UpperLeftCorner.X, source.UpperLeftCorner.Y,
|
||||
partsize.Width, partsize.Height, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, pixels);
|
||||
|
||||
driver->endScene();
|
||||
|
||||
// Drop scene manager
|
||||
smgr->drop();
|
||||
|
||||
unsigned int pixelcount = partsize.Width*partsize.Height;
|
||||
|
||||
u8* runptr = pixels;
|
||||
for (unsigned int i=0; i < pixelcount; i++) {
|
||||
|
||||
u8 B = *runptr;
|
||||
u8 G = *(runptr+1);
|
||||
u8 R = *(runptr+2);
|
||||
u8 A = *(runptr+3);
|
||||
|
||||
//BGRA -> RGBA
|
||||
*runptr = R;
|
||||
runptr ++;
|
||||
*runptr = G;
|
||||
runptr ++;
|
||||
*runptr = B;
|
||||
runptr ++;
|
||||
*runptr = A;
|
||||
runptr ++;
|
||||
}
|
||||
|
||||
video::IImage* inventory_image =
|
||||
driver->createImage(irr::video::ECF_A8R8G8B8, params.dim);
|
||||
|
||||
rawImage->copyToScaling(inventory_image);
|
||||
rawImage->drop();
|
||||
|
||||
guiScalingCache(io::path(params.rtt_texture_name.c_str()), driver, inventory_image);
|
||||
|
||||
video::ITexture *rtt = driver->addTexture(params.rtt_texture_name.c_str(), inventory_image);
|
||||
inventory_image->drop();
|
||||
|
||||
if (rtt == NULL) {
|
||||
errorstream << "TextureSource::generateTextureFromMesh(): failed to recreate texture from image: " << params.rtt_texture_name << std::endl;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
driver->makeColorKeyTexture(rtt, v2s32(0,0));
|
||||
|
||||
if (params.delete_texture_on_shutdown)
|
||||
m_texture_trash.push_back(rtt);
|
||||
|
||||
return rtt;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!driver->queryFeature(video::EVDF_RENDER_TO_TARGET)) {
|
||||
static bool warned = false;
|
||||
if (!warned)
|
||||
draw_image(left, 0.836660f,
|
||||
4, 0, 0,
|
||||
2, 5, 2 * size,
|
||||
{
|
||||
errorstream<<"TextureSource::generateTextureFromMesh(): "
|
||||
<<"EVDF_RENDER_TO_TARGET not supported."<<std::endl;
|
||||
warned = true;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
{0, 0}, {1, 0},
|
||||
{0, 1}, {1, 1}, {2, 1}, {3, 1},
|
||||
{0, 2}, {1, 2}, {2, 2}, {3, 2},
|
||||
{0, 3}, {1, 3}, {2, 3}, {3, 3},
|
||||
{0, 4}, {1, 4}, {2, 4}, {3, 4},
|
||||
{2, 5}, {3, 5},
|
||||
});
|
||||
|
||||
// Create render target texture
|
||||
video::ITexture *rtt = driver->addRenderTargetTexture(
|
||||
params.dim, params.rtt_texture_name.c_str(),
|
||||
video::ECF_A8R8G8B8);
|
||||
if (rtt == NULL)
|
||||
draw_image(right, 0.670820f,
|
||||
4, 0, 4 * size,
|
||||
-2, 5, 4 * size - 2,
|
||||
{
|
||||
errorstream<<"TextureSource::generateTextureFromMesh(): "
|
||||
<<"addRenderTargetTexture returned NULL."<<std::endl;
|
||||
return NULL;
|
||||
}
|
||||
{2, 0}, {3, 0},
|
||||
{0, 1}, {1, 1}, {2, 1}, {3, 1},
|
||||
{0, 2}, {1, 2}, {2, 2}, {3, 2},
|
||||
{0, 3}, {1, 3}, {2, 3}, {3, 3},
|
||||
{0, 4}, {1, 4}, {2, 4}, {3, 4},
|
||||
{0, 5}, {1, 5},
|
||||
});
|
||||
|
||||
// Set render target
|
||||
if (!driver->setRenderTarget(rtt, false, true, video::SColor(0,0,0,0))) {
|
||||
driver->removeTexture(rtt);
|
||||
errorstream<<"TextureSource::generateTextureFromMesh(): "
|
||||
<<"failed to set render target"<<std::endl;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get a scene manager
|
||||
scene::ISceneManager *smgr_main = RenderingEngine::get_scene_manager();
|
||||
assert(smgr_main);
|
||||
scene::ISceneManager *smgr = smgr_main->createNewSceneManager();
|
||||
assert(smgr);
|
||||
|
||||
scene::IMeshSceneNode* meshnode =
|
||||
smgr->addMeshSceneNode(params.mesh, NULL,
|
||||
-1, v3f(0,0,0), v3f(0,0,0), v3f(1,1,1), true);
|
||||
meshnode->setMaterialFlag(video::EMF_LIGHTING, true);
|
||||
meshnode->setMaterialFlag(video::EMF_ANTI_ALIASING, true);
|
||||
meshnode->setMaterialFlag(video::EMF_TRILINEAR_FILTER, m_setting_trilinear_filter);
|
||||
meshnode->setMaterialFlag(video::EMF_BILINEAR_FILTER, m_setting_bilinear_filter);
|
||||
meshnode->setMaterialFlag(video::EMF_ANISOTROPIC_FILTER, m_setting_anisotropic_filter);
|
||||
|
||||
scene::ICameraSceneNode* camera = smgr->addCameraSceneNode(0,
|
||||
params.camera_position, params.camera_lookat);
|
||||
// second parameter of setProjectionMatrix (isOrthogonal) is ignored
|
||||
camera->setProjectionMatrix(params.camera_projection_matrix, false);
|
||||
|
||||
smgr->setAmbientLight(params.ambient_light);
|
||||
smgr->addLightSceneNode(0,
|
||||
params.light_position,
|
||||
params.light_color,
|
||||
params.light_radius);
|
||||
|
||||
// Render scene
|
||||
driver->beginScene(true, true, video::SColor(0,0,0,0));
|
||||
smgr->drawAll();
|
||||
driver->endScene();
|
||||
|
||||
// Drop scene manager
|
||||
smgr->drop();
|
||||
|
||||
// Unset render target
|
||||
driver->setRenderTarget(0, false, true, video::SColor(0,0,0,0));
|
||||
|
||||
if (params.delete_texture_on_shutdown)
|
||||
m_texture_trash.push_back(rtt);
|
||||
|
||||
return rtt;
|
||||
result->unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
video::IImage* TextureSource::generateImage(const std::string &name)
|
||||
@ -1516,90 +1427,15 @@ bool TextureSource::generateImagePart(std::string part_of_name,
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
assert(img_top->getDimension().Height == npot2(img_top->getDimension().Height));
|
||||
assert(img_top->getDimension().Width == npot2(img_top->getDimension().Width));
|
||||
baseimg = createInventoryCubeImage(img_top, img_left, img_right);
|
||||
|
||||
assert(img_left->getDimension().Height == npot2(img_left->getDimension().Height));
|
||||
assert(img_left->getDimension().Width == npot2(img_left->getDimension().Width));
|
||||
|
||||
assert(img_right->getDimension().Height == npot2(img_right->getDimension().Height));
|
||||
assert(img_right->getDimension().Width == npot2(img_right->getDimension().Width));
|
||||
#endif
|
||||
|
||||
// Create textures from images
|
||||
video::ITexture *texture_top = driver->addTexture(
|
||||
(imagename_top + "__temp__").c_str(), img_top);
|
||||
video::ITexture *texture_left = driver->addTexture(
|
||||
(imagename_left + "__temp__").c_str(), img_left);
|
||||
video::ITexture *texture_right = driver->addTexture(
|
||||
(imagename_right + "__temp__").c_str(), img_right);
|
||||
FATAL_ERROR_IF(!(texture_top && texture_left && texture_right), "");
|
||||
|
||||
// Drop images
|
||||
// Face images are not needed anymore
|
||||
img_top->drop();
|
||||
img_left->drop();
|
||||
img_right->drop();
|
||||
|
||||
/*
|
||||
Draw a cube mesh into a render target texture
|
||||
*/
|
||||
scene::IMesh* cube = createCubeMesh(v3f(1, 1, 1));
|
||||
setMeshColor(cube, video::SColor(255, 255, 255, 255));
|
||||
cube->getMeshBuffer(0)->getMaterial().setTexture(0, texture_top);
|
||||
cube->getMeshBuffer(1)->getMaterial().setTexture(0, texture_top);
|
||||
cube->getMeshBuffer(2)->getMaterial().setTexture(0, texture_right);
|
||||
cube->getMeshBuffer(3)->getMaterial().setTexture(0, texture_right);
|
||||
cube->getMeshBuffer(4)->getMaterial().setTexture(0, texture_left);
|
||||
cube->getMeshBuffer(5)->getMaterial().setTexture(0, texture_left);
|
||||
|
||||
TextureFromMeshParams params;
|
||||
params.mesh = cube;
|
||||
params.dim.set(64, 64);
|
||||
params.rtt_texture_name = part_of_name + "_RTT";
|
||||
// We will delete the rtt texture ourselves
|
||||
params.delete_texture_on_shutdown = false;
|
||||
params.camera_position.set(0, 1.0, -1.5);
|
||||
params.camera_position.rotateXZBy(45);
|
||||
params.camera_lookat.set(0, 0, 0);
|
||||
// Set orthogonal projection
|
||||
params.camera_projection_matrix.buildProjectionMatrixOrthoLH(
|
||||
1.65, 1.65, 0, 100);
|
||||
|
||||
params.ambient_light.set(1.0, 0.2, 0.2, 0.2);
|
||||
params.light_position.set(10, 100, -50);
|
||||
params.light_color.set(1.0, 0.5, 0.5, 0.5);
|
||||
params.light_radius = 1000;
|
||||
|
||||
video::ITexture *rtt = generateTextureFromMesh(params);
|
||||
|
||||
// Drop mesh
|
||||
cube->drop();
|
||||
|
||||
// Free textures
|
||||
driver->removeTexture(texture_top);
|
||||
driver->removeTexture(texture_left);
|
||||
driver->removeTexture(texture_right);
|
||||
|
||||
if (rtt == NULL) {
|
||||
baseimg = generateImage(imagename_top);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create image of render target
|
||||
video::IImage *image = driver->createImage(rtt, v2s32(0, 0), params.dim);
|
||||
FATAL_ERROR_IF(!image, "Could not create image of render target");
|
||||
|
||||
// Cleanup texture
|
||||
driver->removeTexture(rtt);
|
||||
|
||||
baseimg = driver->createImage(video::ECF_A8R8G8B8, params.dim);
|
||||
|
||||
if (image) {
|
||||
image->copyTo(baseimg);
|
||||
image->drop();
|
||||
}
|
||||
}
|
||||
/*
|
||||
[lowpart:percent:filename
|
||||
Adds the lower part of a texture
|
||||
|
@ -67,25 +67,6 @@ std::string getTexturePath(const std::string &filename);
|
||||
|
||||
void clearTextureNameCache();
|
||||
|
||||
/*
|
||||
ITextureSource::generateTextureFromMesh parameters
|
||||
*/
|
||||
namespace irr {namespace scene {class IMesh;}}
|
||||
struct TextureFromMeshParams
|
||||
{
|
||||
scene::IMesh *mesh = nullptr;
|
||||
core::dimension2d<u32> dim;
|
||||
std::string rtt_texture_name;
|
||||
bool delete_texture_on_shutdown;
|
||||
v3f camera_position;
|
||||
v3f camera_lookat;
|
||||
core::CMatrix4<f32> camera_projection_matrix;
|
||||
video::SColorf ambient_light;
|
||||
v3f light_position;
|
||||
video::SColorf light_color;
|
||||
f32 light_radius;
|
||||
};
|
||||
|
||||
/*
|
||||
TextureSource creates and caches textures.
|
||||
*/
|
||||
@ -123,8 +104,6 @@ public:
|
||||
*/
|
||||
virtual Palette* getPalette(const std::string &name) = 0;
|
||||
virtual bool isKnownSourceImage(const std::string &name)=0;
|
||||
virtual video::ITexture* generateTextureFromMesh(
|
||||
const TextureFromMeshParams ¶ms)=0;
|
||||
virtual video::ITexture* getNormalTexture(const std::string &name)=0;
|
||||
virtual video::SColor getTextureAverageColor(const std::string &name)=0;
|
||||
virtual video::ITexture *getShaderFlagsTexture(bool normalmap_present)=0;
|
||||
@ -143,8 +122,6 @@ public:
|
||||
virtual video::ITexture* getTexture(
|
||||
const std::string &name, u32 *id = nullptr)=0;
|
||||
virtual bool isKnownSourceImage(const std::string &name)=0;
|
||||
virtual video::ITexture* generateTextureFromMesh(
|
||||
const TextureFromMeshParams ¶ms)=0;
|
||||
|
||||
virtual void processQueue()=0;
|
||||
virtual void insertSourceImage(const std::string &name, video::IImage *img)=0;
|
||||
|
@ -427,7 +427,6 @@ void set_default_settings(Settings *settings)
|
||||
settings->setDefault("chunksize", "5");
|
||||
|
||||
settings->setDefault("viewing_range", "50");
|
||||
settings->setDefault("inventory_image_hack", "false");
|
||||
|
||||
// Check for a device with a small screen
|
||||
float x_inches = ((double) porting::getDisplaySize().X /
|
||||
|
Loading…
Reference in New Issue
Block a user