mirror of
https://github.com/Minestom/Minestom.git
synced 2025-02-13 10:51:44 +01:00
Remove FakePlayer
(#2006)
* feat: remove FakePlayer * fix: oops, demo server exists
This commit is contained in:
parent
009ba773ed
commit
2947279898
@ -14,7 +14,6 @@ import net.minestom.server.entity.GameMode;
|
||||
import net.minestom.server.entity.ItemEntity;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.damage.Damage;
|
||||
import net.minestom.server.entity.fakeplayer.FakePlayer;
|
||||
import net.minestom.server.event.Event;
|
||||
import net.minestom.server.event.EventNode;
|
||||
import net.minestom.server.event.entity.EntityAttackEvent;
|
||||
@ -41,7 +40,6 @@ import net.minestom.server.world.DimensionType;
|
||||
import java.time.Duration;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@ -84,10 +82,6 @@ public class PlayerInit {
|
||||
itemEntity.setInstance(player.getInstance(), playerPos.withY(y -> y + 1.5));
|
||||
Vec velocity = playerPos.direction().mul(6);
|
||||
itemEntity.setVelocity(velocity);
|
||||
|
||||
FakePlayer.initPlayer(UUID.randomUUID(), "fake123", fp -> {
|
||||
System.out.println("fp = " + fp);
|
||||
});
|
||||
})
|
||||
.addListener(PlayerDisconnectEvent.class, event -> System.out.println("DISCONNECTION " + event.getPlayer().getUsername()))
|
||||
.addListener(AsyncPlayerConfigurationEvent.class, event -> {
|
||||
|
@ -33,7 +33,6 @@ import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.effects.Effects;
|
||||
import net.minestom.server.entity.damage.DamageType;
|
||||
import net.minestom.server.entity.fakeplayer.FakePlayer;
|
||||
import net.minestom.server.entity.metadata.LivingEntityMeta;
|
||||
import net.minestom.server.entity.metadata.PlayerMeta;
|
||||
import net.minestom.server.entity.vehicle.PlayerVehicleInformation;
|
||||
@ -112,8 +111,7 @@ import java.util.function.Consumer;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Those are the major actors of the server,
|
||||
* they are not necessary backed by a {@link PlayerSocketConnection} as shown by {@link FakePlayer}.
|
||||
* Those are the major actors of the server
|
||||
* <p>
|
||||
* You can easily create your own implementation of this and use it with {@link ConnectionManager#setPlayerProvider(PlayerProvider)}.
|
||||
*/
|
||||
|
@ -1,170 +0,0 @@
|
||||
package net.minestom.server.entity.fakeplayer;
|
||||
|
||||
import com.extollit.gaming.ai.path.HydrazinePathFinder;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.pathfinding.NavigableEntity;
|
||||
import net.minestom.server.entity.pathfinding.Navigator;
|
||||
import net.minestom.server.event.EventListener;
|
||||
import net.minestom.server.event.player.PlayerSpawnEvent;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.listener.manager.PacketListenerManager;
|
||||
import net.minestom.server.network.ConnectionManager;
|
||||
import net.minestom.server.network.ConnectionState;
|
||||
import net.minestom.server.network.packet.client.login.ClientLoginAcknowledgedPacket;
|
||||
import net.minestom.server.network.player.FakePlayerConnection;
|
||||
import net.minestom.server.network.player.PlayerConnection;
|
||||
import net.minestom.server.utils.time.TimeUnit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A fake player will behave exactly the same way as would do a {@link Player} backed by a socket connection
|
||||
* (events, velocity, gravity, player list, etc...) with the exception that you need to control it server-side
|
||||
* using a {@link FakePlayerController} (see {@link #getController()}).
|
||||
* <p>
|
||||
* You can create one using {@link #initPlayer(UUID, String, Consumer)}. Be aware that this really behave exactly like a player
|
||||
* and this is a feature not a bug, you will need to check at some place if the player is a fake one or not (instanceof) if you want to change it.
|
||||
*/
|
||||
public class FakePlayer extends Player implements NavigableEntity {
|
||||
|
||||
private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
|
||||
private static final PacketListenerManager PACKET_LISTENER_MANAGER = MinecraftServer.getPacketListenerManager();
|
||||
|
||||
private final FakePlayerOption option;
|
||||
private final FakePlayerController fakePlayerController;
|
||||
|
||||
private final Navigator navigator = new Navigator(this);
|
||||
|
||||
private EventListener<PlayerSpawnEvent> spawnListener;
|
||||
|
||||
/**
|
||||
* Initializes a new {@link FakePlayer} with the given {@code uuid}, {@code username} and {@code option}'s.
|
||||
*
|
||||
* @param uuid The unique identifier for the fake player.
|
||||
* @param username The username for the fake player.
|
||||
* @param option Any option for the fake player.
|
||||
*/
|
||||
protected FakePlayer(@NotNull UUID uuid, @NotNull String username,
|
||||
@NotNull FakePlayerOption option,
|
||||
@Nullable Consumer<FakePlayer> spawnCallback) {
|
||||
super(uuid, username, new FakePlayerConnection());
|
||||
|
||||
this.option = option;
|
||||
|
||||
this.fakePlayerController = new FakePlayerController(this);
|
||||
|
||||
if (spawnCallback != null) {
|
||||
spawnListener = EventListener.builder(PlayerSpawnEvent.class)
|
||||
.expireWhen(ignored -> this.isRemoved())
|
||||
.handler(event -> {
|
||||
if (event.getPlayer().equals(this))
|
||||
if (event.isFirstSpawn()) {
|
||||
spawnCallback.accept(this);
|
||||
MinecraftServer.getGlobalEventHandler().removeListener(spawnListener);
|
||||
}
|
||||
}).build();
|
||||
MinecraftServer.getGlobalEventHandler().addListener(spawnListener);
|
||||
}
|
||||
|
||||
playerConnection.setConnectionState(ConnectionState.LOGIN);
|
||||
CONNECTION_MANAGER.transitionLoginToConfig(this).thenRun(() -> {
|
||||
// Need to immediately reply with login acknowledged for the player to enter config.
|
||||
PACKET_LISTENER_MANAGER.processClientPacket(new ClientLoginAcknowledgedPacket(), getPlayerConnection());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new {@link FakePlayer}.
|
||||
*
|
||||
* @param uuid the FakePlayer uuid
|
||||
* @param username the FakePlayer username
|
||||
* @param spawnCallback the optional callback called when the fake player first spawn
|
||||
*/
|
||||
public static void initPlayer(@NotNull UUID uuid, @NotNull String username,
|
||||
@NotNull FakePlayerOption option, @Nullable Consumer<FakePlayer> spawnCallback) {
|
||||
new FakePlayer(uuid, username, option, spawnCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new {@link FakePlayer} without adding it in cache.
|
||||
* <p>
|
||||
* If you want the fake player to be obtainable with the {@link net.minestom.server.network.ConnectionManager}
|
||||
* you need to specify it in a {@link FakePlayerOption} and use {@link #initPlayer(UUID, String, FakePlayerOption, Consumer)}.
|
||||
*
|
||||
* @param uuid the FakePlayer uuid
|
||||
* @param username the FakePlayer username
|
||||
* @param spawnCallback the optional callback called when the fake player first spawn
|
||||
*/
|
||||
public static void initPlayer(@NotNull UUID uuid, @NotNull String username, @Nullable Consumer<FakePlayer> spawnCallback) {
|
||||
initPlayer(uuid, username, new FakePlayerOption(), spawnCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fake player option container.
|
||||
*
|
||||
* @return the fake player option
|
||||
*/
|
||||
@NotNull
|
||||
public FakePlayerOption getOption() {
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the controller for the fake player.
|
||||
*
|
||||
* @return The fake player's controller.
|
||||
*/
|
||||
@NotNull
|
||||
public FakePlayerController getController() {
|
||||
return fakePlayerController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(long time) {
|
||||
super.update(time);
|
||||
// Path finding
|
||||
this.navigator.tick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) {
|
||||
this.navigator.setPathFinder(new HydrazinePathFinder(navigator.getPathingEntity(), instance.getInstanceSpace()));
|
||||
|
||||
return super.setInstance(instance, spawnPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNewViewer(@NotNull Player player) {
|
||||
player.sendPacket(getAddPlayerToList());
|
||||
handleTabList(player.getPlayerConnection());
|
||||
super.updateNewViewer(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void showPlayer(@NotNull PlayerConnection connection) {
|
||||
super.showPlayer(connection);
|
||||
handleTabList(connection);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Navigator getNavigator() {
|
||||
return navigator;
|
||||
}
|
||||
|
||||
private void handleTabList(PlayerConnection connection) {
|
||||
if (!option.isInTabList()) {
|
||||
// Remove from tab-list
|
||||
MinecraftServer.getSchedulerManager().buildTask(() -> connection.sendPacket(getRemovePlayerToList())).delay(20, TimeUnit.SERVER_TICK).schedule();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,215 +0,0 @@
|
||||
package net.minestom.server.entity.fakeplayer;
|
||||
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.instance.block.BlockFace;
|
||||
import net.minestom.server.inventory.AbstractInventory;
|
||||
import net.minestom.server.inventory.Inventory;
|
||||
import net.minestom.server.inventory.PlayerInventory;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.network.packet.client.ClientPacket;
|
||||
import net.minestom.server.network.packet.client.common.ClientKeepAlivePacket;
|
||||
import net.minestom.server.network.packet.client.common.ClientPluginMessagePacket;
|
||||
import net.minestom.server.network.packet.client.configuration.ClientFinishConfigurationPacket;
|
||||
import net.minestom.server.network.packet.client.play.*;
|
||||
import net.minestom.server.network.packet.server.SendablePacket;
|
||||
import net.minestom.server.network.packet.server.ServerPacket;
|
||||
import net.minestom.server.network.packet.server.common.KeepAlivePacket;
|
||||
import net.minestom.server.network.packet.server.configuration.FinishConfigurationPacket;
|
||||
import net.minestom.server.network.packet.server.play.PlayerPositionAndLookPacket;
|
||||
import net.minestom.server.network.player.PlayerConnection;
|
||||
import net.minestom.server.utils.MathUtils;
|
||||
import net.minestom.server.utils.inventory.PlayerInventoryUtils;
|
||||
import net.minestom.server.utils.validate.Check;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class acts as a client controller for {@link FakePlayer}.
|
||||
* <p>
|
||||
* The main use is to simulate the receiving of {@link ClientPacket}
|
||||
*/
|
||||
public class FakePlayerController {
|
||||
|
||||
private final FakePlayer fakePlayer;
|
||||
|
||||
/**
|
||||
* Initializes a new {@link FakePlayerController} with the given {@link FakePlayer}.
|
||||
*
|
||||
* @param fakePlayer The fake player that should used the controller.
|
||||
*/
|
||||
public FakePlayerController(@NotNull FakePlayer fakePlayer) {
|
||||
this.fakePlayer = fakePlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates a click in a window.
|
||||
*
|
||||
* @param playerInventory {@code true} if the window a {@link PlayerInventory}, otherwise {@code false}.
|
||||
* @param slot The slot where the fake player should click on.
|
||||
* @param button The mouse button that the fake player should used.
|
||||
* @param clickType The click type
|
||||
*/
|
||||
public void clickWindow(boolean playerInventory, short slot, byte button, ClientClickWindowPacket.ClickType clickType) {
|
||||
Inventory inventory = playerInventory ? null : fakePlayer.getOpenInventory();
|
||||
AbstractInventory abstractInventory = inventory == null ? fakePlayer.getInventory() : inventory;
|
||||
playerInventory = abstractInventory instanceof PlayerInventory;
|
||||
|
||||
slot = playerInventory ? (short) PlayerInventoryUtils.convertToPacketSlot(slot) : slot;
|
||||
|
||||
ItemStack itemStack = abstractInventory.getItemStack(slot);
|
||||
|
||||
addToQueue(new ClientClickWindowPacket(playerInventory ? 0 : inventory.getWindowId(), 0,
|
||||
slot, button, clickType,
|
||||
List.of(), itemStack));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the current opened inventory if there is any.
|
||||
*/
|
||||
public void closeWindow() {
|
||||
Inventory openInventory = fakePlayer.getOpenInventory();
|
||||
addToQueue(new ClientCloseWindowPacket(openInventory == null ? 0 : openInventory.getWindowId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a plugin message to the player.
|
||||
*
|
||||
* @param channel The channel of the message.
|
||||
* @param message The message data.
|
||||
*/
|
||||
public void sendPluginMessage(String channel, byte[] message) {
|
||||
addToQueue(new ClientPluginMessagePacket(channel, message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a plugin message to the player.
|
||||
*
|
||||
* @param channel The channel of the message.
|
||||
* @param message The message data.
|
||||
*/
|
||||
public void sendPluginMessage(String channel, String message) {
|
||||
sendPluginMessage(channel, message.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Attacks the given {@code entity}.
|
||||
*
|
||||
* @param entity The entity that is to be attacked.
|
||||
*/
|
||||
public void attackEntity(Entity entity) {
|
||||
addToQueue(new ClientInteractEntityPacket(entity.getEntityId(), new ClientInteractEntityPacket.Attack(), fakePlayer.isSneaking()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Respawns the player.
|
||||
*
|
||||
* @see Player#respawn()
|
||||
*/
|
||||
public void respawn() {
|
||||
// Sending the respawn packet for some reason
|
||||
// Is related to FakePlayer#showPlayer and the tablist option (probably because of the scheduler)
|
||||
/*ClientStatusPacket statusPacket = new ClientStatusPacket();
|
||||
statusPacket.action = ClientStatusPacket.Action.PERFORM_RESPAWN;
|
||||
addToQueue(statusPacket);*/
|
||||
fakePlayer.respawn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the current held slot for the player.
|
||||
*
|
||||
* @param slot The slot that the player has to held.
|
||||
* @throws IllegalArgumentException If {@code slot} is not between {@code 0} and {@code 8}.
|
||||
*/
|
||||
public void setHeldItem(short slot) {
|
||||
Check.argCondition(!MathUtils.isBetween(slot, 0, 8), "Slot has to be between 0 and 8!");
|
||||
addToQueue(new ClientHeldItemChangePacket(slot));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an animation packet that animates the specified arm.
|
||||
*
|
||||
* @param hand The hand of the arm to be animated.
|
||||
*/
|
||||
public void sendArmAnimation(Player.Hand hand) {
|
||||
addToQueue(new ClientAnimationPacket(hand));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the item in the given {@code hand}.
|
||||
*
|
||||
* @param hand The hand in which an ite mshould be.
|
||||
*/
|
||||
public void useItem(Player.Hand hand) {
|
||||
addToQueue(new ClientUseItemPacket(hand, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the fake player.
|
||||
*
|
||||
* @param yaw The new yaw for the fake player.
|
||||
* @param pitch The new pitch for the fake player.
|
||||
*/
|
||||
public void rotate(float yaw, float pitch) {
|
||||
addToQueue(new ClientPlayerRotationPacket(yaw, pitch, fakePlayer.isOnGround()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the digging process of the fake player.
|
||||
*
|
||||
* @param blockPosition The position of the block to be excavated.
|
||||
* @param blockFace From where the block is struck.
|
||||
*/
|
||||
public void startDigging(Point blockPosition, BlockFace blockFace) {
|
||||
addToQueue(new ClientPlayerDiggingPacket(ClientPlayerDiggingPacket.Status.STARTED_DIGGING, blockPosition, blockFace, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the digging process of the fake player.
|
||||
*
|
||||
* @param blockPosition The position of the block to be excavated.
|
||||
* @param blockFace From where the block is struck.
|
||||
*/
|
||||
public void stopDigging(Point blockPosition, BlockFace blockFace) {
|
||||
addToQueue(new ClientPlayerDiggingPacket(ClientPlayerDiggingPacket.Status.CANCELLED_DIGGING, blockPosition, blockFace, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes the digging process of the fake player.
|
||||
*
|
||||
* @param blockPosition The position of the block to be excavated.
|
||||
* @param blockFace From where the block is struck.
|
||||
*/
|
||||
public void finishDigging(Point blockPosition, BlockFace blockFace) {
|
||||
addToQueue(new ClientPlayerDiggingPacket(ClientPlayerDiggingPacket.Status.FINISHED_DIGGING, blockPosition, blockFace, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the player receives a packet
|
||||
* WARNING: pretty much unsafe, used internally to redirect packets here,
|
||||
* you should instead use {@link PlayerConnection#sendPacket(SendablePacket)}
|
||||
*
|
||||
* @param serverPacket the packet to consume
|
||||
*/
|
||||
public void consumePacket(ServerPacket serverPacket) {
|
||||
if (serverPacket instanceof PlayerPositionAndLookPacket playerPositionAndLookPacket) {
|
||||
addToQueue(new ClientTeleportConfirmPacket(playerPositionAndLookPacket.teleportId()));
|
||||
} else if (serverPacket instanceof KeepAlivePacket keepAlivePacket) {
|
||||
addToQueue(new ClientKeepAlivePacket(keepAlivePacket.id()));
|
||||
} else if (serverPacket instanceof FinishConfigurationPacket) {
|
||||
addToQueue(new ClientFinishConfigurationPacket());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All packets in the queue are executed in the {@link Player#update(long)} method. It is used internally to add all
|
||||
* received packet from the client. Could be used to "simulate" a received packet, but to use at your own risk!
|
||||
*
|
||||
* @param clientPlayPacket The packet to add in the queue.
|
||||
*/
|
||||
private void addToQueue(ClientPacket clientPlayPacket) {
|
||||
this.fakePlayer.addPacketToQueue(clientPlayPacket);
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package net.minestom.server.entity.fakeplayer;
|
||||
|
||||
import net.minestom.server.network.ConnectionManager;
|
||||
|
||||
/**
|
||||
* Represents any options for a {@link FakePlayer}.
|
||||
*/
|
||||
public class FakePlayerOption {
|
||||
|
||||
private boolean registered = false;
|
||||
private boolean inTabList = false;
|
||||
|
||||
/**
|
||||
* Gets if the player is registered internally as a Player.
|
||||
*
|
||||
* @return true if the player is registered in {@link ConnectionManager}, false otherwise
|
||||
*/
|
||||
public boolean isRegistered() {
|
||||
return registered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the FakePlayer as registered or not.
|
||||
* <p>
|
||||
* WARNING: this can't be changed halfway.
|
||||
*
|
||||
* @param registered should the fake player be registered internally
|
||||
* @return this instance, allowing for chained method calls
|
||||
*/
|
||||
public FakePlayerOption setRegistered(boolean registered) {
|
||||
this.registered = registered;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if the player is visible in the tab-list or not.
|
||||
*
|
||||
* @return true if the player is in the tab-list, false otherwise
|
||||
*/
|
||||
public boolean isInTabList() {
|
||||
return inTabList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the player in the tab-list or not.
|
||||
* <p>
|
||||
* WARNING: this can't be changed halfway.
|
||||
*
|
||||
* @param inTabList should the player be in the tab-list
|
||||
* @return this instance, allowing for chained method calls
|
||||
*/
|
||||
public FakePlayerOption setInTabList(boolean inTabList) {
|
||||
this.inTabList = inTabList;
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package net.minestom.server.network.player;
|
||||
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.fakeplayer.FakePlayer;
|
||||
import net.minestom.server.entity.fakeplayer.FakePlayerController;
|
||||
import net.minestom.server.network.packet.server.SendablePacket;
|
||||
import net.minestom.server.network.packet.server.ServerPacket;
|
||||
import net.minestom.server.utils.validate.Check;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
public class FakePlayerConnection extends PlayerConnection {
|
||||
|
||||
@Override
|
||||
public void sendPacket(@NotNull SendablePacket packet) {
|
||||
FakePlayerController controller = getFakePlayer().getController();
|
||||
final ServerPacket serverPacket = SendablePacket.extractServerPacket(getConnectionState(), packet);
|
||||
controller.consumePacket(serverPacket);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public SocketAddress getRemoteAddress() {
|
||||
return new InetSocketAddress(0);
|
||||
}
|
||||
|
||||
public FakePlayer getFakePlayer() {
|
||||
return (FakePlayer) getPlayer();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setPlayer(Player player) {
|
||||
Check.argCondition(!(player instanceof FakePlayer), "FakePlayerController needs a FakePlayer object");
|
||||
super.setPlayer(player);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user