package carpet.patches; import carpet.CarpetSettings; import com.mojang.authlib.GameProfile; import net.minecraft.core.BlockPos; import net.minecraft.core.UUIDUtil; import net.minecraft.network.DisconnectionDetails; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.contents.TranslatableContents; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket; import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; 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.OldUsersConverter; import net.minecraft.util.ProblemReporter; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.player.Player; import net.minecraft.world.food.FoodData; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.ResolvableProfile; import net.minecraft.world.level.GameType; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.portal.TeleportTransition; import net.minecraft.world.level.storage.TagValueInput; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.phys.Vec3; import carpet.fakes.ServerPlayerInterface; import carpet.utils.Messenger; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @SuppressWarnings("EntityConstructor") public class EntityPlayerMPFake extends ServerPlayer { private static final Set spawning = new HashSet<>(); public Runnable fixStartingPosition = () -> {}; public boolean isAShadow; // Returns true if it was successful, false if couldn't spawn due to the player not existing in Mojang servers public static boolean createFake(String username, MinecraftServer server, Vec3 pos, double yaw, double pitch, ResourceKey dimensionId, GameType gamemode, boolean flying) { //prolly half of that crap is not necessary, but it works ServerLevel worldIn = server.getLevel(dimensionId); server.services().nameToIdCache().resolveOfflineUsers(false); GameProfile gameprofile; UUID uuid = OldUsersConverter.convertMobOwnerIfNecessary(server, username); //NameAndId res = server.services().nameToIdCache().get(username).orElseThrow(); //findByName .orElse(null) if (uuid == null && CarpetSettings.allowSpawningOfflinePlayers) { server.services().nameToIdCache().resolveOfflineUsers(server.isDedicatedServer() && server.usesAuthentication()); uuid = UUIDUtil.createOfflinePlayerUUID(username); } if (uuid == null) { return false; // no uuid, no player } gameprofile = new GameProfile(uuid, username); //GameProfile finalGP = gameprofile; // We need to mark this player as spawning so that we do not // try to spawn another player with the name while the profile // is being fetched - preventing multiple players spawning String name = gameprofile.name(); spawning.add(name); fetchGameProfile(server, gameprofile.id()).whenCompleteAsync((p, t) -> { // Always remove the name, even if exception occurs spawning.remove(name); if (t != null) { return; } GameProfile current; if (p.name().isEmpty()) { current = gameprofile; } else { current = p; } EntityPlayerMPFake instance = new EntityPlayerMPFake(server, worldIn, current, ClientInformation.createDefault(), false); instance.fixStartingPosition = () -> instance.snapTo(pos.x, pos.y, pos.z, (float) yaw, (float) pitch); server.getPlayerList().placeNewPlayer(new FakeClientConnection(PacketFlow.SERVERBOUND), instance, new CommonListenerCookie(current, 0, instance.clientInformation(), false)); loadPlayerData(instance); instance.stopRiding(); // otherwise the created fake player will be on the vehicle instance.teleportTo(worldIn, pos.x, pos.y, pos.z, Set.of(), (float) yaw, (float) pitch, true); instance.setHealth(20.0F); instance.unsetRemoved(); instance.getAttribute(Attributes.STEP_HEIGHT).setBaseValue(0.6F); instance.gameMode.changeGameModeForPlayer(gamemode); server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte) (instance.yHeadRot * 256 / 360)), dimensionId);//instance.dimension); server.getPlayerList().broadcastAll(ClientboundEntityPositionSyncPacket.of(instance), dimensionId);//instance.dimension); //instance.world.getChunkManager(). updatePosition(instance); instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte) 0x7f); // show all model layers (incl. capes) instance.getAbilities().flying = flying; }, server); return true; } private static CompletableFuture fetchGameProfile(MinecraftServer server, final UUID name) { final ResolvableProfile resolvableProfile = ResolvableProfile.createUnresolved(name); return resolvableProfile.resolveProfile(server.services().profileResolver()); } private static void loadPlayerData(EntityPlayerMPFake player) { try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), CarpetSettings.LOG)) { Optional optional = player.level().getServer().getPlayerList().loadPlayerData(player.nameAndId()).map((compoundTag) -> TagValueInput.create(scopedCollector, player.registryAccess(), compoundTag)); optional.ifPresent( valueInput -> { player.load(valueInput); player.loadAndSpawnEnderPearls(valueInput); player.loadAndSpawnParentVehicle(valueInput); }); } } public static EntityPlayerMPFake createShadow(MinecraftServer server, ServerPlayer player) { player.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login")); ServerLevel worldIn = player.level();//.getWorld(player.dimension); GameProfile gameprofile = player.getGameProfile(); EntityPlayerMPFake playerShadow = new EntityPlayerMPFake(server, worldIn, gameprofile, player.clientInformation(), true); playerShadow.setChatSession(player.getChatSession()); server.getPlayerList().placeNewPlayer(new FakeClientConnection(PacketFlow.SERVERBOUND), playerShadow, new CommonListenerCookie(gameprofile, 0, player.clientInformation(), true)); loadPlayerData(playerShadow); playerShadow.setHealth(player.getHealth()); playerShadow.connection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); playerShadow.gameMode.changeGameModeForPlayer(player.gameMode.getGameModeForPlayer()); ((ServerPlayerInterface) playerShadow).getActionPack().copyFrom(((ServerPlayerInterface) player).getActionPack()); // this might create problems if a player logs back in... playerShadow.getAttribute(Attributes.STEP_HEIGHT).setBaseValue(0.6F); playerShadow.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, player.getEntityData().get(DATA_PLAYER_MODE_CUSTOMISATION)); server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(playerShadow, (byte) (player.yHeadRot * 256 / 360)), playerShadow.level().dimension()); server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, playerShadow)); //player.world.getChunkManager().updatePosition(playerShadow); playerShadow.getAbilities().flying = player.getAbilities().flying; return playerShadow; } public static EntityPlayerMPFake respawnFake(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli) { return new EntityPlayerMPFake(server, level, profile, cli, false); } public static boolean isSpawningPlayer(String username) { return spawning.contains(username); } private EntityPlayerMPFake(MinecraftServer server, ServerLevel worldIn, GameProfile profile, ClientInformation cli, boolean shadow) { super(server, worldIn, profile, cli); this.isAShadow = shadow; } @Override public void onEquipItem(final EquipmentSlot slot, final ItemStack previous, final ItemStack stack) { if (!isUsingItem()) super.onEquipItem(slot, previous, stack); } @Override public void kill(ServerLevel level) { kill(Messenger.s("Killed")); } public void kill(Component reason) { shakeOff(); if (reason.getContents() instanceof TranslatableContents text && text.getKey().equals("multiplayer.disconnect.duplicate_login")) { this.connection.onDisconnect(new DisconnectionDetails(reason)); } else { this.level().getServer().schedule(new TickTask(this.level().getServer().getTickCount(), () -> { this.connection.onDisconnect(new DisconnectionDetails(reason)); })); } } @Override public void tick() { if (this.level().getServer().getTickCount() % 10 == 0) { this.connection.resetPosition(); this.level().getChunkSource().move(this); } try { super.tick(); this.doTick(); } catch (NullPointerException ignored) { // happens with that paper port thingy - not sure what that would fix, but hey // the game not gonna crash violently. } } private void shakeOff() { if (getVehicle() instanceof Player) stopRiding(); for (Entity passenger : getIndirectPassengers()) { if (passenger instanceof Player) passenger.stopRiding(); } } @Override public void die(DamageSource cause) { shakeOff(); super.die(cause); setHealth(20); this.foodData = new FoodData(); kill(this.getCombatTracker().getDeathMessage()); } @Override public String getIpAddress() { return "127.0.0.1"; } @Override public boolean allowsListing() { return CarpetSettings.allowListingFakePlayers; } @Override protected void checkFallDamage(double y, boolean onGround, BlockState state, BlockPos pos) { doCheckFallDamage(0.0, y, 0.0, onGround); } @Override public ServerPlayer teleport(TeleportTransition serverLevel) { super.teleport(serverLevel); if (wonGame) { ServerboundClientCommandPacket p = new ServerboundClientCommandPacket(ServerboundClientCommandPacket.Action.PERFORM_RESPAWN); connection.handleClientCommand(p); } // If above branch was taken, *this* has been removed and replaced, the new instance has been set // on 'our' connection (which is now theirs, but we still have a ref). if (connection.player.isChangingDimension()) { connection.player.hasChangedDimension(); } return connection.player; } }