made multiplayer possible
This commit is contained in:
parent
710838645f
commit
2799a40c58
src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris
@ -2,12 +2,15 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris;
|
||||
|
||||
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
|
||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game.TetrisGame;
|
||||
import eu.mhsl.minenet.minigames.score.FirstWinsScore;
|
||||
import eu.mhsl.minenet.minigames.instance.Dimension;
|
||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game.Tetromino;
|
||||
import eu.mhsl.minenet.minigames.score.LastWinsScore;
|
||||
import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.GameMode;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.event.player.*;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
@ -16,84 +19,36 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
class Tetris extends StatelessGame {
|
||||
private TetrisGame tetrisGame;
|
||||
|
||||
private final Map<Button, Long> lastPresses = new HashMap<>();
|
||||
|
||||
enum Button {
|
||||
W,
|
||||
A,
|
||||
S,
|
||||
D,
|
||||
mouseLeft,
|
||||
mouseRight,
|
||||
space
|
||||
}
|
||||
private final Map<Player, TetrisGame> tetrisGames = new HashMap<>();
|
||||
|
||||
public Tetris() {
|
||||
super(Dimension.THE_END.key, "Tetris", new FirstWinsScore());
|
||||
super(Dimension.THE_END.key, "Tetris", new LastWinsScore());
|
||||
this.setGenerator(new CircularPlateTerrainGenerator(30).setPlateHeight(0));
|
||||
|
||||
eventNode()
|
||||
.addListener(PlayerUseItemEvent.class, this::onPlayerInteract)
|
||||
.addListener(PlayerHandAnimationEvent.class, this::onPlayerAttack);
|
||||
.addListener(PlayerHandAnimationEvent.class, this::onPlayerAttack)
|
||||
.addListener(PlayerTickEvent.class, this::onPlayerTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
this.tetrisGame.start();
|
||||
}
|
||||
|
||||
protected void pressedButton(Button button) {
|
||||
if(lastPresses.getOrDefault(button, 0L) >= System.currentTimeMillis()-100) return;
|
||||
lastPresses.put(button, System.currentTimeMillis());
|
||||
|
||||
switch (button) {
|
||||
case A -> {
|
||||
System.out.println("A");
|
||||
System.out.println(this.tetrisGame.moveLeft());
|
||||
}
|
||||
case S -> {
|
||||
System.out.println("S");
|
||||
System.out.println(this.tetrisGame.moveDown());
|
||||
}
|
||||
case D -> {
|
||||
System.out.println("D");
|
||||
System.out.println(this.tetrisGame.moveRight());
|
||||
}
|
||||
case W -> {
|
||||
System.out.println("W");
|
||||
while(this.tetrisGame.moveDown()) {
|
||||
this.tetrisGame.addPoints(2);
|
||||
}
|
||||
}
|
||||
case mouseLeft -> {
|
||||
System.out.println("mouse left");
|
||||
System.out.println(this.tetrisGame.rotate(false));
|
||||
}
|
||||
case mouseRight -> {
|
||||
System.out.println("mouse right");
|
||||
System.out.println(this.tetrisGame.rotate(true));
|
||||
}
|
||||
case space -> {
|
||||
System.out.println("space");
|
||||
System.out.println(this.tetrisGame.switchHold());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void releasedButton(Button button) {
|
||||
lastPresses.put(button, 0L);
|
||||
this.getEntities().stream()
|
||||
.filter(entity -> entity.getEntityType().equals(Tetromino.getGhostEntityType()))
|
||||
.forEach(Entity::remove);
|
||||
this.tetrisGames.forEach((player, tetrisGame) -> tetrisGame.start());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLoad(@NotNull CompletableFuture<Void> callback) {
|
||||
protected void onStop() {
|
||||
this.tetrisGames.forEach((player, tetrisGame) -> tetrisGame.sidebar.removeViewer(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPlayerLeave(Player p) {
|
||||
this.tetrisGames.get(p).sidebar.removeViewer(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -102,9 +57,13 @@ class Tetris extends StatelessGame {
|
||||
Pos previousPosition = event.getPlayer().getPosition();
|
||||
Pos currentPosition = event.getNewPosition();
|
||||
|
||||
if(this.tetrisGame == null) return;
|
||||
TetrisGame tetrisGame = this.tetrisGames.get(player);
|
||||
|
||||
event.setNewPosition(this.tetrisGame.getPlayerSpawnPosition().withView(event.getNewPosition()));
|
||||
if(tetrisGame == null) return;
|
||||
if(tetrisGame.lost) return;
|
||||
if(player.getGameMode() == GameMode.SPECTATOR) return;
|
||||
|
||||
event.setNewPosition(tetrisGame.getPlayerSpawnPosition().withView(currentPosition));
|
||||
player.setSprinting(false);
|
||||
|
||||
Vec movementVector = currentPosition.asVec().sub(previousPosition.asVec());
|
||||
@ -122,36 +81,36 @@ class Tetris extends StatelessGame {
|
||||
double leftAmount = movementVector.dot(left);
|
||||
|
||||
if (forwardAmount > 0.018) {
|
||||
pressedButton(Button.W);
|
||||
releasedButton(Button.S);
|
||||
tetrisGame.pressedButton(TetrisGame.Button.W);
|
||||
} else if (forwardAmount < -0.018) {
|
||||
pressedButton(Button.S);
|
||||
releasedButton(Button.W);
|
||||
} else {
|
||||
releasedButton(Button.W);
|
||||
releasedButton(Button.S);
|
||||
tetrisGame.pressedButton(TetrisGame.Button.S);
|
||||
}
|
||||
|
||||
if (leftAmount > 0.018) {
|
||||
pressedButton(Button.D);
|
||||
releasedButton(Button.A);
|
||||
tetrisGame.pressedButton(TetrisGame.Button.D);
|
||||
} else if (leftAmount < -0.018) {
|
||||
pressedButton(Button.A);
|
||||
releasedButton(Button.D);
|
||||
} else {
|
||||
releasedButton(Button.A);
|
||||
releasedButton(Button.D);
|
||||
tetrisGame.pressedButton(TetrisGame.Button.A);
|
||||
}
|
||||
|
||||
if(previousPosition.y() < currentPosition.y()) pressedButton(Button.space);
|
||||
if(previousPosition.y() < currentPosition.y()) tetrisGame.pressedButton(TetrisGame.Button.space);
|
||||
}
|
||||
|
||||
protected void onPlayerInteract(@NotNull PlayerUseItemEvent event) {
|
||||
pressedButton(Button.mouseRight);
|
||||
this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseRight);
|
||||
}
|
||||
|
||||
protected void onPlayerAttack(@NotNull PlayerHandAnimationEvent event) {
|
||||
pressedButton(Button.mouseLeft);
|
||||
this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseLeft);
|
||||
}
|
||||
|
||||
protected void onPlayerTick(PlayerTickEvent event) {
|
||||
TetrisGame tetrisGame = this.tetrisGames.get(event.getPlayer());
|
||||
if(tetrisGame == null) return;
|
||||
if(tetrisGame.lost && event.getPlayer().getGameMode() != GameMode.SPECTATOR) {
|
||||
event.getPlayer().setGameMode(GameMode.SPECTATOR);
|
||||
getScore().insertResult(event.getPlayer());
|
||||
tetrisGame.sidebar.removeViewer(event.getPlayer());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -159,16 +118,20 @@ class Tetris extends StatelessGame {
|
||||
p.getInventory().setItemStack(0, ItemStack.builder(Material.BIRCH_BUTTON).customName(Component.text("Controller")).build());
|
||||
p.setSprinting(false);
|
||||
|
||||
this.tetrisGame = new TetrisGame(this, getSpawn().sub(6, 8, 15));
|
||||
this.tetrisGame.generate();
|
||||
p.teleport(this.tetrisGame.getPlayerSpawnPosition());
|
||||
if(this.tetrisGames.get(p) == null) {
|
||||
this.tetrisGames.put(p, new TetrisGame(this, getSpawn().sub(6, 8, 15).add(this.tetrisGames.size()*23, 0, 0)));
|
||||
this.tetrisGames.get(p).generate();
|
||||
}
|
||||
TetrisGame tetrisGame = this.tetrisGames.get(p);
|
||||
|
||||
p.teleport(tetrisGame.getPlayerSpawnPosition());
|
||||
tetrisGame.sidebar.addViewer(p);
|
||||
|
||||
this.tetrisGame.sidebar.addViewer(p);
|
||||
return super.onPlayerJoin(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pos getSpawn() {
|
||||
return new Pos(0, 50, 15).withView(180, 0);
|
||||
return new Pos(-50, 50, 15).withView(180, 0);
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,15 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game;
|
||||
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
|
||||
import eu.mhsl.minenet.minigames.util.BatchUtil;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.EntityType;
|
||||
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
|
||||
public class Playfield {
|
||||
private final Pos lowerLeftCorner;
|
||||
private final StatelessGame instance;
|
||||
private final int height = 22;
|
||||
private final static int height = 22;
|
||||
|
||||
public Playfield(Pos lowerLeftCorner, StatelessGame instance) {
|
||||
this.lowerLeftCorner = lowerLeftCorner;
|
||||
@ -17,7 +19,8 @@ public class Playfield {
|
||||
}
|
||||
|
||||
public Pos getPlayerSpawnPosition() {
|
||||
return this.lowerLeftCorner.add(6, 8, 20);
|
||||
// return this.lowerLeftCorner.add(6.5, 9+((double) 3/16), 20.5).withView(180, 0);
|
||||
return this.lowerLeftCorner.add(6.5, 9, 20.5).withView(180, 0);
|
||||
}
|
||||
|
||||
public Pos getTetrominoSpawnPosition() {
|
||||
@ -36,7 +39,7 @@ public class Playfield {
|
||||
AbsoluteBlockBatch batch = new AbsoluteBlockBatch();
|
||||
|
||||
for(int x=0; x<12; x++) {
|
||||
for(int y=0; y<this.height; y++) {
|
||||
for(int y = 0; y< height; y++) {
|
||||
batch.setBlock(this.lowerLeftCorner.add(x, y, 0), Block.GLASS);
|
||||
batch.setBlock(this.lowerLeftCorner.add(x, y, -1), Block.BLACK_CONCRETE);
|
||||
|
||||
@ -48,16 +51,16 @@ public class Playfield {
|
||||
}
|
||||
|
||||
batch.setBlock(getPlayerSpawnPosition().sub(0, 1, 0), Block.STONE);
|
||||
batch.setBlock(getPlayerSpawnPosition().sub(0, 1, 1), Block.STONE);
|
||||
batch.setBlock(getPlayerSpawnPosition().sub(1, 1, 1), Block.STONE);
|
||||
batch.setBlock(getPlayerSpawnPosition().sub(1, 1, 0), Block.STONE);
|
||||
|
||||
// batch.setBlock(getPlayerSpawnPosition(), Block.IRON_TRAPDOOR.withProperty("half", "bottom"));
|
||||
// batch.setBlock(getPlayerSpawnPosition().add(0, 1, 0), Block.IRON_TRAPDOOR.withProperty("half", "top"));
|
||||
|
||||
BatchUtil.loadAndApplyBatch(batch, this.instance, () -> {});
|
||||
}
|
||||
|
||||
public int removeFullLines() {
|
||||
int removedLinesCounter = 0;
|
||||
for(int y=1; y<this.height; y++) {
|
||||
for(int y = 1; y< height; y++) {
|
||||
boolean isFullLine = true;
|
||||
for(int x=1; x<11; x++) {
|
||||
if(this.instance.getBlock(this.lowerLeftCorner.add(x, y, 1)) == Block.AIR) isFullLine = false;
|
||||
@ -71,19 +74,22 @@ public class Playfield {
|
||||
return removedLinesCounter;
|
||||
}
|
||||
|
||||
public void removeBlock(Block block) {
|
||||
for(int x=0; x<12; x++) {
|
||||
for(int y=0; y<this.height; y++) {
|
||||
if(this.instance.getBlock(this.lowerLeftCorner.add(x, y, 1)).equals(block)) {
|
||||
this.instance.setBlock(this.lowerLeftCorner.add(x, y, 1), Block.AIR);
|
||||
public void removeEntity(EntityType entityType) {
|
||||
this.instance.getEntities().stream()
|
||||
.filter(entity -> entity.getEntityType().equals(entityType))
|
||||
.filter(entity -> {
|
||||
Pos position = entity.getPosition();
|
||||
if(position.x() > this.lowerLeftCorner.x() && position.y() > this.lowerLeftCorner.y()) {
|
||||
return position.x() < this.lowerLeftCorner.x() + 11 && position.y() < this.lowerLeftCorner.y() + height;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.forEach(Entity::remove);
|
||||
}
|
||||
|
||||
|
||||
private void removeFullLine(int positionY) {
|
||||
for(int y=positionY; y<this.height; y++) {
|
||||
for(int y = positionY; y< height; y++) {
|
||||
for(int x=1; x<11; x++) {
|
||||
Block blockAbove = this.instance.getBlock(this.lowerLeftCorner.add(x, y+1, 1));
|
||||
this.instance.setBlock(this.lowerLeftCorner.add(x, y, 1), blockAbove);
|
||||
|
66
src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/TetrisGame.java
66
src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/TetrisGame.java
@ -8,9 +8,7 @@ import net.minestom.server.scoreboard.Sidebar;
|
||||
import net.minestom.server.timer.Scheduler;
|
||||
import net.minestom.server.timer.TaskSchedule;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
public class TetrisGame {
|
||||
private final StatelessGame instance;
|
||||
@ -18,7 +16,7 @@ public class TetrisGame {
|
||||
private int level = 1;
|
||||
private int lines = 0;
|
||||
private int score = 0;
|
||||
private boolean lost = false;
|
||||
public boolean lost = false;
|
||||
public boolean paused = true;
|
||||
public Tetromino currentTetromino;
|
||||
private Tetromino nextTetromino;
|
||||
@ -29,6 +27,17 @@ public class TetrisGame {
|
||||
private final Pos holdPosition;
|
||||
private final Pos tetrominoSpawnPosition;
|
||||
public Sidebar sidebar = new Sidebar(Component.text("Info:"));
|
||||
private final Map<Button, Long> lastPresses = new HashMap<>();
|
||||
|
||||
public enum Button {
|
||||
W,
|
||||
A,
|
||||
S,
|
||||
D,
|
||||
mouseLeft,
|
||||
mouseRight,
|
||||
space
|
||||
}
|
||||
|
||||
public TetrisGame(StatelessGame instance, Pos lowerLeftCorner) {
|
||||
this(instance, lowerLeftCorner, Tetromino.Shape.J);
|
||||
@ -47,6 +56,24 @@ public class TetrisGame {
|
||||
this.nextTetromino = this.getNextTetromino();
|
||||
}
|
||||
|
||||
public void pressedButton(Button button) {
|
||||
if(lastPresses.getOrDefault(button, 0L) >= System.currentTimeMillis()-100) return;
|
||||
|
||||
lastPresses.put(button, System.currentTimeMillis());
|
||||
if(button == Button.W) lastPresses.put(button, System.currentTimeMillis()+70);
|
||||
if(button == Button.S) lastPresses.put(button, System.currentTimeMillis()-70);
|
||||
|
||||
switch (button) {
|
||||
case A -> this.moveLeft();
|
||||
case S -> this.moveDown();
|
||||
case D -> this.moveRight();
|
||||
case W -> this.hardDrop();
|
||||
case mouseLeft -> this.rotate(false);
|
||||
case mouseRight -> this.rotate(true);
|
||||
case space -> this.switchHold();
|
||||
}
|
||||
}
|
||||
|
||||
public Pos getPlayerSpawnPosition() {
|
||||
return this.playfield.getPlayerSpawnPosition();
|
||||
}
|
||||
@ -76,28 +103,23 @@ public class TetrisGame {
|
||||
}
|
||||
}
|
||||
|
||||
public void addPoints(int points) {
|
||||
this.score += points;
|
||||
this.updateSidebar();
|
||||
}
|
||||
|
||||
|
||||
public boolean rotate(boolean clockwise) {
|
||||
private boolean rotate(boolean clockwise) {
|
||||
if(this.lost || this.paused) return false;
|
||||
return this.currentTetromino.rotate(clockwise);
|
||||
}
|
||||
|
||||
public boolean moveLeft() {
|
||||
private boolean moveLeft() {
|
||||
if(this.lost || this.paused) return false;
|
||||
return this.currentTetromino.moveLeft();
|
||||
}
|
||||
|
||||
public boolean moveRight() {
|
||||
private boolean moveRight() {
|
||||
if(this.lost || this.paused) return false;
|
||||
return this.currentTetromino.moveRight();
|
||||
}
|
||||
|
||||
public boolean moveDown() {
|
||||
private boolean moveDown() {
|
||||
if(this.lost || this.paused) return false;
|
||||
if(!this.currentTetromino.moveDown()) {
|
||||
this.setActiveTetrominoDown();
|
||||
@ -108,7 +130,23 @@ public class TetrisGame {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean switchHold() {
|
||||
private boolean hardDrop() {
|
||||
if(this.lost || this.paused) return false;
|
||||
if(!this.currentTetromino.moveDown()) {
|
||||
this.setActiveTetrominoDown();
|
||||
return false;
|
||||
}
|
||||
this.score += 2;
|
||||
this.updateSidebar();
|
||||
while(this.currentTetromino.moveDown()) {
|
||||
this.score += 2;
|
||||
this.updateSidebar();
|
||||
}
|
||||
this.setActiveTetrominoDown();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean switchHold() {
|
||||
if(!holdPossible) return false;
|
||||
if(this.lost || this.paused) return false;
|
||||
|
||||
|
@ -2,6 +2,9 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game;
|
||||
|
||||
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.EntityType;
|
||||
import net.minestom.server.entity.metadata.other.FallingBlockMeta;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -14,7 +17,7 @@ public class Tetromino {
|
||||
private final Playfield playfield;
|
||||
private Pos position;
|
||||
private int[][] shapeArray;
|
||||
private final Block ghostBlock = Block.ICE;
|
||||
private final static EntityType ghostEntityType = EntityType.FALLING_BLOCK;
|
||||
|
||||
public enum Shape {
|
||||
I,
|
||||
@ -42,6 +45,10 @@ public class Tetromino {
|
||||
}
|
||||
}
|
||||
|
||||
public static EntityType getGhostEntityType() {
|
||||
return ghostEntityType;
|
||||
}
|
||||
|
||||
public void setPosition(Pos newPosition) {
|
||||
this.position = newPosition;
|
||||
}
|
||||
@ -72,13 +79,20 @@ public class Tetromino {
|
||||
|
||||
public void draw(boolean withGhost) {
|
||||
if(withGhost) {
|
||||
this.playfield.removeBlock(this.ghostBlock);
|
||||
this.playfield.removeEntity(ghostEntityType);
|
||||
Pos ghostPos = this.position;
|
||||
while (!checkCollision(ghostPos.sub(0, 1, 0), this.shapeArray)) {
|
||||
ghostPos = ghostPos.sub(0, 1, 0);
|
||||
}
|
||||
Pos positionChange = this.position.sub(ghostPos);
|
||||
getBlockPositions().forEach(pos -> this.instance.setBlock(pos.sub(positionChange), this.ghostBlock));
|
||||
getBlockPositions().forEach(pos -> {
|
||||
Entity ghostBlock = new Entity(ghostEntityType);
|
||||
((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getGhostBlock());
|
||||
ghostBlock.setNoGravity(true);
|
||||
ghostBlock.setGlowing(true);
|
||||
ghostBlock.setInvisible(true);
|
||||
ghostBlock.setInstance(this.instance, pos.sub(positionChange).add(0.5, 0, 0.5));
|
||||
});
|
||||
}
|
||||
|
||||
getBlockPositions().forEach(pos -> this.instance.setBlock(pos, this.getColoredBlock()));
|
||||
@ -104,6 +118,21 @@ public class Tetromino {
|
||||
return returnBlock;
|
||||
}
|
||||
|
||||
private Block getGhostBlock() {
|
||||
Block returnBlock;
|
||||
switch (this.shape) {
|
||||
case I -> returnBlock = Block.LIGHT_BLUE_STAINED_GLASS;
|
||||
case J -> returnBlock = Block.BLUE_STAINED_GLASS;
|
||||
case L -> returnBlock = Block.ORANGE_STAINED_GLASS;
|
||||
case O -> returnBlock = Block.YELLOW_STAINED_GLASS;
|
||||
case S -> returnBlock = Block.GREEN_STAINED_GLASS;
|
||||
case T -> returnBlock = Block.PURPLE_STAINED_GLASS;
|
||||
case Z -> returnBlock = Block.RED_STAINED_GLASS;
|
||||
default -> returnBlock = Block.WHITE_STAINED_GLASS;
|
||||
}
|
||||
return returnBlock;
|
||||
}
|
||||
|
||||
private int[][] getTurnedShapeArray(boolean clockwise) {
|
||||
int iterations = 1;
|
||||
if(!clockwise) iterations = 3;
|
||||
@ -163,7 +192,7 @@ public class Tetromino {
|
||||
|
||||
for(Pos pos : newBlockPositions) {
|
||||
if(isPartOfTetromino(pos)) continue;
|
||||
if(this.instance.getBlock(pos) == this.ghostBlock) continue;
|
||||
if(this.instance.getBlock(pos) == this.getGhostBlock()) continue;
|
||||
if(this.instance.getBlock(pos) != Block.AIR) return true;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user