Merge branch 'master' into 1.13.2
This commit is contained in:
@@ -316,8 +316,7 @@ public class ArgConsumer implements IArgConsumer {
|
||||
try {
|
||||
return datatype.apply(this.context, original);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new CommandInvalidTypeException(hasAny() ? peek() : consumed(), datatype.getClass().getSimpleName());
|
||||
throw new CommandInvalidTypeException(hasAny() ? peek() : consumed(), datatype.getClass().getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +345,7 @@ public class ArgConsumer implements IArgConsumer {
|
||||
try {
|
||||
return datatype.get(this.context);
|
||||
} catch (Exception e) {
|
||||
throw new CommandInvalidTypeException(hasAny() ? peek() : consumed(), datatype.getClass().getSimpleName());
|
||||
throw new CommandInvalidTypeException(hasAny() ? peek() : consumed(), datatype.getClass().getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* This file is part of Baritone.
|
||||
*
|
||||
* Baritone is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Baritone is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package baritone.command.defaults;
|
||||
|
||||
import baritone.api.IBaritone;
|
||||
import baritone.api.command.Command;
|
||||
import baritone.api.command.exception.CommandException;
|
||||
import baritone.api.command.argument.IArgConsumer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class CancelCommand extends Command {
|
||||
|
||||
public CancelCommand(IBaritone baritone) {
|
||||
super(baritone, "cancel", "stop");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(String label, IArgConsumer args) throws CommandException {
|
||||
args.requireMax(0);
|
||||
baritone.getPathingBehavior().cancelEverything();
|
||||
logDirect("ok canceled");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<String> tabComplete(String label, IArgConsumer args) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getShortDesc() {
|
||||
return "Cancel what Baritone is currently doing";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getLongDesc() {
|
||||
return Arrays.asList(
|
||||
"The cancel command tells Baritone to stop whatever it's currently doing.",
|
||||
"",
|
||||
"Usage:",
|
||||
"> cancel"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,52 +24,53 @@ import java.util.*;
|
||||
|
||||
public final class DefaultCommands {
|
||||
|
||||
private DefaultCommands() {}
|
||||
private DefaultCommands() {
|
||||
}
|
||||
|
||||
public static List<ICommand> createAll(IBaritone baritone) {
|
||||
Objects.requireNonNull(baritone);
|
||||
List<ICommand> commands = new ArrayList<>(Arrays.asList(
|
||||
new HelpCommand(baritone),
|
||||
new SetCommand(baritone),
|
||||
new CommandAlias(baritone, Arrays.asList("modified", "mod", "baritone", "modifiedsettings"), "List modified settings", "set modified"),
|
||||
new CommandAlias(baritone, "reset", "Reset all settings or just one", "set reset"),
|
||||
new GoalCommand(baritone),
|
||||
new GotoCommand(baritone),
|
||||
new PathCommand(baritone),
|
||||
new ProcCommand(baritone),
|
||||
new VersionCommand(baritone),
|
||||
new RepackCommand(baritone),
|
||||
new BuildCommand(baritone),
|
||||
new SchematicaCommand(baritone),
|
||||
new ComeCommand(baritone),
|
||||
new AxisCommand(baritone),
|
||||
new CancelCommand(baritone),
|
||||
new ForceCancelCommand(baritone),
|
||||
new GcCommand(baritone),
|
||||
new InvertCommand(baritone),
|
||||
new TunnelCommand(baritone),
|
||||
new RenderCommand(baritone),
|
||||
new FarmCommand(baritone),
|
||||
new ChestsCommand(baritone),
|
||||
new FollowCommand(baritone),
|
||||
new ExploreFilterCommand(baritone),
|
||||
new ReloadAllCommand(baritone),
|
||||
new SaveAllCommand(baritone),
|
||||
new ExploreCommand(baritone),
|
||||
new BlacklistCommand(baritone),
|
||||
new FindCommand(baritone),
|
||||
new MineCommand(baritone),
|
||||
new ClickCommand(baritone),
|
||||
new ThisWayCommand(baritone),
|
||||
new WaypointsCommand(baritone),
|
||||
new CommandAlias(baritone, "sethome", "Sets your home waypoint", "waypoints save home"),
|
||||
new CommandAlias(baritone, "home", "Set goal to your home waypoint", "waypoints goal home"),
|
||||
new SelCommand(baritone)
|
||||
new HelpCommand(baritone),
|
||||
new SetCommand(baritone),
|
||||
new CommandAlias(baritone, Arrays.asList("modified", "mod", "baritone", "modifiedsettings"), "List modified settings", "set modified"),
|
||||
new CommandAlias(baritone, "reset", "Reset all settings or just one", "set reset"),
|
||||
new GoalCommand(baritone),
|
||||
new GotoCommand(baritone),
|
||||
new PathCommand(baritone),
|
||||
new ProcCommand(baritone),
|
||||
new VersionCommand(baritone),
|
||||
new RepackCommand(baritone),
|
||||
new BuildCommand(baritone),
|
||||
new SchematicaCommand(baritone),
|
||||
new ComeCommand(baritone),
|
||||
new AxisCommand(baritone),
|
||||
new ForceCancelCommand(baritone),
|
||||
new GcCommand(baritone),
|
||||
new InvertCommand(baritone),
|
||||
new TunnelCommand(baritone),
|
||||
new RenderCommand(baritone),
|
||||
new FarmCommand(baritone),
|
||||
new ChestsCommand(baritone),
|
||||
new FollowCommand(baritone),
|
||||
new ExploreFilterCommand(baritone),
|
||||
new ReloadAllCommand(baritone),
|
||||
new SaveAllCommand(baritone),
|
||||
new ExploreCommand(baritone),
|
||||
new BlacklistCommand(baritone),
|
||||
new FindCommand(baritone),
|
||||
new MineCommand(baritone),
|
||||
new ClickCommand(baritone),
|
||||
new ThisWayCommand(baritone),
|
||||
new WaypointsCommand(baritone),
|
||||
new CommandAlias(baritone, "sethome", "Sets your home waypoint", "waypoints save home"),
|
||||
new CommandAlias(baritone, "home", "Set goal to your home waypoint", "waypoints goal home"),
|
||||
new SelCommand(baritone)
|
||||
));
|
||||
PauseResumeCommands prc = new PauseResumeCommands(baritone);
|
||||
ExecutionControlCommands prc = new ExecutionControlCommands(baritone);
|
||||
commands.add(prc.pauseCommand);
|
||||
commands.add(prc.resumeCommand);
|
||||
commands.add(prc.pausedCommand);
|
||||
commands.add(prc.cancelCommand);
|
||||
return Collections.unmodifiableList(commands);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
package baritone.command.defaults;
|
||||
|
||||
import baritone.api.IBaritone;
|
||||
import baritone.api.command.Command;
|
||||
import baritone.api.command.argument.IArgConsumer;
|
||||
import baritone.api.command.exception.CommandException;
|
||||
import baritone.api.command.exception.CommandInvalidStateException;
|
||||
import baritone.api.process.IBaritoneProcess;
|
||||
import baritone.api.process.PathingCommand;
|
||||
import baritone.api.process.PathingCommandType;
|
||||
import baritone.api.command.Command;
|
||||
import baritone.api.command.exception.CommandException;
|
||||
import baritone.api.command.exception.CommandInvalidStateException;
|
||||
import baritone.api.command.argument.IArgConsumer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -37,13 +37,14 @@ import java.util.stream.Stream;
|
||||
* TO USE THIS to pause and resume Baritone. Make your own process that returns {@link PathingCommandType#REQUEST_PAUSE
|
||||
* REQUEST_PAUSE} as needed.
|
||||
*/
|
||||
public class PauseResumeCommands {
|
||||
public class ExecutionControlCommands {
|
||||
|
||||
Command pauseCommand;
|
||||
Command resumeCommand;
|
||||
Command pausedCommand;
|
||||
Command cancelCommand;
|
||||
|
||||
public PauseResumeCommands(IBaritone baritone) {
|
||||
public ExecutionControlCommands(IBaritone baritone) {
|
||||
// array for mutability, non-field so reflection can't touch it
|
||||
final boolean[] paused = {false};
|
||||
baritone.getPathingControlManager().registerProcess(
|
||||
@@ -64,7 +65,8 @@ public class PauseResumeCommands {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLostControl() {}
|
||||
public void onLostControl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public double priority() {
|
||||
@@ -169,5 +171,36 @@ public class PauseResumeCommands {
|
||||
);
|
||||
}
|
||||
};
|
||||
cancelCommand = new Command(baritone, "cancel", "stop") {
|
||||
@Override
|
||||
public void execute(String label, IArgConsumer args) throws CommandException {
|
||||
args.requireMax(0);
|
||||
if (paused[0]) {
|
||||
paused[0] = false;
|
||||
}
|
||||
baritone.getPathingBehavior().cancelEverything();
|
||||
logDirect("ok canceled");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<String> tabComplete(String label, IArgConsumer args) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getShortDesc() {
|
||||
return "Cancel what Baritone is currently doing";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getLongDesc() {
|
||||
return Arrays.asList(
|
||||
"The cancel command tells Baritone to stop whatever it's currently doing.",
|
||||
"",
|
||||
"Usage:",
|
||||
"> cancel"
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,13 @@ public class GotoCommand extends Command {
|
||||
|
||||
@Override
|
||||
public void execute(String label, IArgConsumer args) throws CommandException {
|
||||
if (args.peekDatatypeOrNull(RelativeCoordinate.INSTANCE) != null) { // if we have a numeric first argument...
|
||||
// If we have a numeric first argument, then parse arguments as coordinates.
|
||||
// Note: There is no reason to want to go where you're already at so there
|
||||
// is no need to handle the case of empty arguments.
|
||||
if (args.peekDatatypeOrNull(RelativeCoordinate.INSTANCE) != null) {
|
||||
args.requireMax(3);
|
||||
BetterBlockPos origin = baritone.getPlayerContext().playerFeet();
|
||||
Goal goal = args.getDatatypePostOrNull(RelativeGoal.INSTANCE, origin);
|
||||
Goal goal = args.getDatatypePost(RelativeGoal.INSTANCE, origin);
|
||||
logDirect(String.format("Going to: %s", goal.toString()));
|
||||
baritone.getCustomGoalProcess().setGoalAndPath(goal);
|
||||
return;
|
||||
|
||||
@@ -42,6 +42,7 @@ import net.minecraft.util.EnumFacing;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.RayTraceResult;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import net.minecraft.world.IBlockReader;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -143,7 +144,7 @@ public interface MovementHelper extends ActionCosts, Helper {
|
||||
// every block that overrides isPassable with anything more complicated than a "return true;" or "return false;"
|
||||
// has already been accounted for above
|
||||
// therefore it's safe to not construct a blockpos from our x, y, z ints and instead just pass null
|
||||
return state.allowsMovement(null, BlockPos.ORIGIN, PathType.LAND); // workaround for future compatibility =P
|
||||
return state.allowsMovement(bsi.access, BlockPos.ORIGIN, PathType.LAND); // workaround for future compatibility =P
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,10 +158,18 @@ public interface MovementHelper extends ActionCosts, Helper {
|
||||
* @return Whether or not the block at the specified position
|
||||
*/
|
||||
static boolean fullyPassable(CalculationContext context, int x, int y, int z) {
|
||||
return fullyPassable(context.get(x, y, z));
|
||||
return fullyPassable(
|
||||
context.bsi.access,
|
||||
context.bsi.isPassableBlockPos.setPos(x, y, z),
|
||||
context.bsi.get0(x, y, z)
|
||||
);
|
||||
}
|
||||
|
||||
static boolean fullyPassable(IBlockState state) {
|
||||
static boolean fullyPassable(IPlayerContext ctx, BlockPos pos) {
|
||||
return fullyPassable(ctx.world(), pos, ctx.world().getBlockState(pos));
|
||||
}
|
||||
|
||||
static boolean fullyPassable(IBlockReader access, BlockPos pos, IBlockState state) {
|
||||
Block block = state.getBlock();
|
||||
if (block instanceof BlockAir) { // early return for most common case
|
||||
return true;
|
||||
@@ -183,7 +192,7 @@ public interface MovementHelper extends ActionCosts, Helper {
|
||||
return false;
|
||||
}
|
||||
// door, fence gate, liquid, trapdoor have been accounted for, nothing else uses the world or pos parameters
|
||||
return state.allowsMovement(null, null, PathType.LAND);
|
||||
return state.allowsMovement(access, pos, PathType.LAND);
|
||||
}
|
||||
|
||||
static boolean isReplaceable(int x, int y, int z, IBlockState state, BlockStateInterface bsi) {
|
||||
|
||||
@@ -115,7 +115,7 @@ public class MovementParkour extends Movement {
|
||||
return;
|
||||
}
|
||||
IBlockState destInto = context.bsi.get0(destX, y, destZ);
|
||||
if (!MovementHelper.fullyPassable(destInto)) {
|
||||
if (!MovementHelper.fullyPassable(context.bsi.access, context.bsi.isPassableBlockPos.setPos(destX, y, destZ), destInto)) {
|
||||
if (i <= 3 && context.allowParkourAscend && context.canSprint && MovementHelper.canWalkOn(context.bsi, destX, y, destZ, destInto) && checkOvershootSafety(context.bsi, destX + xDiff, y + 1, destZ + zDiff)) {
|
||||
res.x = destX;
|
||||
res.y = y + 1;
|
||||
|
||||
@@ -465,7 +465,7 @@ public class PathExecutor implements IPathExecutor, Helper {
|
||||
}
|
||||
for (int y = next.getDest().y; y <= movement.getSrc().y + 1; y++) {
|
||||
BlockPos chk = new BlockPos(next.getDest().x, y, next.getDest().z);
|
||||
if (!MovementHelper.fullyPassable(ctx.world().getBlockState(chk))) {
|
||||
if (!MovementHelper.fullyPassable(ctx, chk)) {
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
@@ -490,7 +490,7 @@ public class PathExecutor implements IPathExecutor, Helper {
|
||||
}
|
||||
// we are centered
|
||||
BlockPos headBonk = current.getSrc().subtract(current.getDirection()).up(2);
|
||||
if (MovementHelper.fullyPassable(ctx.world().getBlockState(headBonk))) {
|
||||
if (MovementHelper.fullyPassable(ctx, headBonk)) {
|
||||
return true;
|
||||
}
|
||||
// wait 0.3
|
||||
@@ -523,7 +523,7 @@ public class PathExecutor implements IPathExecutor, Helper {
|
||||
if (x == 1) {
|
||||
chk = chk.add(current.getDirection());
|
||||
}
|
||||
if (!MovementHelper.fullyPassable(ctx.world().getBlockState(chk))) {
|
||||
if (!MovementHelper.fullyPassable(ctx, chk)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ import baritone.api.process.IBuilderProcess;
|
||||
import baritone.api.process.PathingCommand;
|
||||
import baritone.api.process.PathingCommandType;
|
||||
import baritone.api.schematic.ISchematic;
|
||||
import baritone.api.utils.*;
|
||||
import baritone.api.utils.BetterBlockPos;
|
||||
import baritone.api.utils.RayTraceUtils;
|
||||
import baritone.api.utils.Rotation;
|
||||
import baritone.api.utils.RotationUtils;
|
||||
import baritone.api.utils.input.Input;
|
||||
import baritone.pathing.movement.CalculationContext;
|
||||
import baritone.pathing.movement.Movement;
|
||||
@@ -72,6 +75,7 @@ public final class BuilderProcess extends BaritoneProcessHelper implements IBuil
|
||||
private int ticks;
|
||||
private boolean paused;
|
||||
private int layer;
|
||||
private int numRepeats;
|
||||
private List<IBlockState> approxPlaceable;
|
||||
|
||||
public BuilderProcess(Baritone baritone) {
|
||||
@@ -98,6 +102,7 @@ public final class BuilderProcess extends BaritoneProcessHelper implements IBuil
|
||||
this.origin = new Vec3i(x, y, z);
|
||||
this.paused = false;
|
||||
this.layer = 0;
|
||||
this.numRepeats = 0;
|
||||
this.observedCompleted = new LongOpenHashSet();
|
||||
}
|
||||
|
||||
@@ -408,7 +413,9 @@ public final class BuilderProcess extends BaritoneProcessHelper implements IBuil
|
||||
return onTick(calcFailed, isSafeToCancel);
|
||||
}
|
||||
Vec3i repeat = Baritone.settings().buildRepeat.value;
|
||||
if (repeat.equals(new Vec3i(0, 0, 0))) {
|
||||
int max = Baritone.settings().buildRepeatCount.value;
|
||||
numRepeats++;
|
||||
if (repeat.equals(new Vec3i(0, 0, 0)) || (max != -1 && numRepeats >= max)) {
|
||||
logDirect("Done building");
|
||||
onLostControl();
|
||||
return null;
|
||||
@@ -748,6 +755,7 @@ public final class BuilderProcess extends BaritoneProcessHelper implements IBuil
|
||||
schematic = null;
|
||||
realSchematic = null;
|
||||
layer = 0;
|
||||
numRepeats = 0;
|
||||
paused = false;
|
||||
observedCompleted = null;
|
||||
}
|
||||
@@ -783,6 +791,12 @@ public final class BuilderProcess extends BaritoneProcessHelper implements IBuil
|
||||
return true;
|
||||
}
|
||||
// TODO more complicated comparison logic I guess
|
||||
if (desired.getBlock() instanceof BlockAir && Baritone.settings().buildIgnoreBlocks.value.contains(current.getBlock())) {
|
||||
return true;
|
||||
}
|
||||
if (!(current.getBlock() instanceof BlockAir) && Baritone.settings().buildIgnoreExisting.value) {
|
||||
return true;
|
||||
}
|
||||
return current.equals(desired);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.ChunkPos;
|
||||
import net.minecraft.world.IBlockReader;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
|
||||
@@ -42,6 +43,9 @@ public class BlockStateInterface {
|
||||
|
||||
private final Long2ObjectMap<Chunk> loadedChunks;
|
||||
private final WorldData worldData;
|
||||
protected final IBlockReader world;
|
||||
public final BlockPos.MutableBlockPos isPassableBlockPos;
|
||||
public final IBlockReader access;
|
||||
|
||||
private Chunk prev = null;
|
||||
private CachedRegion prevCached = null;
|
||||
@@ -59,6 +63,7 @@ public class BlockStateInterface {
|
||||
}
|
||||
|
||||
public BlockStateInterface(World world, WorldData worldData, boolean copyLoadedChunks) {
|
||||
this.world = world;
|
||||
this.worldData = worldData;
|
||||
Long2ObjectMap<Chunk> worldLoaded = ((IChunkProviderClient) world.getChunkProvider()).loadedChunks();
|
||||
if (copyLoadedChunks) {
|
||||
@@ -70,6 +75,8 @@ public class BlockStateInterface {
|
||||
if (!Minecraft.getInstance().isCallingFromMinecraftThread()) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
this.isPassableBlockPos = new BlockPos.MutableBlockPos();
|
||||
this.access = new BlockStateInterfaceAccessWrapper(this);
|
||||
}
|
||||
|
||||
public boolean worldContainsLoadedChunk(int blockX, int blockZ) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* This file is part of Baritone.
|
||||
*
|
||||
* Baritone is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Baritone is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package baritone.utils;
|
||||
|
||||
import net.minecraft.block.state.IBlockState;
|
||||
import net.minecraft.fluid.IFluidState;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.IBlockReader;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* @author Brady
|
||||
* @since 11/5/2019
|
||||
*/
|
||||
@SuppressWarnings("NullableProblems")
|
||||
public final class BlockStateInterfaceAccessWrapper implements IBlockReader {
|
||||
|
||||
private final BlockStateInterface bsi;
|
||||
|
||||
BlockStateInterfaceAccessWrapper(BlockStateInterface bsi) {
|
||||
this.bsi = bsi;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public TileEntity getTileEntity(BlockPos pos) {
|
||||
throw new UnsupportedOperationException("getTileEntity not supported by BlockStateInterfaceAccessWrapper");
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBlockState getBlockState(BlockPos pos) {
|
||||
// BlockStateInterface#get0(BlockPos) btfo!
|
||||
return this.bsi.get0(pos.getX(), pos.getY(), pos.getZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IFluidState getFluidState(BlockPos blockPos) {
|
||||
return getBlockState(blockPos).getFluidState();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user