mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-18 19:58:00 +00:00
wavefunction collapse first integration
This commit is contained in:
@@ -55,6 +55,11 @@
|
||||
<artifactId>gdx-freetype</artifactId>
|
||||
<version>1.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.sjcasey21</groupId>
|
||||
<artifactId>wavefunctioncollapse</artifactId>
|
||||
<version>0.2.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -123,6 +123,7 @@ public class Forge implements ApplicationListener {
|
||||
private static Cursor cursor0, cursor1, cursor2, cursorA0, cursorA1, cursorA2;
|
||||
public static boolean forcedEnglishonCJKMissing = false;
|
||||
public static boolean adventureLoaded = false;
|
||||
public static boolean createNewAdventureMap = false;
|
||||
private static Localizer localizer;
|
||||
|
||||
public static ApplicationListener getApp(Clipboard clipboard0, IDeviceAdapter deviceAdapter0, String assetDir0, boolean value, boolean androidOrientation, int totalRAM, boolean isTablet, int AndroidAPI, String AndroidRelease, String deviceName) {
|
||||
|
||||
@@ -30,6 +30,7 @@ public class BiomeData implements Serializable {
|
||||
public String[] spriteNames;
|
||||
public List<String> enemies;
|
||||
public List<String> pointsOfInterest;
|
||||
public BiomeStructureData[] structures;
|
||||
|
||||
private ArrayList<EnemyData> enemyList;
|
||||
private ArrayList<PointOfInterestData> pointOfInterestList;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package forge.adventure.data;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public class BiomeStructureData {
|
||||
public int N = 3;
|
||||
public float x;
|
||||
public float y;
|
||||
public float size;
|
||||
public boolean randomPosition;
|
||||
public boolean collision;
|
||||
|
||||
public String structureAtlasPath;
|
||||
public boolean periodicInput;
|
||||
public float height;
|
||||
public float width;
|
||||
public int ground;
|
||||
public int symmetry;
|
||||
public boolean periodicOutput;
|
||||
|
||||
public BiomeStructureData( )
|
||||
{
|
||||
|
||||
}
|
||||
public BiomeStructureData(BiomeStructureData biomeStructureData) {
|
||||
this.structureAtlasPath=biomeStructureData.structureAtlasPath;
|
||||
this.x=biomeStructureData.x;
|
||||
this.y=biomeStructureData.y;
|
||||
this.size=biomeStructureData.size;
|
||||
this.randomPosition=biomeStructureData.randomPosition;
|
||||
this.collision=biomeStructureData.collision;
|
||||
}
|
||||
|
||||
public BufferedImage sourceImage() {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -228,6 +228,11 @@ public class NewGameScene extends UIScene {
|
||||
public void enter() {
|
||||
updateAvatar();
|
||||
Gdx.input.setInputProcessor(stage); //Start taking input from the ui
|
||||
|
||||
if(Forge.createNewAdventureMap)
|
||||
{
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -7,9 +7,11 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
|
||||
import com.badlogic.gdx.utils.Align;
|
||||
import forge.Forge;
|
||||
import forge.adventure.stage.GameHUD;
|
||||
import forge.adventure.stage.GameStage;
|
||||
import forge.adventure.stage.MapStage;
|
||||
import forge.adventure.util.Config;
|
||||
import forge.adventure.util.Controls;
|
||||
import forge.adventure.util.Current;
|
||||
import forge.adventure.world.WorldSave;
|
||||
import forge.screens.TransitionScreen;
|
||||
|
||||
@@ -104,6 +106,13 @@ public class StartScene extends UIScene {
|
||||
}
|
||||
|
||||
Gdx.input.setInputProcessor(stage); //Start taking input from the ui
|
||||
|
||||
if(Forge.createNewAdventureMap)
|
||||
{
|
||||
this.NewGame();
|
||||
Current.setDebug(true);
|
||||
GameStage.maximumScrollDistance=4f;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -36,6 +36,8 @@ public abstract class GameStage extends Stage {
|
||||
private float touchY = -1;
|
||||
private final float timer = 0;
|
||||
private float animationTimeout = 0;
|
||||
public static float maximumScrollDistance=1.5f;
|
||||
public static float minimumScrollDistance=0.3f;
|
||||
|
||||
public void startPause(float i) {
|
||||
startPause(i, null);
|
||||
@@ -222,10 +224,10 @@ public abstract class GameStage extends Stage {
|
||||
if (isPaused())
|
||||
return true;
|
||||
camera.zoom += (amountY * 0.03);
|
||||
if (camera.zoom < 0.3f)
|
||||
camera.zoom = 0.3f;
|
||||
if (camera.zoom > 1.5f)
|
||||
camera.zoom = 1.5f;
|
||||
if (camera.zoom < minimumScrollDistance)
|
||||
camera.zoom = minimumScrollDistance;
|
||||
if (camera.zoom > maximumScrollDistance)
|
||||
camera.zoom = maximumScrollDistance;
|
||||
return super.scrolled(amountX, amountY);
|
||||
}
|
||||
|
||||
|
||||
@@ -176,19 +176,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
|
||||
public boolean isColliding(Rectangle boundingRect)
|
||||
{
|
||||
|
||||
World world = WorldSave.getCurrentSave().getWorld();
|
||||
int currentBiome = World.highestBiome(world.getBiome((int) boundingRect.getX() / world.getTileSize(), (int) boundingRect.getY() / world.getTileSize()));
|
||||
if(currentBiome==0)
|
||||
return true;
|
||||
currentBiome = World.highestBiome(world.getBiome((int) (boundingRect.getX()+boundingRect.getWidth()) / world.getTileSize(), (int) boundingRect.getY() / world.getTileSize()));
|
||||
if(currentBiome==0)
|
||||
return true;
|
||||
currentBiome = World.highestBiome(world.getBiome((int) (boundingRect.getX()+boundingRect.getWidth())/ world.getTileSize(), (int) (boundingRect.getY()+boundingRect.getHeight()) / world.getTileSize()));
|
||||
if(currentBiome==0)
|
||||
return true;
|
||||
currentBiome = World.highestBiome(world.getBiome((int) boundingRect.getX() / world.getTileSize(), (int) (boundingRect.getY()+boundingRect.getHeight()) / world.getTileSize()));
|
||||
|
||||
return (currentBiome==0);
|
||||
return WorldSave.getCurrentSave().getWorld().collidingTile(boundingRect);
|
||||
}
|
||||
|
||||
private void HandleMonsterSpawn(float delta) {
|
||||
|
||||
114
forge-gui-mobile/src/forge/adventure/world/BiomeStructure.java
Normal file
114
forge-gui-mobile/src/forge/adventure/world/BiomeStructure.java
Normal file
@@ -0,0 +1,114 @@
|
||||
package forge.adventure.world;
|
||||
|
||||
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
|
||||
import com.github.sjcasey21.wavefunctioncollapse.OverlappingModel;
|
||||
import forge.adventure.data.BiomeStructureData;
|
||||
import forge.adventure.util.Config;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class BiomeStructure {
|
||||
|
||||
private BiomeStructureData data;
|
||||
long seed;
|
||||
private int biomeWidth;
|
||||
private int biomeHeight;
|
||||
private int dataMap[][];
|
||||
boolean init=false;
|
||||
private TextureAtlas structureAtlas;
|
||||
public BiomeStructure(BiomeStructureData data,long seed,int width,int height)
|
||||
{
|
||||
this.data=data;
|
||||
this.seed=seed;
|
||||
this.biomeWidth = width;
|
||||
this.biomeHeight = height;
|
||||
}
|
||||
public TextureAtlas atlas() {
|
||||
if(structureAtlas==null)
|
||||
{
|
||||
structureAtlas = Config.instance().getAtlas(data.structureAtlasPath);
|
||||
}
|
||||
return structureAtlas;
|
||||
}
|
||||
public int structureObjectCount() {
|
||||
int count=0;
|
||||
for(TextureAtlas.AtlasRegion region:atlas ().getRegions())
|
||||
{
|
||||
if(region.name.startsWith("structure"))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public int objectID(int x, int y) {
|
||||
|
||||
if(!init)
|
||||
{
|
||||
init=true;
|
||||
initialize();
|
||||
}
|
||||
if(x>biomeWidth*data.width)
|
||||
return -1;
|
||||
if(y>biomeHeight*data.height)
|
||||
return -1;
|
||||
if(x<biomeWidth*data.x)
|
||||
return -1;
|
||||
if(y<biomeHeight*data.y)
|
||||
return -1;
|
||||
return dataMap[x][y];
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
OverlappingModel model= new OverlappingModel(sourceImage(),data.N, (int) (data.width* biomeWidth), (int) (data.height*biomeHeight),data.periodicInput,data.periodicOutput,data.symmetry,data.ground);
|
||||
HashMap<Integer,Integer> colorIdMap=new HashMap<>();
|
||||
int counter=0;
|
||||
for(TextureAtlas.AtlasRegion region:atlas ().getRegions())
|
||||
{
|
||||
if(region.name.startsWith("structure"))
|
||||
{
|
||||
String[] split= region.name.split("_");
|
||||
if(split.length<2)
|
||||
continue;
|
||||
int rgb=Integer.parseInt(split[1],16);
|
||||
colorIdMap.put(rgb,counter);
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
BufferedImage image=model.graphics();
|
||||
dataMap=new int[image.getWidth()][image.getHeight()];
|
||||
for(int x=0;x<image.getWidth();x++)
|
||||
{
|
||||
|
||||
for(int y=0;y<image.getHeight();y++)
|
||||
{
|
||||
int rgb=image.getRGB(x,y);
|
||||
if(!colorIdMap.containsKey(rgb))
|
||||
{
|
||||
dataMap[x][y]=-1;
|
||||
}
|
||||
else {
|
||||
dataMap[x][y]=colorIdMap.get(rgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private BufferedImage sourceImage() {
|
||||
TextureAtlas.AtlasRegion region=atlas().findRegion("Source");
|
||||
if(region==null)
|
||||
return null;
|
||||
try {
|
||||
return ImageIO.read(new File(Config.instance().getFilePath(data.structureAtlasPath))).getSubimage((int) region.offsetX, (int) region.offsetY,region.originalWidth,region.originalHeight);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.badlogic.gdx.graphics.Pixmap;
|
||||
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
|
||||
import com.badlogic.gdx.utils.IntMap;
|
||||
import forge.adventure.data.BiomeData;
|
||||
import forge.adventure.data.BiomeStructureData;
|
||||
import forge.adventure.data.BiomeTerrainData;
|
||||
import forge.adventure.util.Config;
|
||||
import forge.gui.FThreads;
|
||||
@@ -19,7 +20,7 @@ import java.util.ArrayList;
|
||||
public class BiomeTexture implements Serializable {
|
||||
private final BiomeData data;
|
||||
private final int tileSize;
|
||||
public Pixmap emptyPixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
|
||||
public static Pixmap emptyPixmap = null;
|
||||
ArrayList<ArrayList<Pixmap>> images = new ArrayList<>();
|
||||
ArrayList<ArrayList<Pixmap>> smallImages = new ArrayList<>();
|
||||
ArrayList<IntMap<Pixmap>> edgeImages = new ArrayList<>();
|
||||
@@ -45,7 +46,6 @@ public class BiomeTexture implements Serializable {
|
||||
FThreads.invokeInEdtNowOrLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Pixmap completePicture = null;
|
||||
|
||||
if (images != null) {
|
||||
for (ArrayList<Pixmap> val : images) {
|
||||
@@ -79,22 +79,38 @@ public class BiomeTexture implements Serializable {
|
||||
edgeImages = new ArrayList<>();
|
||||
|
||||
ArrayList<TextureAtlas.AtlasRegion> regions =new ArrayList<>();
|
||||
ArrayList<TextureAtlas> source =new ArrayList<>();
|
||||
regions.add(Config.instance().getAtlas(data.tilesetAtlas).findRegion(data.tilesetName));
|
||||
source.add(Config.instance().getAtlas(data.tilesetAtlas));
|
||||
if(data.terrain!=null)
|
||||
{
|
||||
for(BiomeTerrainData terrain:data.terrain)
|
||||
{
|
||||
regions.add(Config.instance().getAtlas(data.tilesetAtlas).findRegion(terrain.spriteName));
|
||||
source.add(Config.instance().getAtlas(data.tilesetAtlas));
|
||||
}
|
||||
}
|
||||
if(data.structures!=null)
|
||||
{
|
||||
for(BiomeStructureData structureData:data.structures)
|
||||
{
|
||||
BiomeStructure structure=new BiomeStructure(structureData,0,0,0);
|
||||
for(TextureAtlas.AtlasRegion region:structure.atlas ().getRegions())
|
||||
{
|
||||
if(region.name.startsWith("structure"))
|
||||
{
|
||||
regions.add(region);
|
||||
source.add(structure.atlas());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (TextureAtlas.AtlasRegion region : regions) {
|
||||
ArrayList<Pixmap> pics = new ArrayList<>();
|
||||
ArrayList<Pixmap> spics = new ArrayList<>();
|
||||
if (completePicture == null) {
|
||||
region.getTexture().getTextureData().prepare();
|
||||
completePicture = region.getTexture().getTextureData().consumePixmap();
|
||||
}
|
||||
|
||||
Pixmap completePicture = region.getTexture().getTextureData().consumePixmap();
|
||||
for (int y = 0; y < 4; y++) {
|
||||
for (int x = 0; x < 3; x++) {
|
||||
int px = region.getRegionX() + (x * tileSize);
|
||||
@@ -117,6 +133,7 @@ public class BiomeTexture implements Serializable {
|
||||
smallImages.add(spics);
|
||||
edgeImages.add(new IntMap<>());
|
||||
|
||||
completePicture.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -124,6 +141,8 @@ public class BiomeTexture implements Serializable {
|
||||
|
||||
public Pixmap getPixmap(int biomeSubIndex) {
|
||||
if (biomeSubIndex >= edgeImages.size() || biomeSubIndex < 0) {
|
||||
if(emptyPixmap==null)
|
||||
emptyPixmap=new Pixmap(1, 1, Pixmap.Format.RGBA8888);
|
||||
return emptyPixmap;
|
||||
}
|
||||
return images.get(biomeSubIndex).get(BigPictures.Center.value);
|
||||
|
||||
@@ -9,11 +9,7 @@ import com.badlogic.gdx.math.Vector2;
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor;
|
||||
import com.badlogic.gdx.utils.Disposable;
|
||||
import com.badlogic.gdx.utils.Json;
|
||||
import forge.adventure.data.BiomeData;
|
||||
import forge.adventure.data.BiomeSpriteData;
|
||||
import forge.adventure.data.BiomeTerrainData;
|
||||
import forge.adventure.data.PointOfInterestData;
|
||||
import forge.adventure.data.WorldData;
|
||||
import forge.adventure.data.*;
|
||||
import forge.adventure.pointofintrest.PointOfInterest;
|
||||
import forge.adventure.pointofintrest.PointOfInterestMap;
|
||||
import forge.adventure.scene.Scene;
|
||||
@@ -24,10 +20,7 @@ import forge.adventure.util.SaveFileContent;
|
||||
import forge.adventure.util.SaveFileData;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class that will create the world from the configuration
|
||||
@@ -55,6 +48,26 @@ public class World implements Disposable, SaveFileContent {
|
||||
return (int) (Math.log(Long.highestOneBit(biome)) / Math.log(2));
|
||||
}
|
||||
|
||||
public boolean collidingTile(Rectangle boundingRect)
|
||||
{
|
||||
Set<Pair<Integer,Integer>> points=new HashSet<>();
|
||||
|
||||
int xLeft=(int) boundingRect.getX() / getTileSize();
|
||||
int yTop=(int) boundingRect.getY() / getTileSize();
|
||||
int xRight=(int) (boundingRect.getX()+boundingRect.getWidth()) / getTileSize();
|
||||
int yBottom= (int) (boundingRect.getY()+boundingRect.getHeight()) / getTileSize();
|
||||
|
||||
if(getBiome(xLeft,yTop)==0)
|
||||
return true;
|
||||
if(getBiome(xLeft,yBottom)==0)
|
||||
return true;
|
||||
if(getBiome(xRight,yBottom)==0)
|
||||
return true;
|
||||
if(getBiome(xRight,yTop)==0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
public void loadWorldData() {
|
||||
if(worldDataLoaded)
|
||||
return;
|
||||
@@ -85,6 +98,9 @@ public class World implements Disposable, SaveFileContent {
|
||||
biomeImage=saveFileData.readPixmap("biomeImage");
|
||||
biomeMap=(long[][])saveFileData.readObject("biomeMap");
|
||||
terrainMap=(int[][])saveFileData.readObject("terrainMap");
|
||||
|
||||
|
||||
|
||||
width=saveFileData.readInt("width");
|
||||
height=saveFileData.readInt("height");
|
||||
mapObjectIds = new SpritesDataMap(getChunkSize(), this.data.tileSize, this.data.width / getChunkSize());
|
||||
@@ -269,6 +285,7 @@ public class World implements Disposable, SaveFileContent {
|
||||
endX = width;
|
||||
endY = height;
|
||||
}
|
||||
HashMap<BiomeStructureData,BiomeStructure> structureDataMap=new HashMap<>();
|
||||
for (int x = beginX; x < endX; x++) {
|
||||
for (int y = beginY; y < endY; y++) {
|
||||
//value 0-1 based on noise
|
||||
@@ -288,16 +305,34 @@ public class World implements Disposable, SaveFileContent {
|
||||
pix.drawPixel(x, y);
|
||||
biomeMap[x][y] |= (1L << biomeIndex);
|
||||
int terrainCounter=1;
|
||||
if(biome.terrain==null)
|
||||
continue;
|
||||
for(BiomeTerrainData terrain:biome.terrain)
|
||||
if(biome.terrain!=null)
|
||||
{
|
||||
float terrainNoise = ((float)noise.eval(x / (float) width * (noiseZoom*terrain.resolution), y / (float) height * (noiseZoom*terrain.resolution)) + 1) / 2;
|
||||
if(terrainNoise>=terrain.min&&terrainNoise<=terrain.max)
|
||||
for(BiomeTerrainData terrain:biome.terrain)
|
||||
{
|
||||
terrainMap[x][y]=terrainCounter;
|
||||
float terrainNoise = ((float)noise.eval(x / (float) width * (noiseZoom*terrain.resolution), y / (float) height * (noiseZoom*terrain.resolution)) + 1) / 2;
|
||||
if(terrainNoise>=terrain.min&&terrainNoise<=terrain.max)
|
||||
{
|
||||
terrainMap[x][y]=terrainCounter;
|
||||
}
|
||||
terrainCounter++;
|
||||
}
|
||||
}
|
||||
if(biome.structures!=null)
|
||||
{
|
||||
for(BiomeStructureData data:biome.structures)
|
||||
{
|
||||
BiomeStructure structure;
|
||||
if(!structureDataMap.containsKey(data))
|
||||
{
|
||||
structureDataMap.put(data,new BiomeStructure(data,seed,biomeWidth,biomeHeight));
|
||||
}
|
||||
structure=structureDataMap.get(data);
|
||||
int structureIndex=structure.objectID(x-biomeXStart,y-biomeYStart);
|
||||
if(structureIndex>=0)
|
||||
terrainMap[x][y]=terrainCounter+structureIndex;
|
||||
|
||||
terrainCounter+=structure.structureObjectCount();
|
||||
}
|
||||
terrainCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +467,9 @@ public class World implements Disposable, SaveFileContent {
|
||||
for (int y = (int) currentPoint.y - 1; y < currentPoint.y + 2; y++) {
|
||||
if(x<0||y<=0||x>=width||y>height)continue;
|
||||
biomeMap[x][height - y] |= (1L << biomeIndex);
|
||||
terrainMap[x][height-y]=0;
|
||||
|
||||
|
||||
pix.drawPixel(x, height-y);
|
||||
}
|
||||
}
|
||||
@@ -465,7 +503,9 @@ public class World implements Disposable, SaveFileContent {
|
||||
|
||||
if( (int)currentPoint.x<0|| (int)currentPoint.y<=0|| (int)currentPoint.x>=width|| (int)currentPoint.y>height)continue;
|
||||
biomeMap[(int) currentPoint.x][height - (int) currentPoint.y] |= (1L << biomeIndex);
|
||||
terrainMap[(int) currentPoint.x][height - (int) currentPoint.y]=0;
|
||||
pix.drawPixel((int) currentPoint.x, height - (int) currentPoint.y);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -482,6 +522,8 @@ public class World implements Disposable, SaveFileContent {
|
||||
BiomeSpriteData sprite = data.GetBiomeSprites().getSpriteData(name);
|
||||
double spriteNoise = (noise.eval(x / (double) width * noiseZoom*sprite.resolution, y / (double) invertedHeight * noiseZoom*sprite.resolution) + 1) / 2;
|
||||
if (spriteNoise >= sprite.startArea && spriteNoise <= sprite.endArea) {
|
||||
if(terrainMap[x][invertedHeight]>biome.terrain.length)
|
||||
continue;
|
||||
if (random.nextFloat() <= sprite.density) {
|
||||
String spriteKey = sprite.key();
|
||||
int key;
|
||||
|
||||
@@ -58,6 +58,8 @@ public class WorldSave {
|
||||
static public boolean load(int currentSlot) {
|
||||
|
||||
String fileName = WorldSave.getSaveFile(currentSlot);
|
||||
if(!new File(fileName).exists())
|
||||
return false;
|
||||
new File(getSaveDir()).mkdirs();
|
||||
try {
|
||||
try(FileInputStream fos = new FileInputStream(fileName);
|
||||
|
||||
@@ -265,6 +265,14 @@ public class SplashScreen extends FContainer {
|
||||
add(btnHome);
|
||||
btnAdventure.setBounds(btn_x, btn_y + height + padding / 2, btn_w, height);
|
||||
add(btnAdventure);
|
||||
|
||||
if(Forge.createNewAdventureMap)
|
||||
{
|
||||
bgAnimation.progress = 1;
|
||||
bgAnimation.openAdventure = true;
|
||||
Forge.openAdventure();
|
||||
Forge.clearSplashScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user