mirror of
https://github.com/Anuken/Mindustry.git
synced 2026-09-22 13:01:12 +03:00
WIP texture array sprite batch + gl3 min
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-feature android:glEsVersion="0x00020000" android:required="true"/>
|
||||
<uses-feature android:glEsVersion="0x00030000" android:required="true"/>
|
||||
<uses-feature android:name="android.hardware.type.pc" android:required="false" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 287 B |
@@ -1,9 +0,0 @@
|
||||
{
|
||||
duplicatePadding: true,
|
||||
combineSubdirectories: true,
|
||||
flattenPaths: true,
|
||||
maxWidth: 2048,
|
||||
maxHeight: 2048,
|
||||
fast: true,
|
||||
stripWhitespaceCenter: false
|
||||
}
|
||||
@@ -6,5 +6,5 @@
|
||||
maxHeight: 4096,
|
||||
fast: true,
|
||||
stripWhitespaceCenter: true,
|
||||
ignoredWhitespaceStrings: ["effects/"]
|
||||
ignoredWhitespaceStrings: ["effects/", "environment/"]
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
duplicatePadding: true,
|
||||
combineSubdirectories: true,
|
||||
flattenPaths: true,
|
||||
maxWidth: 4096,
|
||||
maxHeight: 4096,
|
||||
fast: true,
|
||||
stripWhitespaceCenter: true
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
duplicatePadding: true,
|
||||
combineSubdirectories: true,
|
||||
flattenPaths: true,
|
||||
maxWidth: 2048,
|
||||
maxHeight: 2048,
|
||||
fast: true,
|
||||
stripWhitespaceCenter: true
|
||||
}
|
||||
@@ -61,15 +61,6 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
//debug GL information
|
||||
Log.info("[GL] Version: @", graphics.getGLVersion());
|
||||
Log.info("[GL] Max texture size: @", maxTextureSize);
|
||||
Log.info("[GL] Using @ API.", gl30 != null ? "OpenGL 3" : "OpenGL 2");
|
||||
|
||||
IntelGpuCheck.init(graphics.getGLVersion().vendorString);
|
||||
|
||||
boolean isIntel = IntelGpuCheck.wasIntel();
|
||||
|
||||
if(isIntel && !graphics.isGL30Available()) Log.warn("[GL] Intel GPU detected on previous launch. Due to memory corruption issues, OpenGL 3 support has been disabled for Intel GPUs. See issue #11041.");
|
||||
|
||||
if(gl30 == null && !isIntel) Log.warn("[GL] Your device or video drivers do not support OpenGL 3. This will cause performance issues.");
|
||||
|
||||
if(NvGpuInfo.hasMemoryInfo()) Log.info("[GL] Total available VRAM: @mb", NvGpuInfo.getMaxMemoryKB()/1024);
|
||||
|
||||
@@ -161,7 +152,10 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
Fonts.loadDefaultFont();
|
||||
|
||||
//load fallback atlas if max texture size is below 4096
|
||||
assets.load(new AssetDescriptor<>(maxTextureSize >= 4096 ? "sprites/sprites.aatls" : "sprites/fallback/sprites.aatls", TextureAtlas.class)).loaded = t -> atlas = t;
|
||||
assets.load(new AssetDescriptor<>("sprites/sprites.aatls", TextureAtlas.class)).loaded = t -> {
|
||||
atlas = t;
|
||||
Fonts.mergeFontAtlas(atlas);
|
||||
};
|
||||
assets.loadRun("maps", Map.class, () -> maps.loadPreviews());
|
||||
|
||||
Musics.load();
|
||||
@@ -176,7 +170,6 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
|
||||
});
|
||||
|
||||
assets.load(mods);
|
||||
assets.loadRun("mergeUI", PixmapPacker.class, () -> {}, () -> Fonts.mergeFontAtlas(atlas));
|
||||
|
||||
add(logic = new Logic());
|
||||
add(control = new Control());
|
||||
|
||||
@@ -42,7 +42,7 @@ public class EditorSpriteCache implements Disposable{
|
||||
* @param indices The shared index data in standard quad format, as seen in SpriteBatch.
|
||||
* Since this data is static, it should be the same across all caches, and be large enough to accommodate all sprites.
|
||||
* */
|
||||
public void build(IndexData indices){
|
||||
public void build(IndexBufferObject indices){
|
||||
if(mesh != null) mesh.dispose();
|
||||
|
||||
mesh = new Mesh(true, index / vertexSize, 0,
|
||||
|
||||
@@ -60,8 +60,8 @@ public class CubemapMesh implements Disposable{
|
||||
this.map = map;
|
||||
this.map.setFilter(TextureFilter.linear);
|
||||
this.mesh = new Mesh(true, vertices.length, 0, VertexAttribute.position3);
|
||||
mesh.getVerticesBuffer().limit(vertices.length);
|
||||
mesh.getVerticesBuffer().put(vertices, 0, vertices.length);
|
||||
mesh.getVertices().limit(vertices.length);
|
||||
mesh.getVertices().put(vertices, 0, vertices.length);
|
||||
|
||||
shader = new Shader(Core.files.internal("shaders/cubemap.vert"), Core.files.internal("shaders/cubemap.frag"));
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@ import static mindustry.Vars.*;
|
||||
public class FloorRenderer{
|
||||
public static boolean growSprites = true;
|
||||
|
||||
private static final VertexAttribute[] attributes = {VertexAttribute.packedPosition, VertexAttribute.color, VertexAttribute.packedTexCoords};
|
||||
private static final VertexAttribute[] attributes = {VertexAttribute.packedPosition, VertexAttribute.color, VertexAttribute.packedTexCoords, VertexAttribute.depthCoords};
|
||||
private static final int
|
||||
chunksize = 30, //todo 32?
|
||||
chunkunits = chunksize * tilesize,
|
||||
vertexSize = 1 + 1 + 1,
|
||||
vertexSize = 1 + 1 + 1 + 1,
|
||||
spriteSize = vertexSize * 4,
|
||||
maxSprites = chunksize * chunksize * 9;
|
||||
private static final float packPad = tilesize * 8f;
|
||||
@@ -73,25 +73,29 @@ public class FloorRenderer{
|
||||
attribute vec4 a_position;
|
||||
attribute vec4 a_color;
|
||||
attribute vec2 a_texCoord0;
|
||||
attribute float a_depth;
|
||||
|
||||
uniform mat4 u_projectionViewMatrix;
|
||||
varying vec4 v_color;
|
||||
varying vec2 v_texCoords;
|
||||
varying float v_depth;
|
||||
|
||||
void main(){
|
||||
v_color = a_color;
|
||||
v_color.a = v_color.a * (255.0/254.0);
|
||||
v_texCoords = a_texCoord0;
|
||||
v_depth = a_depth;
|
||||
gl_Position = u_projectionViewMatrix * a_position;
|
||||
}
|
||||
""",
|
||||
"""
|
||||
varying vec4 v_color;
|
||||
varying vec2 v_texCoords;
|
||||
uniform sampler2D u_texture;
|
||||
varying float v_depth;
|
||||
uniform sampler2DArray u_texture;
|
||||
|
||||
void main(){
|
||||
gl_FragColor = v_color * texture2D(u_texture, v_texCoords);
|
||||
gl_FragColor = v_color * texture(u_texture, vec3(v_texCoords, v_depth));
|
||||
}
|
||||
""");
|
||||
|
||||
@@ -456,24 +460,29 @@ public class FloorRenderer{
|
||||
float v = region.v2;
|
||||
float u2 = region.u2;
|
||||
float v2 = region.v;
|
||||
float depth = region.getDepth();
|
||||
|
||||
float color = this.colorPacked;
|
||||
|
||||
verts[idx] = pack(x1, y1);
|
||||
verts[idx + 1] = color;
|
||||
verts[idx + 2] = Pack.packUv(u, v);
|
||||
verts[idx] = pack(x1, y1);
|
||||
verts[idx + 1] = color;
|
||||
verts[idx + 2] = Pack.packUv(u, v);
|
||||
verts[idx + 3] = depth;
|
||||
|
||||
verts[idx + 3] = pack(x2, y2);
|
||||
verts[idx + 4] = color;
|
||||
verts[idx + 5] = Pack.packUv(u, v2);
|
||||
verts[idx + 4] = pack(x2, y2);
|
||||
verts[idx + 5] = color;
|
||||
verts[idx + 6] = Pack.packUv(u, v2);
|
||||
verts[idx + 7] = depth;
|
||||
|
||||
verts[idx + 6] = pack(x3, y3);
|
||||
verts[idx + 7] = color;
|
||||
verts[idx + 8] = Pack.packUv(u2, v2);
|
||||
verts[idx + 8] = pack(x3, y3);
|
||||
verts[idx + 9] = color;
|
||||
verts[idx + 10] = Pack.packUv(u2, v2);
|
||||
verts[idx + 11] = depth;
|
||||
|
||||
verts[idx + 9] = pack(x4, y4);
|
||||
verts[idx + 10] = color;
|
||||
verts[idx + 11] = Pack.packUv(u2, v);
|
||||
verts[idx + 12] = pack(x4, y4);
|
||||
verts[idx + 13] = color;
|
||||
verts[idx + 14] = Pack.packUv(u2, v);
|
||||
verts[idx + 15] = depth;
|
||||
}else{
|
||||
float fx2 = x + width;
|
||||
float fy2 = y + height;
|
||||
@@ -481,24 +490,29 @@ public class FloorRenderer{
|
||||
float v = region.v2;
|
||||
float u2 = region.u2;
|
||||
float v2 = region.v;
|
||||
float depth = region.getDepth();
|
||||
|
||||
float color = this.colorPacked;
|
||||
|
||||
verts[idx] = pack(x, y);
|
||||
verts[idx + 1] = color;
|
||||
verts[idx + 2] = Pack.packUv(u, v);
|
||||
verts[idx] = pack(x, y);
|
||||
verts[idx + 1] = color;
|
||||
verts[idx + 2] = Pack.packUv(u, v);
|
||||
verts[idx + 3] = depth;
|
||||
|
||||
verts[idx + 3] = pack(x, fy2);
|
||||
verts[idx + 4] = color;
|
||||
verts[idx + 5] = Pack.packUv(u, v2);
|
||||
verts[idx + 4] = pack(x, fy2);
|
||||
verts[idx + 5] = color;
|
||||
verts[idx + 6] = Pack.packUv(u, v2);
|
||||
verts[idx + 7] = depth;
|
||||
|
||||
verts[idx + 6] = pack(fx2, fy2);
|
||||
verts[idx + 7] = color;
|
||||
verts[idx + 8] = Pack.packUv(u2, v2);
|
||||
verts[idx + 8] = pack(fx2, fy2);
|
||||
verts[idx + 9] = color;
|
||||
verts[idx + 10] = Pack.packUv(u2, v2);
|
||||
verts[idx + 11] = depth;
|
||||
|
||||
verts[idx + 9] = pack(fx2, y);
|
||||
verts[idx + 10] = color;
|
||||
verts[idx + 11] = Pack.packUv(u2, v);
|
||||
verts[idx + 12] = pack(fx2, y);
|
||||
verts[idx + 13] = color;
|
||||
verts[idx + 14] = Pack.packUv(u2, v);
|
||||
verts[idx + 15] = depth;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -519,6 +533,7 @@ public class FloorRenderer{
|
||||
|
||||
@Override
|
||||
protected void draw(Texture texture, float[] spriteVertices, int offset, int count){
|
||||
//TODO broken
|
||||
if(spriteVertices.length != 20){
|
||||
throw new IllegalArgumentException("cached vertices must be in non-mixcolor format (20 per sprite, 5 per vertex)");
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package mindustry.graphics;
|
||||
|
||||
import arc.files.*;
|
||||
import arc.util.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class IntelGpuCheck{
|
||||
private static boolean wasIntel, checkedLastLaunch;
|
||||
|
||||
/** initialize intel version check for the next application launch */
|
||||
public static void init(String vendor){
|
||||
if(!OS.isWindows) return;
|
||||
|
||||
boolean isIntel = vendor.toLowerCase(Locale.ROOT).contains("intel");
|
||||
try{
|
||||
Fi file = new Fi(OS.getAppDataDirectoryString("Mindustry")).child("was_intel_gpu");
|
||||
if(isIntel){
|
||||
file.writeString("1");
|
||||
}else if(file.exists()){
|
||||
file.delete();
|
||||
}
|
||||
}catch(Throwable e){
|
||||
Log.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return whether the last launch used an intel GPU on Windows */
|
||||
public static boolean wasIntel(){
|
||||
if(!OS.isWindows) return false;
|
||||
if(checkedLastLaunch) return wasIntel;
|
||||
checkedLastLaunch = true;
|
||||
|
||||
try{
|
||||
Fi file = new Fi(OS.getAppDataDirectoryString("Mindustry")).child("was_intel_gpu");
|
||||
if(file.exists() && file.readString().equals("1")){
|
||||
return wasIntel = true;
|
||||
}
|
||||
}catch(Throwable e){
|
||||
Log.err("Failed to check whether the last launch used an intel GPU.", e);
|
||||
}
|
||||
return wasIntel = false;
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ public class ParticleRenderer{
|
||||
}
|
||||
|
||||
void uploadMeshData(Mesh mesh){
|
||||
var buffer = mesh.getVerticesBuffer();
|
||||
var buffer = mesh.getVertices();
|
||||
|
||||
buffer.position(0);
|
||||
buffer.limit(vertexBufferLength);
|
||||
|
||||
@@ -24,14 +24,14 @@ public class MeshBuilder{
|
||||
items[i] *= radius;
|
||||
}
|
||||
|
||||
mesh.getVerticesBuffer().put(items, 0, result.vertices.size);
|
||||
mesh.getVertices().put(items, 0, result.vertices.size);
|
||||
|
||||
short[] indices = new short[result.indices.size];
|
||||
for(int i = 0; i < result.indices.size; i++){
|
||||
indices[i] = (short)result.indices.items[i];
|
||||
}
|
||||
|
||||
mesh.getIndicesBuffer().put(indices);
|
||||
mesh.getIndices().put(indices);
|
||||
|
||||
return end(mesh);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ public class MeshBuilder{
|
||||
floats[6] = v2.z * scale;
|
||||
floats[7] = col;
|
||||
|
||||
mesh.getVerticesBuffer().put(floats);
|
||||
mesh.getVertices().put(floats);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ public class MeshBuilder{
|
||||
shorts[11] = (short)(position + 5);
|
||||
}
|
||||
|
||||
mesh.getIndicesBuffer().put(shorts, 0, c.length > 5 ? 12 : 9);
|
||||
mesh.getIndices().put(shorts, 0, c.length > 5 ? 12 : 9);
|
||||
|
||||
position += c.length;
|
||||
}else{
|
||||
@@ -216,21 +216,21 @@ public class MeshBuilder{
|
||||
|
||||
Mesh mesh = new Mesh(true, vertices, indices, attributes.toArray(VertexAttribute.class));
|
||||
|
||||
mesh.getVerticesBuffer().limit(mesh.getVerticesBuffer().capacity());
|
||||
mesh.getVerticesBuffer().position(0);
|
||||
mesh.getVertices().limit(mesh.getVertices().capacity());
|
||||
mesh.getVertices().position(0);
|
||||
|
||||
if(indices > 0){
|
||||
mesh.getIndicesBuffer().limit(mesh.getIndicesBuffer().capacity());
|
||||
mesh.getIndicesBuffer().position(0);
|
||||
mesh.getIndices().limit(mesh.getIndices().capacity());
|
||||
mesh.getIndices().position(0);
|
||||
}
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
private static Mesh end(Mesh mesh){
|
||||
mesh.getVerticesBuffer().limit(mesh.getVerticesBuffer().position());
|
||||
mesh.getVertices().limit(mesh.getVertices().position());
|
||||
if(mesh.getNumIndices() > 0){
|
||||
mesh.getIndicesBuffer().limit(mesh.getIndicesBuffer().position());
|
||||
mesh.getIndices().limit(mesh.getIndices().position());
|
||||
}
|
||||
|
||||
return mesh;
|
||||
@@ -282,7 +282,7 @@ public class MeshBuilder{
|
||||
if(floats.length > 7) floats[7] = emissive;
|
||||
}
|
||||
|
||||
mesh.getVerticesBuffer().put(floats);
|
||||
mesh.getVertices().put(floats);
|
||||
}
|
||||
|
||||
private static float packNormals(float x, float y, float z){
|
||||
|
||||
@@ -12,7 +12,6 @@ import arc.graphics.Texture.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.graphics.g2d.Font.*;
|
||||
import arc.graphics.g2d.PixmapPacker.*;
|
||||
import arc.graphics.g2d.TextureAtlas.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.scene.style.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
@@ -236,9 +235,9 @@ public class Fonts{
|
||||
|
||||
/** Called from a static context for use in the loading screen.*/
|
||||
public static void loadDefaultFont(){
|
||||
int max = Gl.getInt(Gl.maxTextureSize);
|
||||
//TOOD: which size to use?
|
||||
UI.packer = new PixmapPacker(4096, 2048, 2, true);
|
||||
|
||||
UI.packer = new PixmapPacker(max >= 4096 ? 4096 : 2048, 2048, 2, true);
|
||||
Core.assets.setLoader(FreeTypeFontGenerator.class, new FreeTypeFontGeneratorLoader(Core.files::internal));
|
||||
Core.assets.setLoader(Font.class, null, new FreetypeFontLoader(Core.files::internal){
|
||||
ObjectSet<FreeTypeFontParameter> scaled = new ObjectSet<>();
|
||||
@@ -279,37 +278,14 @@ public class Fonts{
|
||||
}
|
||||
|
||||
/** Merges the UI and font atlas together for better performance. */
|
||||
//TODO: does nothing at the moment.
|
||||
public static void mergeFontAtlas(TextureAtlas atlas){
|
||||
//grab all textures from the ui page, remove all the regions assigned to it, then copy them over to UI.packer and replace the texture in this atlas.
|
||||
|
||||
//grab old UI texture and regions...
|
||||
Texture texture = atlas.find("logo").texture;
|
||||
if(true) return;
|
||||
Texture texture = atlas.find("ui-page-placeholder").texture;
|
||||
|
||||
Page page = UI.packer.getPages().first();
|
||||
|
||||
Seq<AtlasRegion> regions = atlas.getRegions().select(t -> t.texture == texture);
|
||||
for(AtlasRegion region : regions){
|
||||
//get new pack rect
|
||||
page.setDirty(false);
|
||||
Rect rect = UI.packer.pack(region.name, atlas.getPixmap(region), region.splits, region.pads);
|
||||
|
||||
//set new texture
|
||||
region.texture = UI.packer.getPages().first().getTexture();
|
||||
//set its new position
|
||||
region.set((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height);
|
||||
//add old texture
|
||||
atlas.getTextures().add(region.texture);
|
||||
//clear it
|
||||
region.pixmapRegion = null;
|
||||
}
|
||||
|
||||
//remove old texture, it will no longer be used
|
||||
atlas.getTextures().remove(texture);
|
||||
texture.dispose();
|
||||
atlas.disposePixmap(texture);
|
||||
|
||||
page.setDirty(true);
|
||||
page.updateTexture(TextureFilter.linear, TextureFilter.linear, false);
|
||||
page.texture = texture;
|
||||
}
|
||||
|
||||
public static TextureRegionDrawable getGlyph(Font font, char glyph){
|
||||
|
||||
@@ -142,7 +142,7 @@ public class EffectsDialog extends BaseDialog{
|
||||
|
||||
@Override
|
||||
protected void draw(Texture texture, float[] spriteVertices, int offset, int count){
|
||||
for(int i = offset; i < count; i += SpriteBatch.VERTEX_SIZE){
|
||||
for(int i = offset; i < count; i += SpriteBatch.vertexSize){
|
||||
max(spriteVertices[i], spriteVertices[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import mindustry.core.*;
|
||||
import mindustry.desktop.steam.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.mod.Mods.*;
|
||||
import mindustry.net.*;
|
||||
import mindustry.net.Net.*;
|
||||
@@ -57,17 +56,12 @@ public class DesktopLauncher extends ClientLauncher{
|
||||
width = 900;
|
||||
height = 700;
|
||||
|
||||
//on Windows, Intel drivers might be buggy with OpenGL 3.x, so only use 2.x. See https://github.com/Anuken/Mindustry/issues/11041
|
||||
if(IntelGpuCheck.wasIntel()){
|
||||
allowGl30 = false;
|
||||
coreProfile = false;
|
||||
glVersions = new int[][]{{2, 1}, {2, 0}};
|
||||
}else if(OS.isMac){
|
||||
if(OS.isMac){
|
||||
//MacOS supports 4.1 at most
|
||||
glVersions = new int[][]{{4, 1}, {3, 2}, {2, 1}, {2, 0}};
|
||||
glVersions = new int[][]{{4, 1}, {3, 2}};
|
||||
}else{
|
||||
//try essentially every OpenGL version
|
||||
glVersions = new int[][]{{4, 6}, {4, 5}, {4, 4}, {4, 1}, {3, 3}, {3, 2}, {3, 1}, {2, 1}, {2, 0}};
|
||||
glVersions = new int[][]{{4, 6}, {4, 5}, {4, 4}, {4, 1}, {3, 3}, {3, 2}, {3, 1}, {3, 0}};
|
||||
}
|
||||
|
||||
for(int i = 0; i < arg.length; i++){
|
||||
|
||||
@@ -26,4 +26,4 @@ org.gradle.caching=true
|
||||
org.gradle.internal.http.socketTimeout=100000
|
||||
org.gradle.internal.http.connectionTimeout=100000
|
||||
android.enableR8.fullMode=false
|
||||
archash=abb6cc1a11
|
||||
archash=f4336eefed
|
||||
|
||||
@@ -175,18 +175,6 @@ tasks.register('pack'){
|
||||
|
||||
//pack normal sprites
|
||||
TexturePacker.process(new File(rootDir, "core/assets-raw/sprites_out/").absolutePath, new File(rootDir, "core/assets/sprites/").absolutePath, "sprites.aatls")
|
||||
|
||||
println("\n\nPacking fallback 2048 sprites...\n\n")
|
||||
|
||||
//replace config file contents
|
||||
fileTree(dir: '../core/assets-raw/sprites_out/', include: "**/*.json").visit{ file ->
|
||||
if(!file.isDirectory()) file.file.text = file.file.text.replace("4096", "2048")
|
||||
}
|
||||
|
||||
//pack fallback 2048x2048 sprites - disabled when debugging
|
||||
if(!project.hasProperty("args")){
|
||||
TexturePacker.process(new File(rootDir, "core/assets-raw/sprites_out/").absolutePath, new File(rootDir, "core/assets/sprites/fallback/").absolutePath, "sprites.aatls")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user