Too many changes. See details:
- Fix new block color with tint system slowdown - Fix leaves block color - Fix rgba not translating to rgb properly - Which changed water color to be darker - Optimized quad merging to be faster - Fix All types of gray color texture issue - Fix Textures that rely on BlockState - Fix All types of tint - (?)Create Model perhaps no longer purple - Fixed LodBufferBuilder always in single thread mode
This commit is contained in:
+3
-3
@@ -298,12 +298,12 @@ public class LodBufferBuilderFactory {
|
||||
LodQuadBuilder quadBuilder = new LodQuadBuilder(6);
|
||||
makeLodRenderData(quadBuilder, lodDim, regionPos, pX, pZ, minDetail);
|
||||
return new ResultPair(quadBuilder, regionPos);
|
||||
}, bufferUploadThread).whenCompleteAsync((result, e) -> {
|
||||
}, bufferBuilderThreads).whenCompleteAsync((result, e) -> {
|
||||
if (e != null)
|
||||
return;
|
||||
try {
|
||||
uploadBuffers(result.quadBuilder, result.regionPos);
|
||||
} catch (Exception e3) {
|
||||
} catch (Throwable e3) {
|
||||
ApiShared.LOGGER.error("\"LodNodeBufferBuilder\" was unable to upload buffer: ", e3);
|
||||
}
|
||||
}, bufferUploadThread);
|
||||
@@ -320,7 +320,7 @@ public class LodBufferBuilderFactory {
|
||||
CompletableFuture<Void> allFutures = CompletableFuture
|
||||
.allOf(futuresBuffer.toArray(new CompletableFuture[futuresBuffer.size()]));
|
||||
try {
|
||||
allFutures.get(5, TimeUnit.MINUTES);
|
||||
allFutures.get(1, TimeUnit.MINUTES);
|
||||
} catch (TimeoutException te) {
|
||||
ApiShared.LOGGER.error("LodBufferBuilder timed out: ", te);
|
||||
bufferBuilderThreadFactory.dumpAllThreadStacks();
|
||||
|
||||
@@ -7,11 +7,11 @@ import java.util.Iterator;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import com.seibel.lod.core.api.ApiShared;
|
||||
import com.seibel.lod.core.api.ClientApi;
|
||||
import com.seibel.lod.core.enums.LodDirection;
|
||||
import com.seibel.lod.core.enums.LodDirection.Axis;
|
||||
import com.seibel.lod.core.enums.config.GpuUploadMethod;
|
||||
import com.seibel.lod.core.util.ColorUtil;
|
||||
import com.seibel.lod.core.util.LodUtil;
|
||||
|
||||
public class LodQuadBuilder {
|
||||
static final int MAX_BUFFER_SIZE = (1024 * 1024 * 1);
|
||||
@@ -51,14 +51,14 @@ public class LodQuadBuilder {
|
||||
distance = pow(relativeX-x) + pow(relativeY-y) + pow(relativeZ-z);
|
||||
}
|
||||
|
||||
private static int _compondCompare(short a0, short b0, short c0, short a1, short b1, short c1) {
|
||||
if (a0 != a1) return a0-a1;
|
||||
if (b0 != b1) return b0-b1;
|
||||
return c0-c1;
|
||||
private static int _compondCompare(short a0, short a1, short a2, short b0, short b1, short b2) {
|
||||
long a = (long)a0<<48 | (long)a1<<32 | (long)a2 << 16;
|
||||
long b = (long)b0<<48 | (long)b1<<32 | (long)b2 << 16;
|
||||
return Long.compare(a, b);
|
||||
}
|
||||
|
||||
public int compareTo1(Quad o) {
|
||||
if (dir != o.dir) return dir.compareTo(o.dir);
|
||||
if (dir != o.dir) throw new IllegalArgumentException("The other quad is not in the same direction: " + o.dir + " vs "+dir);
|
||||
switch (dir.getAxis()) {
|
||||
case X:
|
||||
return _compondCompare(x, y, z, o.x, o.y, o.z);
|
||||
@@ -71,7 +71,7 @@ public class LodQuadBuilder {
|
||||
}
|
||||
}
|
||||
public int compareTo2(Quad o) {
|
||||
if (dir != o.dir) return dir.compareTo(o.dir);
|
||||
if (dir != o.dir) throw new IllegalArgumentException("The other quad is not in the same direction: " + o.dir + " vs "+dir);
|
||||
switch (dir.getAxis()) {
|
||||
case X:
|
||||
return _compondCompare(x, z, y, o.x, o.z, o.y);
|
||||
@@ -221,44 +221,45 @@ public class LodQuadBuilder {
|
||||
|
||||
}
|
||||
|
||||
final ArrayList<Quad> quads;
|
||||
final ArrayList<Quad>[] quads;
|
||||
|
||||
public LodQuadBuilder(int initialSize) {
|
||||
quads = new ArrayList<Quad>();
|
||||
quads = new ArrayList[6];
|
||||
for (int i=0; i<6; i++) quads[i] = new ArrayList<Quad>();
|
||||
}
|
||||
|
||||
public void addQuadAdj(LodDirection dir, short x, short y, short z, short w0, short wy, int color, byte skylight,
|
||||
byte blocklight) {
|
||||
if (dir.ordinal() <= LodDirection.DOWN.ordinal())
|
||||
throw new IllegalArgumentException("addQuadAdj() is only for adj direction! Not UP or Down!");
|
||||
quads.add(new Quad(x, y, z, w0, wy, color, skylight, blocklight, dir));
|
||||
quads[dir.ordinal()].add(new Quad(x, y, z, w0, wy, color, skylight, blocklight, dir));
|
||||
}
|
||||
|
||||
// XZ
|
||||
public void addQuadUp(short x, short y, short z, short wx, short wz, int color, byte skylight, byte blocklight) {
|
||||
quads.add(new Quad(x, y, z, wx, wz, color, skylight, blocklight, LodDirection.UP));
|
||||
quads[LodDirection.UP.ordinal()].add(new Quad(x, y, z, wx, wz, color, skylight, blocklight, LodDirection.UP));
|
||||
}
|
||||
|
||||
public void addQuadDown(short x, short y, short z, short wx, short wz, int color, byte skylight, byte blocklight) {
|
||||
quads.add(new Quad(x, y, z, wx, wz, color, skylight, blocklight, LodDirection.DOWN));
|
||||
quads[LodDirection.DOWN.ordinal()].add(new Quad(x, y, z, wx, wz, color, skylight, blocklight, LodDirection.DOWN));
|
||||
}
|
||||
|
||||
// XY
|
||||
public void addQuadN(short x, short y, short z, short wx, short wy, int color, byte skylight, byte blocklight) {
|
||||
quads.add(new Quad(x, y, z, wx, wy, color, skylight, blocklight, LodDirection.NORTH));
|
||||
quads[LodDirection.NORTH.ordinal()].add(new Quad(x, y, z, wx, wy, color, skylight, blocklight, LodDirection.NORTH));
|
||||
}
|
||||
|
||||
public void addQuadS(short x, short y, short z, short wx, short wy, int color, byte skylight, byte blocklight) {
|
||||
quads.add(new Quad(x, y, z, wx, wy, color, skylight, blocklight, LodDirection.SOUTH));
|
||||
quads[LodDirection.SOUTH.ordinal()].add(new Quad(x, y, z, wx, wy, color, skylight, blocklight, LodDirection.SOUTH));
|
||||
}
|
||||
|
||||
// ZY
|
||||
public void addQuadW(short x, short y, short z, short wz, short wy, int color, byte skylight, byte blocklight) {
|
||||
quads.add(new Quad(x, y, z, wz, wy, color, skylight, blocklight, LodDirection.WEST));
|
||||
quads[LodDirection.WEST.ordinal()].add(new Quad(x, y, z, wz, wy, color, skylight, blocklight, LodDirection.WEST));
|
||||
}
|
||||
|
||||
public void addQuadE(short x, short y, short z, short wz, short wy, int color, byte skylight, byte blocklight) {
|
||||
quads.add(new Quad(x, y, z, wz, wy, color, skylight, blocklight, LodDirection.EAST));
|
||||
quads[LodDirection.EAST.ordinal()].add(new Quad(x, y, z, wz, wy, color, skylight, blocklight, LodDirection.EAST));
|
||||
}
|
||||
|
||||
private static void putVertex(ByteBuffer bb, short x, short y, short z, int color, byte skylight, byte blocklight) {
|
||||
@@ -268,6 +269,7 @@ public class LodQuadBuilder {
|
||||
bb.putShort(x);
|
||||
bb.putShort(y);
|
||||
bb.putShort(z);
|
||||
|
||||
bb.putShort((short) (skylight | (blocklight << 4)));
|
||||
byte r = (byte) ColorUtil.getRed(color);
|
||||
byte g = (byte) ColorUtil.getGreen(color);
|
||||
@@ -308,30 +310,16 @@ public class LodQuadBuilder {
|
||||
putVertex(bb, (short) (quad.x + dx), (short) (quad.y + dy), (short) (quad.z + dz), quad.color,
|
||||
quad.skylight, quad.blocklight);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private ByteBuffer writeVertexData(ByteBuffer bb, int quadsStart, int quadsCount) {
|
||||
if (quadsStart + quadsCount > quads.size())
|
||||
quadsCount = quads.size() - quadsStart;
|
||||
bb.clear();
|
||||
bb.limit(quadsCount * QUAD_BYTE_SIZE);
|
||||
for (Quad quad : quads.subList(quadsStart, quadsStart + quadsCount)) {
|
||||
putQuad(bb, quad);
|
||||
}
|
||||
if (bb.hasRemaining())
|
||||
throw new RuntimeException();
|
||||
bb.rewind();
|
||||
return bb;
|
||||
}
|
||||
|
||||
public void sort(double dPlayerPosX, double dPlayerPosY, double dPlayerPosZ) {
|
||||
quads.forEach(p -> p.calculateDistance(dPlayerPosX, dPlayerPosY, dPlayerPosZ));
|
||||
quads.sort((a, b) -> Double.compare(a.distance, b.distance));
|
||||
|
||||
}
|
||||
private long merggeQuadsPass1() {
|
||||
quads.sort(Quad::compareTo1);
|
||||
ListIterator<Quad> iter = quads.listIterator();
|
||||
|
||||
private long mergeQuadsPass1(int dir) {
|
||||
if (quads[dir].size()<=1) return 0;
|
||||
quads[dir].sort(Quad::compareTo1);
|
||||
ListIterator<Quad> iter = quads[dir].listIterator();
|
||||
long mergeCount = 0;
|
||||
Quad currentQuad = iter.next();
|
||||
while (iter.hasNext()) {
|
||||
@@ -343,13 +331,14 @@ public class LodQuadBuilder {
|
||||
currentQuad = nextQuad;
|
||||
}
|
||||
}
|
||||
quads.removeIf(o -> o==null);
|
||||
quads[dir].removeIf(o -> o==null);
|
||||
return mergeCount;
|
||||
}
|
||||
|
||||
private long merggeQuadsPass2() {
|
||||
quads.sort(Quad::compareTo2);
|
||||
ListIterator<Quad> iter = quads.listIterator();
|
||||
|
||||
private long mergeQuadsPass2(int dir) {
|
||||
if (quads[dir].size()<=1) return 0;
|
||||
quads[dir].sort(Quad::compareTo2);
|
||||
ListIterator<Quad> iter = quads[dir].listIterator();
|
||||
long mergeCount = 0;
|
||||
Quad currentQuad = iter.next();
|
||||
while (iter.hasNext()) {
|
||||
@@ -361,42 +350,84 @@ public class LodQuadBuilder {
|
||||
currentQuad = nextQuad;
|
||||
}
|
||||
}
|
||||
quads.removeIf(o -> o==null);
|
||||
quads[dir].removeIf(o -> o==null);
|
||||
return mergeCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void mergeQuads() {
|
||||
if (quads.size()<=1) return;
|
||||
|
||||
long mergeCount = 0;
|
||||
long preQuadsCount = quads.size();
|
||||
mergeCount += merggeQuadsPass1();
|
||||
mergeCount += merggeQuadsPass2();
|
||||
long postQuadsCount = quads.size();
|
||||
long preQuadsCount = getCurrentQuadsCount();
|
||||
if (preQuadsCount<=1) return;
|
||||
long skipperMerge = 0;
|
||||
for (int i=0; i<6; i++) {
|
||||
mergeCount += mergeQuadsPass1(i);
|
||||
if (i>=2) {
|
||||
continue;
|
||||
//long pass2 = mergeQuadsPass2(i);
|
||||
//mergeCount += pass2;
|
||||
//skipperMerge += pass2;
|
||||
} else {
|
||||
long pass2 = mergeQuadsPass2(i);
|
||||
mergeCount += pass2;
|
||||
}
|
||||
}
|
||||
long postQuadsCount = getCurrentQuadsCount();
|
||||
//if (mergeCount != 0)
|
||||
//ApiShared.LOGGER.info("Merged {} out of {} quads, to now {} quads.", mergeCount, preQuadsCount, postQuadsCount);
|
||||
// ApiShared.LOGGER.info("Merged {}/{}({}) quads, skip {}", mergeCount, preQuadsCount, mergeCount/(double)preQuadsCount, skipperMerge);
|
||||
}
|
||||
|
||||
public Iterator<ByteBuffer> makeVertexBuffers() {
|
||||
int numOfBuffers = getCurrentNeededVertexBuffers();
|
||||
return new Iterator<ByteBuffer>() {
|
||||
int counter = 0;
|
||||
ByteBuffer bb = ByteBuffer.allocateDirect(MAX_QUADS_PER_BUFFER * QUAD_BYTE_SIZE)
|
||||
.order(ByteOrder.nativeOrder());
|
||||
|
||||
int dir = skipEmpty(0);
|
||||
int quad = 0;
|
||||
|
||||
private int skipEmpty(int d) {
|
||||
while(d<6 && quads[d].isEmpty()) d++;
|
||||
return d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return counter < numOfBuffers;
|
||||
return dir < 6;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer next() {
|
||||
if (counter >= numOfBuffers) {
|
||||
if (dir >= 6) {
|
||||
return null;
|
||||
}
|
||||
return writeVertexData(bb, MAX_QUADS_PER_BUFFER * counter++, MAX_QUADS_PER_BUFFER);
|
||||
bb.clear();
|
||||
bb.limit(MAX_QUADS_PER_BUFFER * QUAD_BYTE_SIZE);
|
||||
while (bb.hasRemaining() && dir < 6) {
|
||||
writeData();
|
||||
}
|
||||
bb.limit(bb.position());
|
||||
bb.rewind();
|
||||
return bb;
|
||||
}
|
||||
|
||||
private void writeData() {
|
||||
int startQ = quad;
|
||||
|
||||
int i = startQ;
|
||||
for (i = startQ; i<quads[dir].size(); i++) {
|
||||
if (!bb.hasRemaining()) {
|
||||
break;
|
||||
}
|
||||
putQuad(bb, quads[dir].get(i));
|
||||
}
|
||||
|
||||
if (i >= quads[dir].size()) {
|
||||
quad = 0;
|
||||
dir++;
|
||||
dir = skipEmpty(dir);
|
||||
} else {
|
||||
quad = i;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -406,30 +437,71 @@ public class LodQuadBuilder {
|
||||
}
|
||||
|
||||
public BufferFiller makeBufferFiller(GpuUploadMethod method) {
|
||||
int numOfBuffers = getCurrentNeededVertexBuffers();
|
||||
return new BufferFiller() {
|
||||
int counter = 0;
|
||||
int dir = 0;
|
||||
int quad = 0;
|
||||
public boolean fill(LodVertexBuffer vbo) {
|
||||
if (counter >= numOfBuffers) {
|
||||
if (dir >= 6) {
|
||||
vbo.vertexCount = 0;
|
||||
return false;
|
||||
}
|
||||
int numOfQuads = MAX_QUADS_PER_BUFFER;
|
||||
if (quads.size()-(counter*MAX_QUADS_PER_BUFFER) < MAX_QUADS_PER_BUFFER)
|
||||
numOfQuads = quads.size()-(counter*MAX_QUADS_PER_BUFFER);
|
||||
if (numOfQuads != 0) {
|
||||
ByteBuffer bb = vbo.mapBuffer(numOfQuads*QUAD_BYTE_SIZE, method, MAX_QUADS_PER_BUFFER * QUAD_BYTE_SIZE);
|
||||
if (bb == null) throw new NullPointerException("mapBuffer returned null");
|
||||
writeVertexData(bb, MAX_QUADS_PER_BUFFER * counter++, numOfQuads).rewind();
|
||||
vbo.unmapBuffer(method);
|
||||
|
||||
int numOfQuads = _countRemainingQuads();
|
||||
if (numOfQuads > MAX_QUADS_PER_BUFFER) numOfQuads = MAX_QUADS_PER_BUFFER;
|
||||
if (numOfQuads == 0) {
|
||||
vbo.vertexCount = 0;
|
||||
return false;
|
||||
}
|
||||
ByteBuffer bb = vbo.mapBuffer(numOfQuads*QUAD_BYTE_SIZE, method, MAX_QUADS_PER_BUFFER * QUAD_BYTE_SIZE);
|
||||
if (bb == null) throw new NullPointerException("mapBuffer returned null");
|
||||
bb.clear();
|
||||
bb.limit(numOfQuads * QUAD_BYTE_SIZE);
|
||||
while (bb.hasRemaining() && dir < 6) {
|
||||
writeData(bb);
|
||||
}
|
||||
bb.rewind();
|
||||
vbo.unmapBuffer(method);
|
||||
vbo.vertexCount = numOfQuads*6;
|
||||
return counter < numOfBuffers;
|
||||
return dir < 6;
|
||||
}
|
||||
private int _countRemainingQuads() {
|
||||
int a = quads[dir].size() - quad;
|
||||
for (int i=dir+1; i<quads.length; i++) {
|
||||
a+=quads[i].size();
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private void writeData(ByteBuffer bb) {
|
||||
int startQ = quad;
|
||||
|
||||
int i = startQ;
|
||||
for (i = startQ; i<quads[dir].size(); i++) {
|
||||
if (!bb.hasRemaining()) {
|
||||
break;
|
||||
}
|
||||
putQuad(bb, quads[dir].get(i));
|
||||
}
|
||||
|
||||
if (i >= quads[dir].size()) {
|
||||
quad = 0;
|
||||
dir++;
|
||||
while (dir<6 && quads[dir].isEmpty()) dir++;
|
||||
} else {
|
||||
quad = i;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public int getCurrentQuadsCount() {
|
||||
int i = 0;
|
||||
for (ArrayList<Quad> qs : quads) i+=qs.size();
|
||||
return i;
|
||||
}
|
||||
|
||||
public int getCurrentNeededVertexBuffers() {
|
||||
return quads.size() / MAX_QUADS_PER_BUFFER + 1;
|
||||
return LodUtil.ceilDiv(getCurrentQuadsCount(), MAX_QUADS_PER_BUFFER);
|
||||
}
|
||||
|
||||
public static final int[][][] DIRECTION_VERTEX_QUAD = new int[][][] {
|
||||
|
||||
@@ -21,6 +21,8 @@ package com.seibel.lod.core.render.objects;
|
||||
|
||||
import org.lwjgl.opengl.GL32;
|
||||
|
||||
import com.seibel.lod.core.util.LodUtil;
|
||||
|
||||
public abstract class VertexAttribute {
|
||||
|
||||
public static final class VertexPointer {
|
||||
@@ -35,7 +37,7 @@ public abstract class VertexAttribute {
|
||||
this.byteSize = byteSize;
|
||||
}
|
||||
private static int _align(int bytes) {
|
||||
return (-Math.floorDiv(-bytes, 4))*4;
|
||||
return LodUtil.ceilDiv(bytes, 4)*4;
|
||||
}
|
||||
|
||||
public static VertexPointer addFloatPointer(boolean normalized) {
|
||||
|
||||
@@ -31,6 +31,11 @@ import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftWrapper;
|
||||
*/
|
||||
public class ColorUtil
|
||||
{
|
||||
//note: Minecraft color format is: 0xAA BB GG RR
|
||||
//________ DH mod color format is: 0xAA RR GG BB
|
||||
//OpenGL RGBA format native order: 0xRR GG BB AA
|
||||
//_ OpenGL RGBA format Java Order: 0xAA BB GG RR
|
||||
|
||||
private static final IMinecraftWrapper MC = SingletonHandler.get(IMinecraftWrapper.class);
|
||||
|
||||
public static int rgbToInt(int red, int green, int blue)
|
||||
@@ -46,7 +51,7 @@ public class ColorUtil
|
||||
/** Returns a value between 0 and 255 */
|
||||
public static int getAlpha(int color)
|
||||
{
|
||||
return (color >> 24) & 0xFF;
|
||||
return (color >>> 24) & 0xFF;
|
||||
}
|
||||
|
||||
/** Returns a value between 0 and 255 */
|
||||
@@ -91,7 +96,7 @@ public class ColorUtil
|
||||
int green = ColorUtil.getGreen(lightColor);
|
||||
int blue = ColorUtil.getBlue(lightColor);
|
||||
|
||||
return ColorUtil.multiplyRGBcolors(color, ColorUtil.rgbToInt(red, green, blue));
|
||||
return ColorUtil.multiplyARGBwithRGB(color, ColorUtil.rgbToInt(red, green, blue));
|
||||
}
|
||||
|
||||
/** Edit the given color as an HSV (Hue Saturation Value) color */
|
||||
@@ -104,18 +109,24 @@ public class ColorUtil
|
||||
LodUtil.clamp(0.0f, hsv[2] * brightnessMultiplier, 1.0f)).getRGB();
|
||||
}
|
||||
|
||||
/** Multiply ARGB with RGB colors */
|
||||
public static int multiplyARGBwithRGB(int argb, int rgb)
|
||||
{
|
||||
return ((getAlpha(argb) << 24) | ((getRed(argb) * getRed(rgb) / 255) << 16)
|
||||
| ((getGreen(argb) * getGreen(rgb) / 255) << 8) | (getBlue(argb) * getBlue(rgb) / 255));
|
||||
}
|
||||
|
||||
/** Multiply 2 RGB colors */
|
||||
public static int multiplyRGBcolors(int color1, int color2)
|
||||
public static int multiplyARGBwithARGB(int color1, int color2)
|
||||
{
|
||||
return ((getAlpha(color1) * getAlpha(color2) / 255) << 24) | ((getRed(color1) * getRed(color2) / 255) << 16) | ((getGreen(color1) * getGreen(color2) / 255) << 8) | (getBlue(color1) * getBlue(color2) / 255);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
public static String toString(int color)
|
||||
{
|
||||
return Integer.toHexString(getAlpha(color)) + " " +
|
||||
Integer.toHexString(getRed(color)) + " " +
|
||||
Integer.toHexString(getGreen(color)) + " " +
|
||||
return "A:"+Integer.toHexString(getAlpha(color)) + ",R:" +
|
||||
Integer.toHexString(getRed(color)) + ",G:" +
|
||||
Integer.toHexString(getGreen(color)) + ",B:" +
|
||||
Integer.toHexString(getBlue(color));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +293,13 @@ public class LodUtil
|
||||
{
|
||||
return Math.min(max, Math.max(value, min));
|
||||
}
|
||||
|
||||
/**
|
||||
* Like Math.floorDiv, but reverse in that it is a ceilDiv
|
||||
*/
|
||||
public static int ceilDiv(int value, int divider) {
|
||||
return -Math.floorDiv(-value, divider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a HashSet of all ChunkPos within the normal render distance
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.seibel.lod.core.wrapperInterfaces.block;
|
||||
import com.seibel.lod.core.enums.config.BlocksToAvoid;
|
||||
import com.seibel.lod.core.util.ColorUtil;
|
||||
|
||||
public class BlockDetail
|
||||
public final class BlockDetail
|
||||
{
|
||||
public final int color;
|
||||
public final boolean isFullBlock;
|
||||
|
||||
Reference in New Issue
Block a user