From a1f8e9fea7d7ae7098c4256dab5defdc87d23349 Mon Sep 17 00:00:00 2001 From: Misode Date: Wed, 20 Dec 2023 05:18:09 +0100 Subject: [PATCH 01/13] Add initial fake players --- build.gradle | 2 + .../io/github/misode/packtest/PackTest.java | 2 + .../packtest/commands/PlayerCommand.java | 74 +++++++++++++++++++ .../packtest/fake/FakeClientConnection.java | 21 ++++++ .../misode/packtest/fake/FakePlayer.java | 62 ++++++++++++++++ .../packtest/mixin/PlayerListMixin.java | 23 ++++++ src/main/resources/fabric.mod.json | 1 + src/main/resources/packtest.accesswidener | 3 + src/main/resources/packtest.mixins.json | 51 ++++++------- 9 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 src/main/java/io/github/misode/packtest/commands/PlayerCommand.java create mode 100644 src/main/java/io/github/misode/packtest/fake/FakeClientConnection.java create mode 100644 src/main/java/io/github/misode/packtest/fake/FakePlayer.java create mode 100644 src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java create mode 100644 src/main/resources/packtest.accesswidener diff --git a/build.gradle b/build.gradle index af52b53..383a020 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,8 @@ repositories { } loom { + accessWidenerPath = file("src/main/resources/packtest.accesswidener") + splitEnvironmentSourceSets() mods { diff --git a/src/main/java/io/github/misode/packtest/PackTest.java b/src/main/java/io/github/misode/packtest/PackTest.java index ac5e170..b5d6982 100644 --- a/src/main/java/io/github/misode/packtest/PackTest.java +++ b/src/main/java/io/github/misode/packtest/PackTest.java @@ -3,6 +3,7 @@ import io.github.misode.packtest.commands.AssertCommand; import io.github.misode.packtest.commands.FailCommand; import io.github.misode.packtest.commands.SucceedCommand; +import io.github.misode.packtest.commands.PlayerCommand; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.minecraft.core.BlockPos; @@ -30,6 +31,7 @@ public void onInitialize() { CommandRegistrationCallback.EVENT.register((dispatcher, buildContext, environment) -> { AssertCommand.register(dispatcher, buildContext); FailCommand.register(dispatcher); + PlayerCommand.register(dispatcher); SucceedCommand.register(dispatcher, buildContext); }); } diff --git a/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java b/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java new file mode 100644 index 0000000..f85742b --- /dev/null +++ b/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java @@ -0,0 +1,74 @@ +package io.github.misode.packtest.commands; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import io.github.misode.packtest.fake.FakePlayer; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; + +import java.util.Optional; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class PlayerCommand { + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(literal("player") + .then(literal("spawn") + .executes(PlayerCommand::spawnRandomName) + .then(argument("player", StringArgumentType.word()) + .executes(PlayerCommand::spawnWithName))) + ); + } + + private static Optional getPlayer(CommandContext ctx) { + String playerName = StringArgumentType.getString(ctx, "player"); + return getPlayer(playerName, ctx); + } + + private static Optional getPlayer(String playerName, CommandContext ctx) { + MinecraftServer server = ctx.getSource().getServer(); + ServerPlayer player = server.getPlayerList().getPlayerByName(playerName); + if (player instanceof FakePlayer fakePlayer) { + return Optional.of(fakePlayer); + } + return Optional.empty(); + } + + private static int spawnRandomName(CommandContext ctx) { + int tries = 0; + while (tries++ < 10) { + RandomSource random = ctx.getSource().getLevel().getRandom(); + String playerName = "Tester" + random.nextInt(100, 1000); + if (getPlayer(playerName, ctx).isEmpty()) { + return spawn(playerName, ctx); + } + } + ctx.getSource().sendFailure(Component.literal("Failed to spawn player with a random name")); + return 0; + } + + private static int spawnWithName(CommandContext ctx) { + String playerName = StringArgumentType.getString(ctx, "player"); + return spawn(playerName, ctx); + } + + private static int spawn(String playerName, CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + MinecraftServer server = source.getServer(); + if (getPlayer(playerName, ctx).isPresent()) { + source.sendFailure(Component.literal("Player " + playerName + " is already logged on")); + return 0; + } + ResourceKey dimension = source.getLevel().dimension(); + FakePlayer.create(playerName, server, dimension, source.getPosition()); + return 1; + } +} diff --git a/src/main/java/io/github/misode/packtest/fake/FakeClientConnection.java b/src/main/java/io/github/misode/packtest/fake/FakeClientConnection.java new file mode 100644 index 0000000..c67f2a7 --- /dev/null +++ b/src/main/java/io/github/misode/packtest/fake/FakeClientConnection.java @@ -0,0 +1,21 @@ +package io.github.misode.packtest.fake; + +import net.minecraft.network.Connection; +import net.minecraft.network.PacketListener; +import net.minecraft.network.protocol.PacketFlow; + +public class FakeClientConnection extends Connection { + + public FakeClientConnection(PacketFlow flow) { + super(flow); + } + + @Override + public void setReadOnly() {} + + @Override + public void handleDisconnection() {} + + @Override + public void setListener(PacketListener packetListener) {} +} diff --git a/src/main/java/io/github/misode/packtest/fake/FakePlayer.java b/src/main/java/io/github/misode/packtest/fake/FakePlayer.java new file mode 100644 index 0000000..c00e03e --- /dev/null +++ b/src/main/java/io/github/misode/packtest/fake/FakePlayer.java @@ -0,0 +1,62 @@ +package io.github.misode.packtest.fake; + +import com.mojang.authlib.GameProfile; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.players.GameProfileCache; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.SkullBlockEntity; +import net.minecraft.world.phys.Vec3; + +/** + * Heavily inspired by Carpet + */ +public class FakePlayer extends ServerPlayer { + public Runnable fixStartingPosition = () -> {}; + + public static void create(String username, MinecraftServer server, ResourceKey dimensionId, Vec3 pos) { + ServerLevel level = server.getLevel(dimensionId); + GameProfileCache.setUsesAuthentication(false); + GameProfile gameProfile; + try { + var profileCache = server.getProfileCache(); + gameProfile = profileCache == null ? null : profileCache.get(username).orElse(null); + } + finally { + GameProfileCache.setUsesAuthentication(server.isDedicatedServer() && server.usesAuthentication()); + } + if (gameProfile == null) { + gameProfile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(username), username); + } + GameProfile finalProfile = gameProfile; + SkullBlockEntity.fetchGameProfile(gameProfile.getName()).thenAccept(p -> { + GameProfile profile = p.orElse(finalProfile); + FakePlayer instance = new FakePlayer(server, level, profile, ClientInformation.createDefault()); + instance.fixStartingPosition = () -> instance.moveTo(pos.x, pos.y, pos.z, 0, 0); + server.getPlayerList().placeNewPlayer( + new FakeClientConnection(PacketFlow.SERVERBOUND), + instance, + new CommonListenerCookie(profile, 0, instance.clientInformation())); + instance.teleportTo(level, pos.x, pos.y, pos.z, 0, 0); + instance.setHealth(20); + instance.unsetRemoved(); + instance.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); + server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte) (instance.yHeadRot * 256 / 360)), dimensionId); + server.getPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(instance), dimensionId); + instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte) 0x7f); + }); + } + + private FakePlayer(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli) { + super(server, level, profile, cli); + } +} diff --git a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java new file mode 100644 index 0000000..8602640 --- /dev/null +++ b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java @@ -0,0 +1,23 @@ +package io.github.misode.packtest.mixin; + +import io.github.misode.packtest.fake.FakePlayer; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Fixes starting position of fake players when they load in + */ +@Mixin(PlayerList.class) +public class PlayerListMixin { + @Inject(method = "load", at = @At(value = "RETURN", shift = At.Shift.BEFORE)) + private void fixStartingPos(ServerPlayer player, CallbackInfoReturnable cir) { + if (player instanceof FakePlayer) { + ((FakePlayer) player).fixStartingPosition.run(); + } + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 0a0b99f..966779f 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -19,6 +19,7 @@ "io.github.misode.packtest.PackTest" ] }, + "accessWidener" : "packtest.accesswidener", "mixins": [ "packtest.mixins.json" ], diff --git a/src/main/resources/packtest.accesswidener b/src/main/resources/packtest.accesswidener new file mode 100644 index 0000000..74b0c9e --- /dev/null +++ b/src/main/resources/packtest.accesswidener @@ -0,0 +1,3 @@ +accessWidener v1 named + +accessible method net/minecraft/world/level/block/entity/SkullBlockEntity fetchGameProfile (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture; diff --git a/src/main/resources/packtest.mixins.json b/src/main/resources/packtest.mixins.json index 1691a07..dc601d8 100644 --- a/src/main/resources/packtest.mixins.json +++ b/src/main/resources/packtest.mixins.json @@ -1,28 +1,29 @@ { - "required": true, - "package": "io.github.misode.packtest.mixin", - "compatibilityLevel": "JAVA_17", - "mixins": [ - "ArgumentTypeInfosMixin", - "BlockPredicateArgumentBlockMixin", - "BlockPredicateArgumentMixin", - "BlockPredicateArgumentTagMixin", - "CommandsMixin", - "EntityArgumentMixin", - "EntitySelectorMixin", - "GameTestHelperMixin", - "GameTestInfoMixin", - "GameTestRegistryMixin", - "GameTestServerMixin", - "LogTestReporterMixin", - "MinecraftServerMixin", - "ReloadableServerResourcesMixin", - "TestCommandMixin" - ], - "server": [ - "server.MainMixin" - ], - "injectors": { - "defaultRequire": 1 + "required": true, + "package": "io.github.misode.packtest.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "ArgumentTypeInfosMixin", + "BlockPredicateArgumentBlockMixin", + "BlockPredicateArgumentMixin", + "BlockPredicateArgumentTagMixin", + "CommandsMixin", + "EntityArgumentMixin", + "EntitySelectorMixin", + "GameTestHelperMixin", + "GameTestInfoMixin", + "GameTestRegistryMixin", + "GameTestServerMixin", + "LogTestReporterMixin", + "MinecraftServerMixin", + "PlayerListMixin", + "ReloadableServerResourcesMixin", + "TestCommandMixin" + ], + "server": [ + "server.MainMixin" + ], + "injectors": { + "defaultRequire": 1 } } From f78bf0df3e0a50200fbad696e1f887ac2aa50c89 Mon Sep 17 00:00:00 2001 From: Misode Date: Thu, 21 Dec 2023 04:27:29 +0100 Subject: [PATCH 02/13] Add player respawn and leave commands --- .../packtest/commands/PlayerCommand.java | 44 ++++++++++- .../misode/packtest/fake/FakePlayer.java | 79 +++++++++++++------ .../packtest/mixin/PlayerListMixin.java | 19 ++++- 3 files changed, 114 insertions(+), 28 deletions(-) diff --git a/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java b/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java index f85742b..2822d14 100644 --- a/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java +++ b/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java @@ -3,16 +3,22 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; import io.github.misode.packtest.fake.FakePlayer; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; import net.minecraft.util.RandomSource; import net.minecraft.world.level.Level; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import static net.minecraft.commands.Commands.argument; import static net.minecraft.commands.Commands.literal; @@ -24,13 +30,31 @@ public static void register(CommandDispatcher dispatcher) { .then(literal("spawn") .executes(PlayerCommand::spawnRandomName) .then(argument("player", StringArgumentType.word()) - .executes(PlayerCommand::spawnWithName))) + .executes(PlayerCommand::spawnFixedName))) + .then(literal("leave") + .then(argument("player", StringArgumentType.word()) + .suggests(PlayerCommand::listFakePlayers) + .executes(PlayerCommand::leave))) + .then(literal("respawn") + .then(argument("player", StringArgumentType.word()) + .suggests(PlayerCommand::listFakePlayers) + .executes(PlayerCommand::respawn))) ); } - private static Optional getPlayer(CommandContext ctx) { + private static CompletableFuture listFakePlayers(CommandContext ctx, SuggestionsBuilder builder) { + PlayerList playerList = ctx.getSource().getServer().getPlayerList(); + playerList.getPlayers().forEach(player -> { + if (player instanceof FakePlayer) { + builder.suggest(player.getName().getString()); + } + }); + return builder.buildFuture(); + } + + private static FakePlayer getPlayer(CommandContext ctx) throws CommandSyntaxException { String playerName = StringArgumentType.getString(ctx, "player"); - return getPlayer(playerName, ctx); + return getPlayer(playerName, ctx).orElseThrow(() -> new SimpleCommandExceptionType(() -> "Fake player " + playerName + " does not exist").create()); } private static Optional getPlayer(String playerName, CommandContext ctx) { @@ -55,7 +79,7 @@ private static int spawnRandomName(CommandContext ctx) { return 0; } - private static int spawnWithName(CommandContext ctx) { + private static int spawnFixedName(CommandContext ctx) { String playerName = StringArgumentType.getString(ctx, "player"); return spawn(playerName, ctx); } @@ -71,4 +95,16 @@ private static int spawn(String playerName, CommandContext c FakePlayer.create(playerName, server, dimension, source.getPosition()); return 1; } + + private static int leave(CommandContext ctx) throws CommandSyntaxException { + FakePlayer player = getPlayer(ctx); + player.leave(Component.literal("Forced to leave")); + return 1; + } + + private static int respawn(CommandContext ctx) throws CommandSyntaxException { + FakePlayer player = getPlayer(ctx); + player.respawn(ctx.getSource().getPosition()); + return 1; + } } diff --git a/src/main/java/io/github/misode/packtest/fake/FakePlayer.java b/src/main/java/io/github/misode/packtest/fake/FakePlayer.java index c00e03e..284bf35 100644 --- a/src/main/java/io/github/misode/packtest/fake/FakePlayer.java +++ b/src/main/java/io/github/misode/packtest/fake/FakePlayer.java @@ -2,6 +2,7 @@ import com.mojang.authlib.GameProfile; import net.minecraft.core.UUIDUtil; +import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; @@ -12,10 +13,14 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.CommonListenerCookie; import net.minecraft.server.players.GameProfileCache; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.GameType; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.entity.SkullBlockEntity; import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; /** * Heavily inspired by Carpet @@ -26,37 +31,65 @@ public class FakePlayer extends ServerPlayer { public static void create(String username, MinecraftServer server, ResourceKey dimensionId, Vec3 pos) { ServerLevel level = server.getLevel(dimensionId); GameProfileCache.setUsesAuthentication(false); - GameProfile gameProfile; + GameProfile profile; try { var profileCache = server.getProfileCache(); - gameProfile = profileCache == null ? null : profileCache.get(username).orElse(null); + profile = profileCache == null ? null : profileCache.get(username).orElse(null); } finally { GameProfileCache.setUsesAuthentication(server.isDedicatedServer() && server.usesAuthentication()); } - if (gameProfile == null) { - gameProfile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(username), username); + if (profile == null) { + profile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(username), username); } - GameProfile finalProfile = gameProfile; - SkullBlockEntity.fetchGameProfile(gameProfile.getName()).thenAccept(p -> { - GameProfile profile = p.orElse(finalProfile); - FakePlayer instance = new FakePlayer(server, level, profile, ClientInformation.createDefault()); - instance.fixStartingPosition = () -> instance.moveTo(pos.x, pos.y, pos.z, 0, 0); - server.getPlayerList().placeNewPlayer( - new FakeClientConnection(PacketFlow.SERVERBOUND), - instance, - new CommonListenerCookie(profile, 0, instance.clientInformation())); - instance.teleportTo(level, pos.x, pos.y, pos.z, 0, 0); - instance.setHealth(20); - instance.unsetRemoved(); - instance.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); - server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte) (instance.yHeadRot * 256 / 360)), dimensionId); - server.getPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(instance), dimensionId); - instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte) 0x7f); - }); + FakePlayer instance = new FakePlayer(server, level, profile, ClientInformation.createDefault()); + instance.fixStartingPosition = () -> instance.moveTo(pos.x, pos.y, pos.z, 0, 0); + server.getPlayerList().placeNewPlayer( + new FakeClientConnection(PacketFlow.SERVERBOUND), + instance, + new CommonListenerCookie(profile, 0, instance.clientInformation())); + instance.teleportTo(level, pos.x, pos.y, pos.z, 0, 0); + instance.setHealth(20); + instance.unsetRemoved(); + instance.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); + server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte) (instance.yHeadRot * 256 / 360)), dimensionId); + server.getPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(instance), dimensionId); + instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte) 0x7f); } - private FakePlayer(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli) { + public FakePlayer(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli) { super(server, level, profile, cli); } + + public void leave(Component reason) { + server.getPlayerList().remove(this); + this.connection.onDisconnect(reason); + } + + public void respawn(Vec3 pos) { + ServerPlayer newPlayer = server.getPlayerList().respawn(this, false); + newPlayer.moveTo(pos.x, pos.y, pos.z, 0, 0); + } + + @Override + public void tick() { + if (Objects.requireNonNull(this.getServer()).getTickCount() % 10 == 0) { + this.connection.resetPosition(); + this.serverLevel().getChunkSource().move(this); + } + try { + super.tick(); + this.doTick(); + } catch (NullPointerException ignored) {} + } + + @Override + public void onEquipItem(final EquipmentSlot slot, final ItemStack previous, final ItemStack stack) { + if (!isUsingItem()) super.onEquipItem(slot, previous, stack); + } + + @Override + public @NotNull String getIpAddress() { + return "127.0.0.1"; + } } diff --git a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java index 8602640..c985fc4 100644 --- a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java +++ b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java @@ -1,7 +1,14 @@ package io.github.misode.packtest.mixin; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.authlib.GameProfile; import io.github.misode.packtest.fake.FakePlayer; import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.players.PlayerList; import org.spongepowered.asm.mixin.Mixin; @@ -10,7 +17,8 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; /** - * Fixes starting position of fake players when they load in + * Fixes starting position of fake players when they load in. + * Respawns fake players. */ @Mixin(PlayerList.class) public class PlayerListMixin { @@ -20,4 +28,13 @@ private void fixStartingPos(ServerPlayer player, CallbackInfoReturnable original, @Local(ordinal = 0) ServerPlayer player) { + if (player instanceof FakePlayer) { + return new FakePlayer(server, level, profile, cli); + } else { + return original.call(server, level, profile, cli); + } + } } From 575ba1f0c775e8b7431a22e5fcc11a8d6139557d Mon Sep 17 00:00:00 2001 From: Misode Date: Thu, 21 Dec 2023 06:49:20 +0100 Subject: [PATCH 03/13] Respawn fake players in their original spawn location --- .../packtest/commands/PlayerCommand.java | 2 +- .../misode/packtest/fake/FakePlayer.java | 9 ++++--- .../packtest/mixin/PlayerListMixin.java | 24 ++++++++++++++++++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java b/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java index 2822d14..2ca97b9 100644 --- a/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java +++ b/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java @@ -104,7 +104,7 @@ private static int leave(CommandContext ctx) throws CommandS private static int respawn(CommandContext ctx) throws CommandSyntaxException { FakePlayer player = getPlayer(ctx); - player.respawn(ctx.getSource().getPosition()); + player.respawn(); return 1; } } diff --git a/src/main/java/io/github/misode/packtest/fake/FakePlayer.java b/src/main/java/io/github/misode/packtest/fake/FakePlayer.java index 284bf35..6984fd7 100644 --- a/src/main/java/io/github/misode/packtest/fake/FakePlayer.java +++ b/src/main/java/io/github/misode/packtest/fake/FakePlayer.java @@ -1,6 +1,7 @@ package io.github.misode.packtest.fake; import com.mojang.authlib.GameProfile; +import net.minecraft.core.BlockPos; import net.minecraft.core.UUIDUtil; import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.PacketFlow; @@ -19,6 +20,7 @@ import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Objects; @@ -26,6 +28,7 @@ * Heavily inspired by Carpet */ public class FakePlayer extends ServerPlayer { + public @Nullable BlockPos origin = null; public Runnable fixStartingPosition = () -> {}; public static void create(String username, MinecraftServer server, ResourceKey dimensionId, Vec3 pos) { @@ -43,6 +46,7 @@ public static void create(String username, MinecraftServer server, ResourceKey instance.moveTo(pos.x, pos.y, pos.z, 0, 0); server.getPlayerList().placeNewPlayer( new FakeClientConnection(PacketFlow.SERVERBOUND), @@ -66,9 +70,8 @@ public void leave(Component reason) { this.connection.onDisconnect(reason); } - public void respawn(Vec3 pos) { - ServerPlayer newPlayer = server.getPlayerList().respawn(this, false); - newPlayer.moveTo(pos.x, pos.y, pos.z, 0, 0); + public void respawn() { + server.getPlayerList().respawn(this, false); } @Override diff --git a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java index c985fc4..3c8949b 100644 --- a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java +++ b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java @@ -5,20 +5,24 @@ import com.llamalad7.mixinextras.sugar.Local; import com.mojang.authlib.GameProfile; import io.github.misode.packtest.fake.FakePlayer; +import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ClientInformation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.players.PlayerList; +import net.minecraft.world.phys.Vec3; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import java.util.Optional; + /** * Fixes starting position of fake players when they load in. - * Respawns fake players. + * Respawns fake players and in the correct position. */ @Mixin(PlayerList.class) public class PlayerListMixin { @@ -37,4 +41,22 @@ private ServerPlayer createPlayer(MinecraftServer server, ServerLevel level, Gam return original.call(server, level, profile, cli); } } + + @WrapOperation(method = "respawn", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;getRespawnPosition()Lnet/minecraft/core/BlockPos;")) + private BlockPos getRespawnBlock(ServerPlayer player, Operation original) { + if (player instanceof FakePlayer fakePlayer && fakePlayer.origin != null) { + return fakePlayer.origin; + } else { + return original.call(player); + } + } + + @WrapOperation(method = "respawn", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;findRespawnPositionAndUseSpawnBlock(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;FZZ)Ljava/util/Optional;")) + private Optional getRespawnPos(ServerLevel level, BlockPos pos, float angle, boolean forced, boolean other, Operation> original, @Local(ordinal = 0) ServerPlayer player) { + if (player instanceof FakePlayer fakePlayer && fakePlayer.origin != null) { + return Optional.of(new Vec3(fakePlayer.origin.getX() + 0.5, fakePlayer.origin.getY(), fakePlayer.origin.getZ() + 0.5)); + } else { + return original.call(level, pos, angle, forced, other); + } + } } From b3e766758bee4ab3314cd92b77aa4c7dba0a4dbe Mon Sep 17 00:00:00 2001 From: Misode Date: Thu, 21 Dec 2023 15:56:25 +0100 Subject: [PATCH 04/13] Add player jump, sneak and sprint --- .../packtest/commands/PlayerCommand.java | 73 +++++++++++++++---- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java b/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java index 2ca97b9..525b5e0 100644 --- a/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java +++ b/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java @@ -1,12 +1,12 @@ package io.github.misode.packtest.commands; import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.BoolArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; -import com.mojang.brigadier.suggestion.Suggestions; -import com.mojang.brigadier.suggestion.SuggestionsBuilder; import io.github.misode.packtest.fake.FakePlayer; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; @@ -18,7 +18,6 @@ import net.minecraft.world.level.Level; import java.util.Optional; -import java.util.concurrent.CompletableFuture; import static net.minecraft.commands.Commands.argument; import static net.minecraft.commands.Commands.literal; @@ -32,24 +31,36 @@ public static void register(CommandDispatcher dispatcher) { .then(argument("player", StringArgumentType.word()) .executes(PlayerCommand::spawnFixedName))) .then(literal("leave") - .then(argument("player", StringArgumentType.word()) - .suggests(PlayerCommand::listFakePlayers) + .then(playerArgument() .executes(PlayerCommand::leave))) .then(literal("respawn") - .then(argument("player", StringArgumentType.word()) - .suggests(PlayerCommand::listFakePlayers) + .then(playerArgument() .executes(PlayerCommand::respawn))) + .then(literal("jump") + .then(playerArgument() + .executes(PlayerCommand::jump))) + .then(literal("sneak") + .then(playerArgument() + .then(argument("active", BoolArgumentType.bool()) + .executes(PlayerCommand::sneak)))) + .then(literal("sprint") + .then(playerArgument() + .then(argument("active", BoolArgumentType.bool()) + .executes(PlayerCommand::sprint)))) ); } - private static CompletableFuture listFakePlayers(CommandContext ctx, SuggestionsBuilder builder) { - PlayerList playerList = ctx.getSource().getServer().getPlayerList(); - playerList.getPlayers().forEach(player -> { - if (player instanceof FakePlayer) { - builder.suggest(player.getName().getString()); - } - }); - return builder.buildFuture(); + private static RequiredArgumentBuilder playerArgument() { + return argument("player", StringArgumentType.word()) + .suggests((ctx, builder) -> { + PlayerList playerList = ctx.getSource().getServer().getPlayerList(); + playerList.getPlayers().forEach(player -> { + if (player instanceof FakePlayer) { + builder.suggest(player.getName().getString()); + } + }); + return builder.buildFuture(); + }); } private static FakePlayer getPlayer(CommandContext ctx) throws CommandSyntaxException { @@ -107,4 +118,36 @@ private static int respawn(CommandContext ctx) throws Comman player.respawn(); return 1; } + + private static int jump(CommandContext ctx) throws CommandSyntaxException { + FakePlayer player = getPlayer(ctx); + if (player.onGround()) { + player.jumpFromGround(); + return 1; + } + ctx.getSource().sendFailure(Component.literal("Player is not on the ground")); + return 0; + } + + private static int sneak(CommandContext ctx) throws CommandSyntaxException { + FakePlayer player = getPlayer(ctx); + boolean toggle = BoolArgumentType.getBool(ctx, "active"); + if (player.isShiftKeyDown() != toggle) { + player.setShiftKeyDown(toggle); + return 1; + } + ctx.getSource().sendFailure(Component.literal(toggle ? "Player is already sneaking" : "Player is already not sneaking")); + return 0; + } + + private static int sprint(CommandContext ctx) throws CommandSyntaxException { + FakePlayer player = getPlayer(ctx); + boolean toggle = BoolArgumentType.getBool(ctx, "active"); + if (player.isSprinting() != toggle) { + player.setSprinting(toggle); + return 1; + } + ctx.getSource().sendFailure(Component.literal(toggle ? "Player is already sprinting" : "Player is already not sprinting")); + return 0; + } } From cc3ab210f1d9be6f183966652224b4e6044d556e Mon Sep 17 00:00:00 2001 From: Misode Date: Thu, 21 Dec 2023 16:47:07 +0100 Subject: [PATCH 05/13] Rename /player to /dummy --- .../io/github/misode/packtest/PackTest.java | 4 +- .../{PlayerCommand.java => DummyCommand.java} | 98 +++++++++---------- .../FakePlayer.java => dummy/Dummy.java} | 32 +++--- .../DummyClientConnection.java} | 6 +- .../packtest/mixin/GameTestRunnerMixin.java | 30 ++++++ .../packtest/mixin/PlayerListMixin.java | 22 ++--- src/main/resources/packtest.mixins.json | 1 + 7 files changed, 112 insertions(+), 81 deletions(-) rename src/main/java/io/github/misode/packtest/commands/{PlayerCommand.java => DummyCommand.java} (56%) rename src/main/java/io/github/misode/packtest/{fake/FakePlayer.java => dummy/Dummy.java} (75%) rename src/main/java/io/github/misode/packtest/{fake/FakeClientConnection.java => dummy/DummyClientConnection.java} (70%) create mode 100644 src/main/java/io/github/misode/packtest/mixin/GameTestRunnerMixin.java diff --git a/src/main/java/io/github/misode/packtest/PackTest.java b/src/main/java/io/github/misode/packtest/PackTest.java index b5d6982..c9a3f73 100644 --- a/src/main/java/io/github/misode/packtest/PackTest.java +++ b/src/main/java/io/github/misode/packtest/PackTest.java @@ -3,7 +3,7 @@ import io.github.misode.packtest.commands.AssertCommand; import io.github.misode.packtest.commands.FailCommand; import io.github.misode.packtest.commands.SucceedCommand; -import io.github.misode.packtest.commands.PlayerCommand; +import io.github.misode.packtest.commands.DummyCommand; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.minecraft.core.BlockPos; @@ -31,7 +31,7 @@ public void onInitialize() { CommandRegistrationCallback.EVENT.register((dispatcher, buildContext, environment) -> { AssertCommand.register(dispatcher, buildContext); FailCommand.register(dispatcher); - PlayerCommand.register(dispatcher); + DummyCommand.register(dispatcher); SucceedCommand.register(dispatcher, buildContext); }); } diff --git a/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java similarity index 56% rename from src/main/java/io/github/misode/packtest/commands/PlayerCommand.java rename to src/main/java/io/github/misode/packtest/commands/DummyCommand.java index 525b5e0..5f944f7 100644 --- a/src/main/java/io/github/misode/packtest/commands/PlayerCommand.java +++ b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java @@ -7,7 +7,7 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; -import io.github.misode.packtest.fake.FakePlayer; +import io.github.misode.packtest.dummy.Dummy; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; @@ -22,40 +22,40 @@ import static net.minecraft.commands.Commands.argument; import static net.minecraft.commands.Commands.literal; -public class PlayerCommand { +public class DummyCommand { public static void register(CommandDispatcher dispatcher) { - dispatcher.register(literal("player") + dispatcher.register(literal("dummy") .then(literal("spawn") - .executes(PlayerCommand::spawnRandomName) - .then(argument("player", StringArgumentType.word()) - .executes(PlayerCommand::spawnFixedName))) + .executes(DummyCommand::spawnRandomName) + .then(argument("name", StringArgumentType.word()) + .executes(DummyCommand::spawnFixedName))) .then(literal("leave") - .then(playerArgument() - .executes(PlayerCommand::leave))) + .then(dummyName() + .executes(DummyCommand::leave))) .then(literal("respawn") - .then(playerArgument() - .executes(PlayerCommand::respawn))) + .then(dummyName() + .executes(DummyCommand::respawn))) .then(literal("jump") - .then(playerArgument() - .executes(PlayerCommand::jump))) + .then(dummyName() + .executes(DummyCommand::jump))) .then(literal("sneak") - .then(playerArgument() + .then(dummyName() .then(argument("active", BoolArgumentType.bool()) - .executes(PlayerCommand::sneak)))) + .executes(DummyCommand::sneak)))) .then(literal("sprint") - .then(playerArgument() + .then(dummyName() .then(argument("active", BoolArgumentType.bool()) - .executes(PlayerCommand::sprint)))) + .executes(DummyCommand::sprint)))) ); } - private static RequiredArgumentBuilder playerArgument() { - return argument("player", StringArgumentType.word()) + private static RequiredArgumentBuilder dummyName() { + return argument("name", StringArgumentType.word()) .suggests((ctx, builder) -> { PlayerList playerList = ctx.getSource().getServer().getPlayerList(); playerList.getPlayers().forEach(player -> { - if (player instanceof FakePlayer) { + if (player instanceof Dummy) { builder.suggest(player.getName().getString()); } }); @@ -63,16 +63,16 @@ private static RequiredArgumentBuilder playerArgumen }); } - private static FakePlayer getPlayer(CommandContext ctx) throws CommandSyntaxException { - String playerName = StringArgumentType.getString(ctx, "player"); - return getPlayer(playerName, ctx).orElseThrow(() -> new SimpleCommandExceptionType(() -> "Fake player " + playerName + " does not exist").create()); + private static Dummy getDummy(CommandContext ctx) throws CommandSyntaxException { + String playerName = StringArgumentType.getString(ctx, "name"); + return getDummy(playerName, ctx).orElseThrow(() -> new SimpleCommandExceptionType(() -> "Dummy " + playerName + " does not exist").create()); } - private static Optional getPlayer(String playerName, CommandContext ctx) { + private static Optional getDummy(String playerName, CommandContext ctx) { MinecraftServer server = ctx.getSource().getServer(); ServerPlayer player = server.getPlayerList().getPlayerByName(playerName); - if (player instanceof FakePlayer fakePlayer) { - return Optional.of(fakePlayer); + if (player instanceof Dummy testPlayer) { + return Optional.of(testPlayer); } return Optional.empty(); } @@ -81,73 +81,73 @@ private static int spawnRandomName(CommandContext ctx) { int tries = 0; while (tries++ < 10) { RandomSource random = ctx.getSource().getLevel().getRandom(); - String playerName = "Tester" + random.nextInt(100, 1000); - if (getPlayer(playerName, ctx).isEmpty()) { + String playerName = "Dummy" + random.nextInt(100, 1000); + if (getDummy(playerName, ctx).isEmpty()) { return spawn(playerName, ctx); } } - ctx.getSource().sendFailure(Component.literal("Failed to spawn player with a random name")); + ctx.getSource().sendFailure(Component.literal("Failed to spawn dummy with a random name")); return 0; } private static int spawnFixedName(CommandContext ctx) { - String playerName = StringArgumentType.getString(ctx, "player"); + String playerName = StringArgumentType.getString(ctx, "name"); return spawn(playerName, ctx); } - private static int spawn(String playerName, CommandContext ctx) { + private static int spawn(String name, CommandContext ctx) { CommandSourceStack source = ctx.getSource(); MinecraftServer server = source.getServer(); - if (getPlayer(playerName, ctx).isPresent()) { - source.sendFailure(Component.literal("Player " + playerName + " is already logged on")); + if (getDummy(name, ctx).isPresent()) { + source.sendFailure(Component.literal("Dummy " + name + " is already logged on")); return 0; } ResourceKey dimension = source.getLevel().dimension(); - FakePlayer.create(playerName, server, dimension, source.getPosition()); + Dummy.create(name, server, dimension, source.getPosition()); return 1; } private static int leave(CommandContext ctx) throws CommandSyntaxException { - FakePlayer player = getPlayer(ctx); - player.leave(Component.literal("Forced to leave")); + Dummy dummy = getDummy(ctx); + dummy.leave(Component.literal("Forced to leave")); return 1; } private static int respawn(CommandContext ctx) throws CommandSyntaxException { - FakePlayer player = getPlayer(ctx); - player.respawn(); + Dummy dummy = getDummy(ctx); + dummy.respawn(); return 1; } private static int jump(CommandContext ctx) throws CommandSyntaxException { - FakePlayer player = getPlayer(ctx); - if (player.onGround()) { - player.jumpFromGround(); + Dummy dummy = getDummy(ctx); + if (dummy.onGround()) { + dummy.jumpFromGround(); return 1; } - ctx.getSource().sendFailure(Component.literal("Player is not on the ground")); + ctx.getSource().sendFailure(Component.literal("Dummy is not on the ground")); return 0; } private static int sneak(CommandContext ctx) throws CommandSyntaxException { - FakePlayer player = getPlayer(ctx); + Dummy dummy = getDummy(ctx); boolean toggle = BoolArgumentType.getBool(ctx, "active"); - if (player.isShiftKeyDown() != toggle) { - player.setShiftKeyDown(toggle); + if (dummy.isShiftKeyDown() != toggle) { + dummy.setShiftKeyDown(toggle); return 1; } - ctx.getSource().sendFailure(Component.literal(toggle ? "Player is already sneaking" : "Player is already not sneaking")); + ctx.getSource().sendFailure(Component.literal(toggle ? "Dummy is already sneaking" : "Dummy is already not sneaking")); return 0; } private static int sprint(CommandContext ctx) throws CommandSyntaxException { - FakePlayer player = getPlayer(ctx); + Dummy dummy = getDummy(ctx); boolean toggle = BoolArgumentType.getBool(ctx, "active"); - if (player.isSprinting() != toggle) { - player.setSprinting(toggle); + if (dummy.isSprinting() != toggle) { + dummy.setSprinting(toggle); return 1; } - ctx.getSource().sendFailure(Component.literal(toggle ? "Player is already sprinting" : "Player is already not sprinting")); + ctx.getSource().sendFailure(Component.literal(toggle ? "Dummy is already sprinting" : "Dummy is already not sprinting")); return 0; } } diff --git a/src/main/java/io/github/misode/packtest/fake/FakePlayer.java b/src/main/java/io/github/misode/packtest/dummy/Dummy.java similarity index 75% rename from src/main/java/io/github/misode/packtest/fake/FakePlayer.java rename to src/main/java/io/github/misode/packtest/dummy/Dummy.java index 6984fd7..7479cc6 100644 --- a/src/main/java/io/github/misode/packtest/fake/FakePlayer.java +++ b/src/main/java/io/github/misode/packtest/dummy/Dummy.java @@ -1,4 +1,4 @@ -package io.github.misode.packtest.fake; +package io.github.misode.packtest.dummy; import com.mojang.authlib.GameProfile; import net.minecraft.core.BlockPos; @@ -27,7 +27,7 @@ /** * Heavily inspired by Carpet */ -public class FakePlayer extends ServerPlayer { +public class Dummy extends ServerPlayer { public @Nullable BlockPos origin = null; public Runnable fixStartingPosition = () -> {}; @@ -45,23 +45,23 @@ public static void create(String username, MinecraftServer server, ResourceKey instance.moveTo(pos.x, pos.y, pos.z, 0, 0); + Dummy dummy = new Dummy(server, level, profile, ClientInformation.createDefault()); + dummy.origin = BlockPos.containing(pos); + dummy.fixStartingPosition = () -> dummy.moveTo(pos.x, pos.y, pos.z, 0, 0); server.getPlayerList().placeNewPlayer( - new FakeClientConnection(PacketFlow.SERVERBOUND), - instance, - new CommonListenerCookie(profile, 0, instance.clientInformation())); - instance.teleportTo(level, pos.x, pos.y, pos.z, 0, 0); - instance.setHealth(20); - instance.unsetRemoved(); - instance.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); - server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte) (instance.yHeadRot * 256 / 360)), dimensionId); - server.getPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(instance), dimensionId); - instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte) 0x7f); + new DummyClientConnection(PacketFlow.SERVERBOUND), + dummy, + new CommonListenerCookie(profile, 0, dummy.clientInformation())); + dummy.teleportTo(level, pos.x, pos.y, pos.z, 0, 0); + dummy.setHealth(20); + dummy.unsetRemoved(); + dummy.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); + server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(dummy, (byte) (dummy.yHeadRot * 256 / 360)), dimensionId); + server.getPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(dummy), dimensionId); + dummy.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte) 0x7f); } - public FakePlayer(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli) { + public Dummy(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli) { super(server, level, profile, cli); } diff --git a/src/main/java/io/github/misode/packtest/fake/FakeClientConnection.java b/src/main/java/io/github/misode/packtest/dummy/DummyClientConnection.java similarity index 70% rename from src/main/java/io/github/misode/packtest/fake/FakeClientConnection.java rename to src/main/java/io/github/misode/packtest/dummy/DummyClientConnection.java index c67f2a7..e466aa7 100644 --- a/src/main/java/io/github/misode/packtest/fake/FakeClientConnection.java +++ b/src/main/java/io/github/misode/packtest/dummy/DummyClientConnection.java @@ -1,12 +1,12 @@ -package io.github.misode.packtest.fake; +package io.github.misode.packtest.dummy; import net.minecraft.network.Connection; import net.minecraft.network.PacketListener; import net.minecraft.network.protocol.PacketFlow; -public class FakeClientConnection extends Connection { +public class DummyClientConnection extends Connection { - public FakeClientConnection(PacketFlow flow) { + public DummyClientConnection(PacketFlow flow) { super(flow); } diff --git a/src/main/java/io/github/misode/packtest/mixin/GameTestRunnerMixin.java b/src/main/java/io/github/misode/packtest/mixin/GameTestRunnerMixin.java new file mode 100644 index 0000000..d062a23 --- /dev/null +++ b/src/main/java/io/github/misode/packtest/mixin/GameTestRunnerMixin.java @@ -0,0 +1,30 @@ +package io.github.misode.packtest.mixin; + +import io.github.misode.packtest.dummy.Dummy; +import net.minecraft.core.BlockPos; +import net.minecraft.gametest.framework.GameTestRunner; +import net.minecraft.gametest.framework.GameTestTicker; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +/** + * Remove all test players when running /test clearall. + */ +@Mixin(GameTestRunner.class) +public class GameTestRunnerMixin { + + @Inject(method = "clearAllTests", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos;betweenClosedStream(Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/BlockPos;)Ljava/util/stream/Stream;", shift = At.Shift.AFTER)) + private static void clearTestPlayers(ServerLevel level, BlockPos pos, GameTestTicker ticker, int radius, CallbackInfo ci) { + List testPlayers = level.getServer().getPlayerList().getPlayers().stream() + .filter(p -> p instanceof Dummy && p.distanceToSqr(pos.getCenter()) <= radius) + .map(p -> (Dummy)p) + .toList(); + testPlayers.forEach(p -> p.leave(Component.literal("Cleared tests"))); + } +} diff --git a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java index 3c8949b..bd2b013 100644 --- a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java +++ b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java @@ -4,7 +4,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; import com.mojang.authlib.GameProfile; -import io.github.misode.packtest.fake.FakePlayer; +import io.github.misode.packtest.dummy.Dummy; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.server.MinecraftServer; @@ -21,22 +21,22 @@ import java.util.Optional; /** - * Fixes starting position of fake players when they load in. - * Respawns fake players and in the correct position. + * Fixes starting position of test players when they load in. + * Respawns test players and in the correct position. */ @Mixin(PlayerList.class) public class PlayerListMixin { @Inject(method = "load", at = @At(value = "RETURN", shift = At.Shift.BEFORE)) private void fixStartingPos(ServerPlayer player, CallbackInfoReturnable cir) { - if (player instanceof FakePlayer) { - ((FakePlayer) player).fixStartingPosition.run(); + if (player instanceof Dummy) { + ((Dummy) player).fixStartingPosition.run(); } } @WrapOperation(method = "respawn", at = @At(value = "NEW", target = "(Lnet/minecraft/server/MinecraftServer;Lnet/minecraft/server/level/ServerLevel;Lcom/mojang/authlib/GameProfile;Lnet/minecraft/server/level/ClientInformation;)Lnet/minecraft/server/level/ServerPlayer;")) private ServerPlayer createPlayer(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli, Operation original, @Local(ordinal = 0) ServerPlayer player) { - if (player instanceof FakePlayer) { - return new FakePlayer(server, level, profile, cli); + if (player instanceof Dummy) { + return new Dummy(server, level, profile, cli); } else { return original.call(server, level, profile, cli); } @@ -44,8 +44,8 @@ private ServerPlayer createPlayer(MinecraftServer server, ServerLevel level, Gam @WrapOperation(method = "respawn", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;getRespawnPosition()Lnet/minecraft/core/BlockPos;")) private BlockPos getRespawnBlock(ServerPlayer player, Operation original) { - if (player instanceof FakePlayer fakePlayer && fakePlayer.origin != null) { - return fakePlayer.origin; + if (player instanceof Dummy testPlayer && testPlayer.origin != null) { + return testPlayer.origin; } else { return original.call(player); } @@ -53,8 +53,8 @@ private BlockPos getRespawnBlock(ServerPlayer player, Operation origin @WrapOperation(method = "respawn", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;findRespawnPositionAndUseSpawnBlock(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;FZZ)Ljava/util/Optional;")) private Optional getRespawnPos(ServerLevel level, BlockPos pos, float angle, boolean forced, boolean other, Operation> original, @Local(ordinal = 0) ServerPlayer player) { - if (player instanceof FakePlayer fakePlayer && fakePlayer.origin != null) { - return Optional.of(new Vec3(fakePlayer.origin.getX() + 0.5, fakePlayer.origin.getY(), fakePlayer.origin.getZ() + 0.5)); + if (player instanceof Dummy testPlayer && testPlayer.origin != null) { + return Optional.of(new Vec3(testPlayer.origin.getX() + 0.5, testPlayer.origin.getY(), testPlayer.origin.getZ() + 0.5)); } else { return original.call(level, pos, angle, forced, other); } diff --git a/src/main/resources/packtest.mixins.json b/src/main/resources/packtest.mixins.json index dc601d8..ef1d7a5 100644 --- a/src/main/resources/packtest.mixins.json +++ b/src/main/resources/packtest.mixins.json @@ -13,6 +13,7 @@ "GameTestHelperMixin", "GameTestInfoMixin", "GameTestRegistryMixin", + "GameTestRunnerMixin", "GameTestServerMixin", "LogTestReporterMixin", "MinecraftServerMixin", From be9951ec8f224725a56356e490cccd653a29722e Mon Sep 17 00:00:00 2001 From: Misode Date: Thu, 21 Dec 2023 17:30:00 +0100 Subject: [PATCH 06/13] Add drop, swap and selectslot --- .../packtest/commands/DummyCommand.java | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java index 5f944f7..7afd3ee 100644 --- a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java +++ b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java @@ -2,6 +2,7 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; @@ -15,6 +16,9 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.players.PlayerList; import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; import java.util.Optional; @@ -44,9 +48,21 @@ public static void register(CommandDispatcher dispatcher) { .then(argument("active", BoolArgumentType.bool()) .executes(DummyCommand::sneak)))) .then(literal("sprint") - .then(dummyName() - .then(argument("active", BoolArgumentType.bool()) - .executes(DummyCommand::sprint)))) + .then(dummyName() + .then(argument("active", BoolArgumentType.bool()) + .executes(DummyCommand::sprint)))) + .then(literal("drop") + .then(dummyName() + .executes(ctx -> dropMainhand(ctx, false)) + .then(literal("all") + .executes(ctx -> dropMainhand(ctx, true))))) + .then(literal("swap") + .then(dummyName() + .executes(DummyCommand::swap))) + .then(literal("selectslot") + .then(dummyName() + .then(argument("slot", IntegerArgumentType.integer(1, 9)) + .executes(DummyCommand::selectSlot)))) ); } @@ -150,4 +166,33 @@ private static int sprint(CommandContext ctx) throws Command ctx.getSource().sendFailure(Component.literal(toggle ? "Dummy is already sprinting" : "Dummy is already not sprinting")); return 0; } + + private static int dropMainhand(CommandContext ctx, boolean stack) throws CommandSyntaxException { + Dummy dummy = getDummy(ctx); + if (!dummy.getInventory().getSelected().is(Items.AIR)) { + dummy.drop(stack); + return 1; + } + ctx.getSource().sendFailure(Component.literal("Dummy is not holding an item in their mainhand")); + return 0; + } + + private static int swap(CommandContext ctx) throws CommandSyntaxException { + Dummy dummy = getDummy(ctx); + ItemStack offhandItem = dummy.getItemInHand(InteractionHand.OFF_HAND); + dummy.setItemInHand(InteractionHand.OFF_HAND, dummy.getItemInHand(InteractionHand.MAIN_HAND)); + dummy.setItemInHand(InteractionHand.MAIN_HAND, offhandItem); + return 1; + } + + private static int selectSlot(CommandContext ctx) throws CommandSyntaxException { + Dummy dummy = getDummy(ctx); + int slot = IntegerArgumentType.getInteger(ctx, "slot"); + if (dummy.getInventory().selected != slot - 1) { + dummy.getInventory().selected = slot - 1; + return 1; + } + ctx.getSource().sendFailure(Component.literal("Dummy already has slot " + slot + " selected")); + return 0; + } } From 6af4949a93d898e9e8784f862ac8009dc8a78b37 Mon Sep 17 00:00:00 2001 From: Misode Date: Fri, 22 Dec 2023 03:39:52 +0100 Subject: [PATCH 07/13] Add use and attack --- .../io/github/misode/packtest/PackTest.java | 12 ++- .../packtest/commands/DirectionArgument.java | 45 +++++++++++ .../packtest/commands/DummyCommand.java | 80 +++++++++++++++++++ 3 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/github/misode/packtest/commands/DirectionArgument.java diff --git a/src/main/java/io/github/misode/packtest/PackTest.java b/src/main/java/io/github/misode/packtest/PackTest.java index c9a3f73..7c1da75 100644 --- a/src/main/java/io/github/misode/packtest/PackTest.java +++ b/src/main/java/io/github/misode/packtest/PackTest.java @@ -1,13 +1,13 @@ package io.github.misode.packtest; -import io.github.misode.packtest.commands.AssertCommand; -import io.github.misode.packtest.commands.FailCommand; -import io.github.misode.packtest.commands.SucceedCommand; -import io.github.misode.packtest.commands.DummyCommand; +import io.github.misode.packtest.commands.*; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.ArgumentTypeRegistry; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.commands.synchronization.SingletonArgumentInfo; import net.minecraft.core.BlockPos; import net.minecraft.gametest.framework.GameTestServer; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.repository.PackRepository; import net.minecraft.world.level.storage.LevelStorageSource; import org.slf4j.Logger; @@ -28,6 +28,10 @@ public static boolean isAutoColoringEnabled() { @Override public void onInitialize() { + ArgumentTypeRegistry.registerArgumentType( + new ResourceLocation("packtest", "direction"), + DirectionArgument.class, + SingletonArgumentInfo.contextFree(DirectionArgument::direction)); CommandRegistrationCallback.EVENT.register((dispatcher, buildContext, environment) -> { AssertCommand.register(dispatcher, buildContext); FailCommand.register(dispatcher); diff --git a/src/main/java/io/github/misode/packtest/commands/DirectionArgument.java b/src/main/java/io/github/misode/packtest/commands/DirectionArgument.java new file mode 100644 index 0000000..a993808 --- /dev/null +++ b/src/main/java/io/github/misode/packtest/commands/DirectionArgument.java @@ -0,0 +1,45 @@ +package io.github.misode.packtest.commands; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +public class DirectionArgument implements ArgumentType { + private static final DynamicCommandExceptionType ERROR_UNKNOWN_DIRECTION = new DynamicCommandExceptionType( + dir -> Component.literal("Unknown direction " + dir) + ); + + public static DirectionArgument direction() { + return new DirectionArgument(); + } + + public static Direction getDirection(CommandContext ctx, String name) { + return ctx.getArgument(name, Direction.class); + } + + @Override + public Direction parse(StringReader reader) throws CommandSyntaxException { + String str = reader.readUnquotedString(); + Direction direction = Direction.byName(str); + if (direction == null) { + throw ERROR_UNKNOWN_DIRECTION.create(str); + } + return direction; + } + + @Override + public CompletableFuture listSuggestions(CommandContext ctx, SuggestionsBuilder builder) { + return SharedSuggestionProvider.suggest(Arrays.stream(Direction.values()).map(Direction::getName), builder); + } +} diff --git a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java index 7afd3ee..b2b93e3 100644 --- a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java +++ b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java @@ -10,6 +10,10 @@ import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import io.github.misode.packtest.dummy.Dummy; import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; @@ -17,9 +21,13 @@ import net.minecraft.server.players.PlayerList; import net.minecraft.util.RandomSource; import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; import java.util.Optional; @@ -63,6 +71,24 @@ public static void register(CommandDispatcher dispatcher) { .then(dummyName() .then(argument("slot", IntegerArgumentType.integer(1, 9)) .executes(DummyCommand::selectSlot)))) + .then(literal("use") + .then(dummyName() + .then(literal("item") + .executes(DummyCommand::useItem)) + .then(literal("block") + .then(argument("pos", Vec3Argument.vec3(false)) + .executes(ctx -> useBlock(ctx, Direction.UP)) + .then(argument("direction", DirectionArgument.direction()) + .executes(ctx -> useBlock(ctx, DirectionArgument.getDirection(ctx, "direction")))))) + .then(literal("entity") + .then(argument("entity", EntityArgument.entity()) + .executes(ctx -> useEntity(ctx, null)) + .then(argument("pos", Vec3Argument.vec3(false)) + .executes(ctx -> useEntity(ctx, Vec3Argument.getVec3(ctx, "pos")))))))) + .then(literal("attack") + .then(dummyName() + .then(argument("entity", EntityArgument.entity()) + .executes(DummyCommand::attackEntity)))) ); } @@ -195,4 +221,58 @@ private static int selectSlot(CommandContext ctx) throws Com ctx.getSource().sendFailure(Component.literal("Dummy already has slot " + slot + " selected")); return 0; } + + private static int useItem(CommandContext ctx) throws CommandSyntaxException { + Dummy dummy = getDummy(ctx); + for (InteractionHand hand : InteractionHand.values()) { + ItemStack handItem = dummy.getItemInHand(hand); + if (dummy.gameMode.useItem(dummy, dummy.level(), handItem, hand).consumesAction()) { + return 1; + } + } + ctx.getSource().sendFailure(Component.literal("Dummy cannot use that item")); + return 0; + } + + private static int useBlock(CommandContext ctx, Direction hitDirection) throws CommandSyntaxException { + Dummy dummy = getDummy(ctx); + Vec3 pos = Vec3Argument.getVec3(ctx, "pos"); + for (InteractionHand hand : InteractionHand.values()) { + ItemStack handItem = dummy.getItemInHand(hand); + BlockHitResult blockHit = new BlockHitResult(pos, hitDirection, BlockPos.containing(pos), false); + InteractionResult result = dummy.gameMode.useItemOn(dummy, dummy.serverLevel(), handItem, hand, blockHit); + if (result.consumesAction()) { + if (result.shouldSwing()) dummy.swing(hand); + return 1; + } + } + ctx.getSource().sendFailure(Component.literal("Dummy cannot interact with that block")); + return 0; + } + + private static int useEntity(CommandContext ctx, Vec3 pos) throws CommandSyntaxException { + Dummy dummy = getDummy(ctx); + Entity entity = EntityArgument.getEntity(ctx, "entity"); + if (pos == null) { + pos = entity.position(); + } + for (InteractionHand hand : InteractionHand.values()) { + if (entity.interactAt(dummy, pos, hand).consumesAction()) { + return 1; + } + if (dummy.interactOn(entity, hand).consumesAction()) { + return 1; + } + } + ctx.getSource().sendFailure(Component.literal("Dummy cannot interact with that entity")); + return 0; + } + + private static int attackEntity(CommandContext ctx) throws CommandSyntaxException { + Dummy dummy = getDummy(ctx); + Entity entity = EntityArgument.getEntity(ctx, "entity"); + dummy.attack(entity); + dummy.swing(InteractionHand.MAIN_HAND); + return 1; + } } From 90ecc68c38b0b0199f075929eb7221437ae92893 Mon Sep 17 00:00:00 2001 From: Misode Date: Sat, 23 Dec 2023 21:54:10 +0100 Subject: [PATCH 08/13] Add dummy directive + refactor dummy command exceptions --- .../misode/packtest/PackTestFunction.java | 50 +++++- .../packtest/commands/DummyCommand.java | 154 ++++++++++-------- .../github/misode/packtest/dummy/Dummy.java | 20 ++- .../packtest/mixin/GameTestHelperMixin.java | 3 + .../packtest/mixin/GameTestRunnerMixin.java | 30 ---- .../packtest/mixin/PlayerListMixin.java | 18 +- .../packtest/mixin/StructureUtilsMixin.java | 29 ++++ src/main/resources/packtest.mixins.json | 2 +- 8 files changed, 189 insertions(+), 117 deletions(-) delete mode 100644 src/main/java/io/github/misode/packtest/mixin/GameTestRunnerMixin.java create mode 100644 src/main/java/io/github/misode/packtest/mixin/StructureUtilsMixin.java diff --git a/src/main/java/io/github/misode/packtest/PackTestFunction.java b/src/main/java/io/github/misode/packtest/PackTestFunction.java index 0bb80d7..ab03398 100644 --- a/src/main/java/io/github/misode/packtest/PackTestFunction.java +++ b/src/main/java/io/github/misode/packtest/PackTestFunction.java @@ -1,10 +1,15 @@ package io.github.misode.packtest; import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.github.misode.packtest.dummy.Dummy; import net.minecraft.commands.*; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; import net.minecraft.commands.execution.ExecutionContext; import net.minecraft.commands.functions.CommandFunction; import net.minecraft.commands.functions.InstantiatedFunction; +import net.minecraft.core.BlockPos; import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.gametest.framework.StructureUtils; import net.minecraft.gametest.framework.TestFunction; @@ -24,7 +29,7 @@ import java.util.regex.Pattern; public class PackTestFunction { - private static final Pattern DIRECTIVE_PATTERN = Pattern.compile("#\\s*@(\\w+)(?:\\s+(\\S+))?"); + private static final Pattern DIRECTIVE_PATTERN = Pattern.compile("^#\\s*@(\\w+)(?:\\s+(.+))?$"); private static final String DEFAULT_BATCH = "packtestBatch"; private static final String DEFAULT_TEMPLATE = "packtest:empty"; private final ResourceLocation id; @@ -83,6 +88,21 @@ private int getTimeout() { return Optional.ofNullable(this.directives.get("timeout")).map(Integer::parseInt).orElse(100); } + private Optional getDummyPos(CommandSourceStack source) { + String dummyValue = this.directives.get("dummy"); + if (dummyValue == null) { + return Optional.empty(); + } + if (dummyValue.equals("true")) { + dummyValue = "~0.5 ~ ~0.5"; + } + try { + return Optional.of(Vec3Argument.vec3().parse(new StringReader(dummyValue)).getPosition(source)); + } catch (CommandSyntaxException e) { + return Optional.empty(); + } + } + private boolean isRequired() { return Optional.ofNullable(this.directives.get("optional")).map(s -> !Boolean.parseBoolean(s)).orElse(true); } @@ -115,19 +135,35 @@ private Consumer createTestBody(int permissionLevel, CommandDisp .withSuppressedOutput() .withCallback((success, result) -> hasFailed.set(!success)); + InstantiatedFunction instantiatedFn; try { - InstantiatedFunction instantiatedFn = function.instantiate(null, dispatcher, sourceStack); - Commands.executeCommandInContext(sourceStack, execution -> ExecutionContext.queueInitialFunctionCall( - execution, - instantiatedFn, - sourceStack, - CommandResultCallback.EMPTY)); + instantiatedFn = function.instantiate(null, dispatcher, sourceStack); } catch (FunctionInstantiationException e) { String message = e.messageComponent().getString(); helper.fail("Failed to instantiate test function: " + message); return; } + Vec3 dummyPos = this.getDummyPos(sourceStack).orElse(null); + Dummy dummy; + if (dummyPos != null) { + try { + PackTest.LOGGER.info("Directive position {} {}", dummyPos, helper.getLevel().getBlockState(BlockPos.containing(dummyPos.x, dummyPos.y, dummyPos.z))); + dummy = Dummy.createRandom(helper.getLevel().getServer(), helper.getLevel().dimension(), dummyPos); + dummy.setOnGround(true); // little hack because we know the dummy will be on the ground + sourceStack = sourceStack.withEntity(dummy); + } catch (IllegalArgumentException e) { + helper.fail("Failed to initialize test with dummy"); + } + } + + CommandSourceStack finalSourceStack = sourceStack; + Commands.executeCommandInContext(sourceStack, execution -> ExecutionContext.queueInitialFunctionCall( + execution, + instantiatedFn, + finalSourceStack, + CommandResultCallback.EMPTY)); + if (hasFailed.get()) { helper.fail("Test failed without a message"); } else if (!((PackTestHelper)helper).packtest$isFinalCheckAdded()) { diff --git a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java index b2b93e3..238e993 100644 --- a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java +++ b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java @@ -7,11 +7,14 @@ import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import io.github.misode.packtest.dummy.Dummy; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.commands.arguments.coordinates.Vec3Argument; +import net.minecraft.commands.arguments.selector.EntitySelector; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.network.chat.Component; @@ -19,23 +22,44 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.players.PlayerList; -import net.minecraft.util.RandomSource; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.Entity; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.Vec3; -import java.util.Optional; - import static net.minecraft.commands.Commands.argument; import static net.minecraft.commands.Commands.literal; public class DummyCommand { + private static final SimpleCommandExceptionType ERROR_DUMMY_NOT_FOUND = new SimpleCommandExceptionType( + Component.literal("No dummy was found") + ); + private static final SimpleCommandExceptionType ERROR_DUMMY_RANDOM = new SimpleCommandExceptionType( + Component.literal("Failed to spawn dummy with a random name") + ); + private static final DynamicCommandExceptionType ERROR_DUMMY_EXISTS = createError("is already logged on"); + private static final DynamicCommandExceptionType ERROR_PLAYER_EXISTS = createError("is already a player"); + private static final DynamicCommandExceptionType ERROR_NOT_ON_GROUND = createError("is not on the ground"); + private static final DynamicCommandExceptionType ERROR_SNEAKING = createError("is already sneaking"); + private static final DynamicCommandExceptionType ERROR_NOT_SNEAKING = createError("is already not sneaking"); + private static final DynamicCommandExceptionType ERROR_SPRINTING = createError("is already sprinting"); + private static final DynamicCommandExceptionType ERROR_NOT_SPRINTING = createError("is already not sprinting"); + private static final DynamicCommandExceptionType ERROR_NOT_HOLDING_ITEM = createError("is not holding an item in their mainhand"); + private static final Dynamic2CommandExceptionType ERROR_SLOT_SELECTED = new Dynamic2CommandExceptionType( + (name, slot) -> Component.literal("Dummy " + name + " already has slot " + slot + " selected") + ); + private static final DynamicCommandExceptionType ERROR_USE_ITEM = createError("cannot use that item"); + private static final DynamicCommandExceptionType ERROR_INTERACT_BLOCK = createError("cannot interact with that block"); + private static final DynamicCommandExceptionType ERROR_INTERACT_ENTITY = createError("cannot interact with that entity"); + + private static DynamicCommandExceptionType createError(String message) { + return new DynamicCommandExceptionType(name -> Component.literal("Dummy " + name + " " + message)); + } + public static void register(CommandDispatcher dispatcher) { dispatcher.register(literal("dummy") .then(literal("spawn") @@ -92,9 +116,10 @@ public static void register(CommandDispatcher dispatcher) { ); } - private static RequiredArgumentBuilder dummyName() { - return argument("name", StringArgumentType.word()) + private static RequiredArgumentBuilder dummyName() { + return argument("name", EntityArgument.entity()) .suggests((ctx, builder) -> { + builder.suggest("@s"); PlayerList playerList = ctx.getSource().getServer().getPlayerList(); playerList.getPlayers().forEach(player -> { if (player instanceof Dummy) { @@ -106,46 +131,44 @@ private static RequiredArgumentBuilder dummyName() { } private static Dummy getDummy(CommandContext ctx) throws CommandSyntaxException { - String playerName = StringArgumentType.getString(ctx, "name"); - return getDummy(playerName, ctx).orElseThrow(() -> new SimpleCommandExceptionType(() -> "Dummy " + playerName + " does not exist").create()); - } - - private static Optional getDummy(String playerName, CommandContext ctx) { - MinecraftServer server = ctx.getSource().getServer(); - ServerPlayer player = server.getPlayerList().getPlayerByName(playerName); - if (player instanceof Dummy testPlayer) { - return Optional.of(testPlayer); + EntitySelector selector = ctx.getArgument("name", EntitySelector.class); + ServerPlayer player; + try { + player = selector.findSinglePlayer(ctx.getSource()); + } catch (CommandSyntaxException e) { + throw ERROR_DUMMY_NOT_FOUND.create(); } - return Optional.empty(); - } - - private static int spawnRandomName(CommandContext ctx) { - int tries = 0; - while (tries++ < 10) { - RandomSource random = ctx.getSource().getLevel().getRandom(); - String playerName = "Dummy" + random.nextInt(100, 1000); - if (getDummy(playerName, ctx).isEmpty()) { - return spawn(playerName, ctx); - } + if (player instanceof Dummy dummy) { + return dummy; } - ctx.getSource().sendFailure(Component.literal("Failed to spawn dummy with a random name")); - return 0; + throw ERROR_DUMMY_NOT_FOUND.create(); } - private static int spawnFixedName(CommandContext ctx) { - String playerName = StringArgumentType.getString(ctx, "name"); - return spawn(playerName, ctx); + private static int spawnRandomName(CommandContext ctx) throws CommandSyntaxException { + CommandSourceStack source = ctx.getSource(); + MinecraftServer server = source.getServer(); + ResourceKey dimension = source.getLevel().dimension(); + try { + Dummy.createRandom(server, dimension, source.getPosition()); + } catch (IllegalArgumentException e) { + throw ERROR_DUMMY_RANDOM.create(); + } + return 1; } - private static int spawn(String name, CommandContext ctx) { + private static int spawnFixedName(CommandContext ctx) throws CommandSyntaxException { + String name = StringArgumentType.getString(ctx, "name"); CommandSourceStack source = ctx.getSource(); MinecraftServer server = source.getServer(); - if (getDummy(name, ctx).isPresent()) { - source.sendFailure(Component.literal("Dummy " + name + " is already logged on")); - return 0; + ServerPlayer player = server.getPlayerList().getPlayerByName(name); + if (player instanceof Dummy) { + throw ERROR_DUMMY_EXISTS.create(name); + } + if (player != null) { + throw ERROR_PLAYER_EXISTS.create(name); } ResourceKey dimension = source.getLevel().dimension(); - Dummy.create(name, server, dimension, source.getPosition()); + Dummy.createRandom(name, server, dimension, source.getPosition()); return 1; } @@ -163,44 +186,40 @@ private static int respawn(CommandContext ctx) throws Comman private static int jump(CommandContext ctx) throws CommandSyntaxException { Dummy dummy = getDummy(ctx); - if (dummy.onGround()) { - dummy.jumpFromGround(); - return 1; + if (!dummy.onGround()) { + throw ERROR_NOT_ON_GROUND.create(dummy.getUsername()); } - ctx.getSource().sendFailure(Component.literal("Dummy is not on the ground")); - return 0; + dummy.jumpFromGround(); + return 1; } private static int sneak(CommandContext ctx) throws CommandSyntaxException { Dummy dummy = getDummy(ctx); - boolean toggle = BoolArgumentType.getBool(ctx, "active"); - if (dummy.isShiftKeyDown() != toggle) { - dummy.setShiftKeyDown(toggle); - return 1; + boolean active = BoolArgumentType.getBool(ctx, "active"); + if (dummy.isShiftKeyDown() == active) { + throw (active ? ERROR_SNEAKING : ERROR_NOT_SNEAKING).create(dummy.getUsername()); } - ctx.getSource().sendFailure(Component.literal(toggle ? "Dummy is already sneaking" : "Dummy is already not sneaking")); - return 0; + dummy.setShiftKeyDown(active); + return 1; } private static int sprint(CommandContext ctx) throws CommandSyntaxException { Dummy dummy = getDummy(ctx); - boolean toggle = BoolArgumentType.getBool(ctx, "active"); - if (dummy.isSprinting() != toggle) { - dummy.setSprinting(toggle); - return 1; + boolean active = BoolArgumentType.getBool(ctx, "active"); + if (dummy.isSprinting() == active) { + throw (active ? ERROR_SPRINTING : ERROR_NOT_SPRINTING).create(dummy.getUsername()); } - ctx.getSource().sendFailure(Component.literal(toggle ? "Dummy is already sprinting" : "Dummy is already not sprinting")); - return 0; + dummy.setSprinting(active); + return 1; } private static int dropMainhand(CommandContext ctx, boolean stack) throws CommandSyntaxException { Dummy dummy = getDummy(ctx); - if (!dummy.getInventory().getSelected().is(Items.AIR)) { - dummy.drop(stack); - return 1; + if (dummy.getInventory().getSelected().isEmpty()) { + throw ERROR_NOT_HOLDING_ITEM.create(dummy.getUsername()); } - ctx.getSource().sendFailure(Component.literal("Dummy is not holding an item in their mainhand")); - return 0; + dummy.drop(stack); + return 1; } private static int swap(CommandContext ctx) throws CommandSyntaxException { @@ -214,12 +233,11 @@ private static int swap(CommandContext ctx) throws CommandSy private static int selectSlot(CommandContext ctx) throws CommandSyntaxException { Dummy dummy = getDummy(ctx); int slot = IntegerArgumentType.getInteger(ctx, "slot"); - if (dummy.getInventory().selected != slot - 1) { - dummy.getInventory().selected = slot - 1; - return 1; + if (dummy.getInventory().selected == slot - 1) { + throw ERROR_SLOT_SELECTED.create(dummy.getUsername(), slot); } - ctx.getSource().sendFailure(Component.literal("Dummy already has slot " + slot + " selected")); - return 0; + dummy.getInventory().selected = slot - 1; + return 1; } private static int useItem(CommandContext ctx) throws CommandSyntaxException { @@ -230,8 +248,7 @@ private static int useItem(CommandContext ctx) throws Comman return 1; } } - ctx.getSource().sendFailure(Component.literal("Dummy cannot use that item")); - return 0; + throw ERROR_USE_ITEM.create(dummy.getUsername()); } private static int useBlock(CommandContext ctx, Direction hitDirection) throws CommandSyntaxException { @@ -246,10 +263,10 @@ private static int useBlock(CommandContext ctx, Direction hi return 1; } } - ctx.getSource().sendFailure(Component.literal("Dummy cannot interact with that block")); - return 0; + throw ERROR_INTERACT_BLOCK.create(dummy.getUsername()); } + @SuppressWarnings("SameReturnValue") private static int useEntity(CommandContext ctx, Vec3 pos) throws CommandSyntaxException { Dummy dummy = getDummy(ctx); Entity entity = EntityArgument.getEntity(ctx, "entity"); @@ -264,8 +281,7 @@ private static int useEntity(CommandContext ctx, Vec3 pos) t return 1; } } - ctx.getSource().sendFailure(Component.literal("Dummy cannot interact with that entity")); - return 0; + throw ERROR_INTERACT_ENTITY.create(dummy.getUsername()); } private static int attackEntity(CommandContext ctx) throws CommandSyntaxException { diff --git a/src/main/java/io/github/misode/packtest/dummy/Dummy.java b/src/main/java/io/github/misode/packtest/dummy/Dummy.java index 7479cc6..72994ca 100644 --- a/src/main/java/io/github/misode/packtest/dummy/Dummy.java +++ b/src/main/java/io/github/misode/packtest/dummy/Dummy.java @@ -14,6 +14,7 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.CommonListenerCookie; import net.minecraft.server.players.GameProfileCache; +import net.minecraft.util.RandomSource; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.GameType; @@ -31,7 +32,19 @@ public class Dummy extends ServerPlayer { public @Nullable BlockPos origin = null; public Runnable fixStartingPosition = () -> {}; - public static void create(String username, MinecraftServer server, ResourceKey dimensionId, Vec3 pos) { + public static Dummy createRandom(MinecraftServer server, ResourceKey dimensionId, Vec3 pos) { + RandomSource random = server.overworld().getRandom(); + int tries = 0; + while (tries++ < 10) { + String playerName = "Dummy" + random.nextInt(100, 1000); + if (server.getPlayerList().getPlayerByName(playerName) == null) { + return createRandom(playerName, server, dimensionId, pos); + } + } + throw new IllegalStateException("Failed to spawn dummy with a random name"); + } + + public static Dummy createRandom(String username, MinecraftServer server, ResourceKey dimensionId, Vec3 pos) { ServerLevel level = server.getLevel(dimensionId); GameProfileCache.setUsesAuthentication(false); GameProfile profile; @@ -59,12 +72,17 @@ public static void create(String username, MinecraftServer server, ResourceKey/test clearall. - */ -@Mixin(GameTestRunner.class) -public class GameTestRunnerMixin { - - @Inject(method = "clearAllTests", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos;betweenClosedStream(Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/BlockPos;)Ljava/util/stream/Stream;", shift = At.Shift.AFTER)) - private static void clearTestPlayers(ServerLevel level, BlockPos pos, GameTestTicker ticker, int radius, CallbackInfo ci) { - List testPlayers = level.getServer().getPlayerList().getPlayers().stream() - .filter(p -> p instanceof Dummy && p.distanceToSqr(pos.getCenter()) <= radius) - .map(p -> (Dummy)p) - .toList(); - testPlayers.forEach(p -> p.leave(Component.literal("Cleared tests"))); - } -} diff --git a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java index bd2b013..aeb1c3e 100644 --- a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java +++ b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java @@ -21,20 +21,20 @@ import java.util.Optional; /** - * Fixes starting position of test players when they load in. - * Respawns test players and in the correct position. + * Fixes starting position of dummies when they load in. + * Respawns dummies and in the correct position. */ @Mixin(PlayerList.class) public class PlayerListMixin { @Inject(method = "load", at = @At(value = "RETURN", shift = At.Shift.BEFORE)) private void fixStartingPos(ServerPlayer player, CallbackInfoReturnable cir) { - if (player instanceof Dummy) { - ((Dummy) player).fixStartingPosition.run(); + if (player instanceof Dummy dummy) { + dummy.fixStartingPosition.run(); } } @WrapOperation(method = "respawn", at = @At(value = "NEW", target = "(Lnet/minecraft/server/MinecraftServer;Lnet/minecraft/server/level/ServerLevel;Lcom/mojang/authlib/GameProfile;Lnet/minecraft/server/level/ClientInformation;)Lnet/minecraft/server/level/ServerPlayer;")) - private ServerPlayer createPlayer(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli, Operation original, @Local(ordinal = 0) ServerPlayer player) { + private ServerPlayer createDummy(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli, Operation original, @Local(ordinal = 0) ServerPlayer player) { if (player instanceof Dummy) { return new Dummy(server, level, profile, cli); } else { @@ -44,8 +44,8 @@ private ServerPlayer createPlayer(MinecraftServer server, ServerLevel level, Gam @WrapOperation(method = "respawn", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;getRespawnPosition()Lnet/minecraft/core/BlockPos;")) private BlockPos getRespawnBlock(ServerPlayer player, Operation original) { - if (player instanceof Dummy testPlayer && testPlayer.origin != null) { - return testPlayer.origin; + if (player instanceof Dummy dummy && dummy.origin != null) { + return dummy.origin; } else { return original.call(player); } @@ -53,8 +53,8 @@ private BlockPos getRespawnBlock(ServerPlayer player, Operation origin @WrapOperation(method = "respawn", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;findRespawnPositionAndUseSpawnBlock(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;FZZ)Ljava/util/Optional;")) private Optional getRespawnPos(ServerLevel level, BlockPos pos, float angle, boolean forced, boolean other, Operation> original, @Local(ordinal = 0) ServerPlayer player) { - if (player instanceof Dummy testPlayer && testPlayer.origin != null) { - return Optional.of(new Vec3(testPlayer.origin.getX() + 0.5, testPlayer.origin.getY(), testPlayer.origin.getZ() + 0.5)); + if (player instanceof Dummy dummy && dummy.origin != null) { + return Optional.of(new Vec3(dummy.origin.getX() + 0.5, dummy.origin.getY(), dummy.origin.getZ() + 0.5)); } else { return original.call(level, pos, angle, forced, other); } diff --git a/src/main/java/io/github/misode/packtest/mixin/StructureUtilsMixin.java b/src/main/java/io/github/misode/packtest/mixin/StructureUtilsMixin.java new file mode 100644 index 0000000..9d15663 --- /dev/null +++ b/src/main/java/io/github/misode/packtest/mixin/StructureUtilsMixin.java @@ -0,0 +1,29 @@ +package io.github.misode.packtest.mixin; + +import io.github.misode.packtest.dummy.Dummy; +import net.minecraft.gametest.framework.StructureUtils; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +/** + * Remove all dummies when clearing space for tests + */ +@Mixin(StructureUtils.class) +public class StructureUtilsMixin { + + @Inject(method = "clearSpaceForStructure", at = @At("TAIL")) + private static void clearDummies(BoundingBox boundingBox, ServerLevel level, CallbackInfo ci) { + List testPlayers = level.getServer().getPlayerList().getPlayers().stream() + .filter(p -> p instanceof Dummy && boundingBox.isInside(p.blockPosition())) + .map(p -> (Dummy)p) + .toList(); + testPlayers.forEach(p -> p.leave(Component.literal("Cleared tests"))); + } +} diff --git a/src/main/resources/packtest.mixins.json b/src/main/resources/packtest.mixins.json index ef1d7a5..af4d634 100644 --- a/src/main/resources/packtest.mixins.json +++ b/src/main/resources/packtest.mixins.json @@ -13,12 +13,12 @@ "GameTestHelperMixin", "GameTestInfoMixin", "GameTestRegistryMixin", - "GameTestRunnerMixin", "GameTestServerMixin", "LogTestReporterMixin", "MinecraftServerMixin", "PlayerListMixin", "ReloadableServerResourcesMixin", + "StructureUtilsMixin", "TestCommandMixin" ], "server": [ From 7d02bff6b6137f9c38f8c9d7b547d2130c608e56 Mon Sep 17 00:00:00 2001 From: Misode Date: Sat, 23 Dec 2023 22:05:56 +0100 Subject: [PATCH 09/13] Switch order of dummy command syntax --- .../packtest/commands/DummyCommand.java | 93 +++++++------------ 1 file changed, 33 insertions(+), 60 deletions(-) diff --git a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java index 238e993..18d1308 100644 --- a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java +++ b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java @@ -38,9 +38,6 @@ public class DummyCommand { private static final SimpleCommandExceptionType ERROR_DUMMY_NOT_FOUND = new SimpleCommandExceptionType( Component.literal("No dummy was found") ); - private static final SimpleCommandExceptionType ERROR_DUMMY_RANDOM = new SimpleCommandExceptionType( - Component.literal("Failed to spawn dummy with a random name") - ); private static final DynamicCommandExceptionType ERROR_DUMMY_EXISTS = createError("is already logged on"); private static final DynamicCommandExceptionType ERROR_PLAYER_EXISTS = createError("is already a player"); private static final DynamicCommandExceptionType ERROR_NOT_ON_GROUND = createError("is not on the ground"); @@ -61,69 +58,57 @@ private static DynamicCommandExceptionType createError(String message) { } public static void register(CommandDispatcher dispatcher) { - dispatcher.register(literal("dummy") + dispatcher.register(literal("dummy").then(dummyName() .then(literal("spawn") - .executes(DummyCommand::spawnRandomName) - .then(argument("name", StringArgumentType.word()) - .executes(DummyCommand::spawnFixedName))) + .executes(DummyCommand::spawnFixedName)) .then(literal("leave") - .then(dummyName() - .executes(DummyCommand::leave))) + .executes(DummyCommand::leave)) .then(literal("respawn") - .then(dummyName() - .executes(DummyCommand::respawn))) + .executes(DummyCommand::respawn)) .then(literal("jump") - .then(dummyName() - .executes(DummyCommand::jump))) + .executes(DummyCommand::jump)) .then(literal("sneak") - .then(dummyName() - .then(argument("active", BoolArgumentType.bool()) - .executes(DummyCommand::sneak)))) + .then(argument("active", BoolArgumentType.bool()) + .executes(DummyCommand::sneak))) .then(literal("sprint") - .then(dummyName() - .then(argument("active", BoolArgumentType.bool()) - .executes(DummyCommand::sprint)))) + .then(argument("active", BoolArgumentType.bool()) + .executes(DummyCommand::sprint))) .then(literal("drop") - .then(dummyName() - .executes(ctx -> dropMainhand(ctx, false)) - .then(literal("all") - .executes(ctx -> dropMainhand(ctx, true))))) + .executes(ctx -> dropMainhand(ctx, false)) + .then(literal("all") + .executes(ctx -> dropMainhand(ctx, true)))) .then(literal("swap") - .then(dummyName() - .executes(DummyCommand::swap))) + .executes(DummyCommand::swap)) .then(literal("selectslot") - .then(dummyName() - .then(argument("slot", IntegerArgumentType.integer(1, 9)) - .executes(DummyCommand::selectSlot)))) + .then(argument("slot", IntegerArgumentType.integer(1, 9)) + .executes(DummyCommand::selectSlot))) .then(literal("use") - .then(dummyName() - .then(literal("item") - .executes(DummyCommand::useItem)) - .then(literal("block") + .then(literal("item") + .executes(DummyCommand::useItem)) + .then(literal("block") + .then(argument("pos", Vec3Argument.vec3(false)) + .executes(ctx -> useBlock(ctx, Direction.UP)) + .then(argument("direction", DirectionArgument.direction()) + .executes(ctx -> useBlock(ctx, DirectionArgument.getDirection(ctx, "direction")))))) + .then(literal("entity") + .then(argument("entity", EntityArgument.entity()) + .executes(ctx -> useEntity(ctx, null)) .then(argument("pos", Vec3Argument.vec3(false)) - .executes(ctx -> useBlock(ctx, Direction.UP)) - .then(argument("direction", DirectionArgument.direction()) - .executes(ctx -> useBlock(ctx, DirectionArgument.getDirection(ctx, "direction")))))) - .then(literal("entity") - .then(argument("entity", EntityArgument.entity()) - .executes(ctx -> useEntity(ctx, null)) - .then(argument("pos", Vec3Argument.vec3(false)) - .executes(ctx -> useEntity(ctx, Vec3Argument.getVec3(ctx, "pos")))))))) + .executes(ctx -> useEntity(ctx, Vec3Argument.getVec3(ctx, "pos"))))))) .then(literal("attack") - .then(dummyName() - .then(argument("entity", EntityArgument.entity()) - .executes(DummyCommand::attackEntity)))) - ); + .then(argument("entity", EntityArgument.entity()) + .executes(DummyCommand::attackEntity))) + )); } private static RequiredArgumentBuilder dummyName() { - return argument("name", EntityArgument.entity()) + return argument("dummy", EntityArgument.entity()) .suggests((ctx, builder) -> { builder.suggest("@s"); PlayerList playerList = ctx.getSource().getServer().getPlayerList(); playerList.getPlayers().forEach(player -> { - if (player instanceof Dummy) { - builder.suggest(player.getName().getString()); + if (player instanceof Dummy dummy) { + builder.suggest(dummy.getUsername()); } }); return builder.buildFuture(); @@ -131,7 +116,7 @@ private static RequiredArgumentBuilder dummy } private static Dummy getDummy(CommandContext ctx) throws CommandSyntaxException { - EntitySelector selector = ctx.getArgument("name", EntitySelector.class); + EntitySelector selector = ctx.getArgument("dummy", EntitySelector.class); ServerPlayer player; try { player = selector.findSinglePlayer(ctx.getSource()); @@ -144,18 +129,6 @@ private static Dummy getDummy(CommandContext ctx) throws Com throw ERROR_DUMMY_NOT_FOUND.create(); } - private static int spawnRandomName(CommandContext ctx) throws CommandSyntaxException { - CommandSourceStack source = ctx.getSource(); - MinecraftServer server = source.getServer(); - ResourceKey dimension = source.getLevel().dimension(); - try { - Dummy.createRandom(server, dimension, source.getPosition()); - } catch (IllegalArgumentException e) { - throw ERROR_DUMMY_RANDOM.create(); - } - return 1; - } - private static int spawnFixedName(CommandContext ctx) throws CommandSyntaxException { String name = StringArgumentType.getString(ctx, "name"); CommandSourceStack source = ctx.getSource(); From 324b16b1f5a0626459a5ad2cd4caffa7b475c94a Mon Sep 17 00:00:00 2001 From: Misode Date: Sat, 23 Dec 2023 22:33:30 +0100 Subject: [PATCH 10/13] Add dummy mine --- .../misode/packtest/commands/DummyCommand.java | 16 +++++++++++++++- .../io/github/misode/packtest/dummy/Dummy.java | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java index 18d1308..bc4d8e6 100644 --- a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java +++ b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java @@ -13,6 +13,7 @@ import io.github.misode.packtest.dummy.Dummy; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; import net.minecraft.commands.arguments.coordinates.Vec3Argument; import net.minecraft.commands.arguments.selector.EntitySelector; import net.minecraft.core.BlockPos; @@ -52,6 +53,7 @@ public class DummyCommand { private static final DynamicCommandExceptionType ERROR_USE_ITEM = createError("cannot use that item"); private static final DynamicCommandExceptionType ERROR_INTERACT_BLOCK = createError("cannot interact with that block"); private static final DynamicCommandExceptionType ERROR_INTERACT_ENTITY = createError("cannot interact with that entity"); + private static final DynamicCommandExceptionType ERROR_MINE_BLOCK = createError("failed to mine block"); private static DynamicCommandExceptionType createError(String message) { return new DynamicCommandExceptionType(name -> Component.literal("Dummy " + name + " " + message)); @@ -98,6 +100,9 @@ public static void register(CommandDispatcher dispatcher) { .then(literal("attack") .then(argument("entity", EntityArgument.entity()) .executes(DummyCommand::attackEntity))) + .then(literal("mine") + .then(argument("pos", BlockPosArgument.blockPos()) + .executes(DummyCommand::mineBlock))) )); } @@ -141,7 +146,7 @@ private static int spawnFixedName(CommandContext ctx) throws throw ERROR_PLAYER_EXISTS.create(name); } ResourceKey dimension = source.getLevel().dimension(); - Dummy.createRandom(name, server, dimension, source.getPosition()); + Dummy.create(name, server, dimension, source.getPosition()); return 1; } @@ -264,4 +269,13 @@ private static int attackEntity(CommandContext ctx) throws C dummy.swing(InteractionHand.MAIN_HAND); return 1; } + + private static int mineBlock(CommandContext ctx) throws CommandSyntaxException { + Dummy dummy = getDummy(ctx); + BlockPos pos = BlockPosArgument.getBlockPos(ctx, "pos"); + if (!dummy.gameMode.destroyBlock(pos)) { + throw ERROR_MINE_BLOCK.create(dummy.getUsername()); + } + return 1; + } } diff --git a/src/main/java/io/github/misode/packtest/dummy/Dummy.java b/src/main/java/io/github/misode/packtest/dummy/Dummy.java index 72994ca..6771846 100644 --- a/src/main/java/io/github/misode/packtest/dummy/Dummy.java +++ b/src/main/java/io/github/misode/packtest/dummy/Dummy.java @@ -38,13 +38,13 @@ public static Dummy createRandom(MinecraftServer server, ResourceKey dime while (tries++ < 10) { String playerName = "Dummy" + random.nextInt(100, 1000); if (server.getPlayerList().getPlayerByName(playerName) == null) { - return createRandom(playerName, server, dimensionId, pos); + return create(playerName, server, dimensionId, pos); } } throw new IllegalStateException("Failed to spawn dummy with a random name"); } - public static Dummy createRandom(String username, MinecraftServer server, ResourceKey dimensionId, Vec3 pos) { + public static Dummy create(String username, MinecraftServer server, ResourceKey dimensionId, Vec3 pos) { ServerLevel level = server.getLevel(dimensionId); GameProfileCache.setUsesAuthentication(false); GameProfile profile; From e13faec3b034d3da2cd1eea8b0ec2d2b5e0dbeaf Mon Sep 17 00:00:00 2001 From: Misode Date: Sat, 23 Dec 2023 22:49:20 +0100 Subject: [PATCH 11/13] Update documentation for dummies --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 14d5ab2..ab0ce62 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,22 @@ Tests can also be run automatically, for instance in a CI environment. When `-Dp * `assert `: if condition is unsuccessful, fails the current test and returns from the function * `assert not `: if condition is successful, fails the current test and returns from the function +### `dummy` +* `dummy spawn`: spawns a new dummy +* `dummy respawn`: respawns the dummy after it has been killed +* `dummy leave`: makes the dummy leave the server +* `dummy jump`: makes the dummy jump, if currently on ground +* `dummy sneak [true|false]`: makes the dummy hold shift or un-shift (not the same as currently crouching) +* `dummy sprint [true|false]`: makes the dummy sprint or un-sprint +* `dummy drop [all]`: makes the dummy drop the current mainhand, either one item or the entire stack +* `dummy swap`: makes the dummy swap its mainhand and offhand +* `dummy selectslot`: makes the dummy select a different hotbar slot +* `dummy use item`: makes the dummy use its hand item, either mainhand or offhand +* `dummy use block []`: makes the dummy use its hand item on a block position +* `dummy use entity `: makes the dummy use its hand item on an entity +* `dummy attack `: makes the dummy attack an entity with its mainhand +* `dummy mine `: makes the dummy mine a block + ## Conditions * `block `: checks if the block at the specified position matches the block predicate * `entity `: checks if the selector matches any entity (can also find entities outside the structure bounds) @@ -58,3 +74,4 @@ Tests can be customized by placing certain directives as special comments at the * `@batch`: the batch name for this test, defaults to `packtestBatch` * `@timeout`: an integer specifying the timeout, defaults to `100` * `@optional`: whether this test is allowed to fail, defaults to `false`, if there is no value after the directive it is considered as `true` +* `@dummy`: whether to spawn a dummy at the start of the test and set `@s` to this dummy, taking a position which defaults to `~0.5 ~ ~0.5` From a2fc1114be4d817d261fe7ae893d47bdf273cf44 Mon Sep 17 00:00:00 2001 From: Misode Date: Sun, 24 Dec 2023 01:07:07 +0100 Subject: [PATCH 12/13] Fix dummy spawn --- .../misode/packtest/PackTestPlayerName.java | 5 +++++ .../misode/packtest/commands/DummyCommand.java | 15 +++++++++++---- .../packtest/mixin/EntitySelectorMixin.java | 15 +++++++++++++-- 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 src/main/java/io/github/misode/packtest/PackTestPlayerName.java diff --git a/src/main/java/io/github/misode/packtest/PackTestPlayerName.java b/src/main/java/io/github/misode/packtest/PackTestPlayerName.java new file mode 100644 index 0000000..968758a --- /dev/null +++ b/src/main/java/io/github/misode/packtest/PackTestPlayerName.java @@ -0,0 +1,5 @@ +package io.github.misode.packtest; + +public interface PackTestPlayerName { + String packtest$getPlayerName(); +} diff --git a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java index bc4d8e6..6567f7a 100644 --- a/src/main/java/io/github/misode/packtest/commands/DummyCommand.java +++ b/src/main/java/io/github/misode/packtest/commands/DummyCommand.java @@ -3,13 +3,13 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.BoolArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType; -import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType; import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import io.github.misode.packtest.PackTestPlayerName; import io.github.misode.packtest.dummy.Dummy; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.arguments.EntityArgument; @@ -39,6 +39,9 @@ public class DummyCommand { private static final SimpleCommandExceptionType ERROR_DUMMY_NOT_FOUND = new SimpleCommandExceptionType( Component.literal("No dummy was found") ); + private static final SimpleCommandExceptionType ERROR_NO_NAME = new SimpleCommandExceptionType( + Component.literal("Cannot spawn dummy without a name") + ); private static final DynamicCommandExceptionType ERROR_DUMMY_EXISTS = createError("is already logged on"); private static final DynamicCommandExceptionType ERROR_PLAYER_EXISTS = createError("is already a player"); private static final DynamicCommandExceptionType ERROR_NOT_ON_GROUND = createError("is not on the ground"); @@ -62,7 +65,7 @@ private static DynamicCommandExceptionType createError(String message) { public static void register(CommandDispatcher dispatcher) { dispatcher.register(literal("dummy").then(dummyName() .then(literal("spawn") - .executes(DummyCommand::spawnFixedName)) + .executes(DummyCommand::spawn)) .then(literal("leave") .executes(DummyCommand::leave)) .then(literal("respawn") @@ -134,8 +137,12 @@ private static Dummy getDummy(CommandContext ctx) throws Com throw ERROR_DUMMY_NOT_FOUND.create(); } - private static int spawnFixedName(CommandContext ctx) throws CommandSyntaxException { - String name = StringArgumentType.getString(ctx, "name"); + private static int spawn(CommandContext ctx) throws CommandSyntaxException { + EntitySelector selector = ctx.getArgument("dummy", EntitySelector.class); + String name = ((PackTestPlayerName)selector).packtest$getPlayerName(); + if (name == null) { + throw ERROR_NO_NAME.create(); + } CommandSourceStack source = ctx.getSource(); MinecraftServer server = source.getServer(); ServerPlayer player = server.getPlayerList().getPlayerByName(name); diff --git a/src/main/java/io/github/misode/packtest/mixin/EntitySelectorMixin.java b/src/main/java/io/github/misode/packtest/mixin/EntitySelectorMixin.java index 70a6b51..8a0824e 100644 --- a/src/main/java/io/github/misode/packtest/mixin/EntitySelectorMixin.java +++ b/src/main/java/io/github/misode/packtest/mixin/EntitySelectorMixin.java @@ -1,17 +1,24 @@ package io.github.misode.packtest.mixin; import io.github.misode.packtest.PackTestArgumentSource; +import io.github.misode.packtest.PackTestPlayerName; import net.minecraft.commands.arguments.selector.EntitySelector; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; /** * Adds a new field for storing the original text of the entity selector */ @Mixin(EntitySelector.class) -public class EntitySelectorMixin implements PackTestArgumentSource { +public class EntitySelectorMixin implements PackTestArgumentSource, PackTestPlayerName { @Unique - public String packtestSource; + private String packtestSource; + + @Shadow + @Final + private String playerName; @Override public String packtest$getSource() { @@ -22,4 +29,8 @@ public class EntitySelectorMixin implements PackTestArgumentSource { public void packtest$setSource(String source) { this.packtestSource = source; } + + public String packtest$getPlayerName() { + return this.playerName; + } } From c6c7bd914d887966c9936c65e4e40f3401c56f34 Mon Sep 17 00:00:00 2001 From: Misode Date: Sun, 24 Dec 2023 03:14:15 +0100 Subject: [PATCH 13/13] fix bugs with dummy respawning + implement doImmediateRespawn --- .../github/misode/packtest/dummy/Dummy.java | 28 ++++++++++++----- .../packtest/mixin/PlayerListMixin.java | 31 ++++++------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/main/java/io/github/misode/packtest/dummy/Dummy.java b/src/main/java/io/github/misode/packtest/dummy/Dummy.java index 6771846..762f3e3 100644 --- a/src/main/java/io/github/misode/packtest/dummy/Dummy.java +++ b/src/main/java/io/github/misode/packtest/dummy/Dummy.java @@ -7,21 +7,24 @@ import net.minecraft.network.protocol.PacketFlow; import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.network.protocol.game.ServerboundClientCommandPacket; import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.TickTask; import net.minecraft.server.level.ClientInformation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.CommonListenerCookie; import net.minecraft.server.players.GameProfileCache; import net.minecraft.util.RandomSource; +import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameRules; import net.minecraft.world.level.GameType; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.Objects; @@ -29,8 +32,7 @@ * Heavily inspired by Carpet */ public class Dummy extends ServerPlayer { - public @Nullable BlockPos origin = null; - public Runnable fixStartingPosition = () -> {}; + public Vec3 originalSpawn; public static Dummy createRandom(MinecraftServer server, ResourceKey dimensionId, Vec3 pos) { RandomSource random = server.overworld().getRandom(); @@ -58,14 +60,13 @@ public static Dummy create(String username, MinecraftServer server, ResourceKey< if (profile == null) { profile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(username), username); } - Dummy dummy = new Dummy(server, level, profile, ClientInformation.createDefault()); - dummy.origin = BlockPos.containing(pos); - dummy.fixStartingPosition = () -> dummy.moveTo(pos.x, pos.y, pos.z, 0, 0); + Vec3 originalSpawn = Vec3.atBottomCenterOf(BlockPos.containing(pos)); + Dummy dummy = new Dummy(server, level, profile, ClientInformation.createDefault(), originalSpawn); server.getPlayerList().placeNewPlayer( new DummyClientConnection(PacketFlow.SERVERBOUND), dummy, new CommonListenerCookie(profile, 0, dummy.clientInformation())); - dummy.teleportTo(level, pos.x, pos.y, pos.z, 0, 0); + dummy.teleportTo(level, originalSpawn.x, originalSpawn.y, originalSpawn.z, 0, 0); dummy.setHealth(20); dummy.unsetRemoved(); dummy.gameMode.changeGameModeForPlayer(GameType.SURVIVAL); @@ -75,8 +76,9 @@ public static Dummy create(String username, MinecraftServer server, ResourceKey< return dummy; } - public Dummy(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli) { + public Dummy(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli, Vec3 originalSpawn) { super(server, level, profile, cli); + this.originalSpawn = originalSpawn; } public String getUsername() { @@ -104,6 +106,16 @@ public void tick() { } catch (NullPointerException ignored) {} } + @Override + public void die(DamageSource cause) { + super.die(cause); + if (this.serverLevel().getGameRules().getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN)) { + this.server.tell(new TickTask(this.server.getTickCount(), + () -> this.connection.handleClientCommand(new ServerboundClientCommandPacket(ServerboundClientCommandPacket.Action.PERFORM_RESPAWN)) + )); + } + } + @Override public void onEquipItem(final EquipmentSlot slot, final ItemStack previous, final ItemStack stack) { if (!isUsingItem()) super.onEquipItem(slot, previous, stack); diff --git a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java index aeb1c3e..4510830 100644 --- a/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java +++ b/src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java @@ -5,7 +5,6 @@ import com.llamalad7.mixinextras.sugar.Local; import com.mojang.authlib.GameProfile; import io.github.misode.packtest.dummy.Dummy; -import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ClientInformation; @@ -18,8 +17,6 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import java.util.Optional; - /** * Fixes starting position of dummies when they load in. * Respawns dummies and in the correct position. @@ -29,34 +26,26 @@ public class PlayerListMixin { @Inject(method = "load", at = @At(value = "RETURN", shift = At.Shift.BEFORE)) private void fixStartingPos(ServerPlayer player, CallbackInfoReturnable cir) { if (player instanceof Dummy dummy) { - dummy.fixStartingPosition.run(); + Vec3 pos = dummy.originalSpawn; + dummy.moveTo(pos.x, pos.y, pos.z, 0, 0); } } @WrapOperation(method = "respawn", at = @At(value = "NEW", target = "(Lnet/minecraft/server/MinecraftServer;Lnet/minecraft/server/level/ServerLevel;Lcom/mojang/authlib/GameProfile;Lnet/minecraft/server/level/ClientInformation;)Lnet/minecraft/server/level/ServerPlayer;")) private ServerPlayer createDummy(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli, Operation original, @Local(ordinal = 0) ServerPlayer player) { - if (player instanceof Dummy) { - return new Dummy(server, level, profile, cli); + if (player instanceof Dummy dummy) { + return new Dummy(server, level, profile, cli, dummy.originalSpawn); } else { return original.call(server, level, profile, cli); } } - @WrapOperation(method = "respawn", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;getRespawnPosition()Lnet/minecraft/core/BlockPos;")) - private BlockPos getRespawnBlock(ServerPlayer player, Operation original) { - if (player instanceof Dummy dummy && dummy.origin != null) { - return dummy.origin; - } else { - return original.call(player); - } - } - - @WrapOperation(method = "respawn", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;findRespawnPositionAndUseSpawnBlock(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;FZZ)Ljava/util/Optional;")) - private Optional getRespawnPos(ServerLevel level, BlockPos pos, float angle, boolean forced, boolean other, Operation> original, @Local(ordinal = 0) ServerPlayer player) { - if (player instanceof Dummy dummy && dummy.origin != null) { - return Optional.of(new Vec3(dummy.origin.getX() + 0.5, dummy.origin.getY(), dummy.origin.getZ() + 0.5)); - } else { - return original.call(level, pos, angle, forced, other); + @Inject(method = "respawn", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;restoreFrom(Lnet/minecraft/server/level/ServerPlayer;Z)V")) + private void teleportDummy(ServerPlayer serverPlayer, boolean bl, CallbackInfoReturnable cir, @Local(ordinal = 1) ServerPlayer player) { + if (player instanceof Dummy dummy) { + Vec3 pos = dummy.originalSpawn; + dummy.moveTo(pos.x, pos.y, pos.z, 0, 0); + dummy.teleportTo(dummy.serverLevel(), pos.x, pos.y, pos.z, 0, 0); } } }