Skip to content

Commit

Permalink
Add initial fake players
Browse files Browse the repository at this point in the history
  • Loading branch information
misode committed Dec 20, 2023
1 parent 418e780 commit b2f202c
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 25 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ repositories {
}

loom {
accessWidenerPath = file("src/main/resources/packtest.accesswidener")

splitEnvironmentSourceSets()

mods {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/io/github/misode/packtest/PackTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +27,7 @@ public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, buildContext, environment) -> {
AssertCommand.register(dispatcher, buildContext);
FailCommand.register(dispatcher);
PlayerCommand.register(dispatcher);
SucceedCommand.register(dispatcher, buildContext);
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CommandSourceStack> dispatcher) {
dispatcher.register(literal("player")
.then(literal("spawn")
.executes(PlayerCommand::spawnRandomName)
.then(argument("player", StringArgumentType.word())
.executes(PlayerCommand::spawnWithName)))
);
}

private static Optional<FakePlayer> getPlayer(CommandContext<CommandSourceStack> ctx) {
String playerName = StringArgumentType.getString(ctx, "player");
return getPlayer(playerName, ctx);
}

private static Optional<FakePlayer> getPlayer(String playerName, CommandContext<CommandSourceStack> 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<CommandSourceStack> 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<CommandSourceStack> ctx) {
String playerName = StringArgumentType.getString(ctx, "player");
return spawn(playerName, ctx);
}

private static int spawn(String playerName, CommandContext<CommandSourceStack> 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<Level> dimension = source.getLevel().dimension();
FakePlayer.create(playerName, server, dimension, source.getPosition());
return 1;
}
}
Original file line number Diff line number Diff line change
@@ -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) {}
}
62 changes: 62 additions & 0 deletions src/main/java/io/github/misode/packtest/fake/FakePlayer.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://github.com/gnembon/fabric-carpet/blob/master/src/main/java/carpet/patches/EntityPlayerMPFake.java">Carpet</a>
*/
public class FakePlayer extends ServerPlayer {
public Runnable fixStartingPosition = () -> {};

public static void create(String username, MinecraftServer server, ResourceKey<Level> 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);
}
}
23 changes: 23 additions & 0 deletions src/main/java/io/github/misode/packtest/mixin/PlayerListMixin.java
Original file line number Diff line number Diff line change
@@ -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<CompoundTag> cir) {
if (player instanceof FakePlayer) {
((FakePlayer) player).fixStartingPosition.run();
}
}
}
1 change: 1 addition & 0 deletions src/main/resources/fabric.mod.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"io.github.misode.packtest.PackTest"
]
},
"accessWidener" : "packtest.accesswidener",
"mixins": [
"packtest.mixins.json"
],
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/packtest.accesswidener
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
accessWidener v1 named

accessible method net/minecraft/world/level/block/entity/SkullBlockEntity fetchGameProfile (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture;
51 changes: 26 additions & 25 deletions src/main/resources/packtest.mixins.json
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit b2f202c

Please sign in to comment.