Files
fabric-carpet/src/main/java/carpet/patches/EntityPlayerMPFake.java
T
altrisi 2f67690ec3 Don't save player twice when shadowing (#2131)
Saving has side effects, such as removing enderpearls
2025-11-27 20:01:30 +01:00

278 lines
12 KiB
Java

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<String> 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<Level> 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<GameProfile> 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<ValueInput> 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;
}
}