Add a Fake Player API (#3005)

This commit is contained in:
Technici4n 2023-05-01 14:54:22 +02:00 committed by GitHub
parent 1e1ae72503
commit 76ba65ebd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 310 additions and 0 deletions

View file

@ -0,0 +1,154 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.entity;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.UUID;
import com.google.common.collect.MapMaker;
import com.mojang.authlib.GameProfile;
import org.jetbrains.annotations.Nullable;
import net.minecraft.block.entity.SignBlockEntity;
import net.minecraft.entity.Entity;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.passive.AbstractHorseEntity;
import net.minecraft.inventory.Inventory;
import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket;
import net.minecraft.scoreboard.AbstractTeam;
import net.minecraft.screen.NamedScreenHandlerFactory;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.stat.Stat;
import net.minecraft.util.math.BlockPos;
import net.fabricmc.fabric.impl.event.interaction.FakePlayerNetworkHandler;
/**
* A "fake player" is a {@link ServerPlayerEntity} that is not a human player.
* They are typically used to automatically perform player actions such as placing blocks.
*
* <p>The easiest way to obtain a fake player is with {@link FakePlayer#get(ServerWorld)} or {@link FakePlayer#get(ServerWorld, GameProfile)}.
* It is also possible to create a subclass for more control over the fake player's behavior.
*
* <p>For good inter-mod compatibility, fake players should have the UUID of their owning (human) player.
* They should still have a different name to ensure the {@link GameProfile} is different.
* For example:
* <pre>{@code
* UUID humanPlayerUuid = ...;
* String humanPlayerName = ...;
* GameProfile fakeProfile = new GameProfile(humanPlayerUuid, "[Block Breaker of " + humanPlayerName + "]");
* }</pre>
* If a fake player does not belong to a specific player, the {@link #DEFAULT_UUID default UUID} should be used.
*
* <p>Fake players try to behave like regular {@link ServerPlayerEntity} objects to a reasonable extent.
* In some edge cases, or for gameplay considerations, it might be necessary to check whether a {@link ServerPlayerEntity} is a fake player.
* This can be done with an {@code instanceof} check: {@code player instanceof FakePlayer}.
*/
public class FakePlayer extends ServerPlayerEntity {
/**
* Default UUID, for fake players not associated with a specific (human) player.
*/
public static final UUID DEFAULT_UUID = UUID.fromString("41C82C87-7AfB-4024-BA57-13D2C99CAE77");
private static final GameProfile DEFAULT_PROFILE = new GameProfile(DEFAULT_UUID, "[Minecraft]");
/**
* Retrieves a fake player for the specified world, using the {@link #DEFAULT_UUID default UUID}.
* This is suitable when the fake player is not associated with a specific (human) player.
* Otherwise, the UUID of the owning (human) player should be used (see class javadoc).
*
* <p>Instances are reused for the same world parameter.
*
* <p>Caution should be exerted when storing the returned value,
* as strong references to the fake player will keep the world loaded.
*/
public static FakePlayer get(ServerWorld world) {
return get(world, DEFAULT_PROFILE);
}
/**
* Retrieves a fake player for the specified world and game profile.
* See class javadoc for more information on fake player game profiles.
*
* <p>Instances are reused for the same parameters.
*
* <p>Caution should be exerted when storing the returned value,
* as strong references to the fake player will keep the world loaded.
*/
public static FakePlayer get(ServerWorld world, GameProfile profile) {
Objects.requireNonNull(world, "World may not be null.");
Objects.requireNonNull(profile, "Game profile may not be null.");
return FAKE_PLAYER_MAP.computeIfAbsent(new FakePlayerKey(world, profile), key -> new FakePlayer(key.world, key.profile));
}
private record FakePlayerKey(ServerWorld world, GameProfile profile) { }
private static final Map<FakePlayerKey, FakePlayer> FAKE_PLAYER_MAP = new MapMaker().weakValues().makeMap();
protected FakePlayer(ServerWorld world, GameProfile profile) {
super(world.getServer(), world, profile);
this.networkHandler = new FakePlayerNetworkHandler(this);
}
@Override
public void tick() { }
@Override
public void setClientSettings(ClientSettingsC2SPacket packet) { }
@Override
public void increaseStat(Stat<?> stat, int amount) { }
@Override
public void resetStat(Stat<?> stat) { }
@Override
public boolean isInvulnerableTo(DamageSource damageSource) {
return true;
}
@Nullable
@Override
public AbstractTeam getScoreboardTeam() {
// Scoreboard team is checked using the gameprofile name by default, which we don't want.
return null;
}
@Override
public void sleep(BlockPos pos) {
// Don't lock bed forever.
}
@Override
public boolean startRiding(Entity entity, boolean force) {
return false;
}
@Override
public void openEditSignScreen(SignBlockEntity sign) { }
@Override
public OptionalInt openHandledScreen(@Nullable NamedScreenHandlerFactory factory) {
return OptionalInt.empty();
}
@Override
public void openHorseInventory(AbstractHorseEntity horse, Inventory inventory) { }
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.impl.event.interaction;
import org.jetbrains.annotations.Nullable;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.NetworkSide;
import net.minecraft.network.PacketCallbacks;
import net.minecraft.network.packet.Packet;
import net.minecraft.server.network.ServerPlayNetworkHandler;
import net.minecraft.server.network.ServerPlayerEntity;
public class FakePlayerNetworkHandler extends ServerPlayNetworkHandler {
private static final ClientConnection FAKE_CONNECTION = new ClientConnection(NetworkSide.CLIENTBOUND);
public FakePlayerNetworkHandler(ServerPlayerEntity player) {
super(player.getServer(), FAKE_CONNECTION, player);
}
@Override
public void sendPacket(Packet<?> packet, @Nullable PacketCallbacks callbacks) { }
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.event.interaction;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.advancement.Advancement;
import net.minecraft.advancement.PlayerAdvancementTracker;
import net.minecraft.server.network.ServerPlayerEntity;
import net.fabricmc.fabric.api.entity.FakePlayer;
@Mixin(PlayerAdvancementTracker.class)
public class PlayerAdvancementTrackerMixin {
@Shadow
private ServerPlayerEntity owner;
@Inject(method = "setOwner", at = @At("HEAD"), cancellable = true)
void preventOwnerOverride(ServerPlayerEntity newOwner, CallbackInfo ci) {
if (newOwner instanceof FakePlayer) {
// Prevent fake players with the same UUID as a real player from stealing the real player's advancement tracker.
ci.cancel();
}
}
@Inject(method = "grantCriterion", at = @At("HEAD"), cancellable = true)
void preventGrantCriterion(Advancement advancement, String criterionName, CallbackInfoReturnable<Boolean> ci) {
if (owner instanceof FakePlayer) {
// Prevent granting advancements to fake players.
ci.setReturnValue(false);
}
}
}

View file

@ -3,6 +3,7 @@
"package": "net.fabricmc.fabric.mixin.event.interaction",
"compatibilityLevel": "JAVA_16",
"mixins": [
"PlayerAdvancementTrackerMixin",
"ServerPlayerEntityMixin",
"ServerPlayerInteractionManagerMixin",
"ServerPlayNetworkHandlerMixin"

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.event.interaction;
import net.minecraft.block.Blocks;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.item.ItemUsageContext;
import net.minecraft.item.Items;
import net.minecraft.test.GameTest;
import net.minecraft.test.TestContext;
import net.minecraft.util.Hand;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3d;
import net.fabricmc.fabric.api.entity.FakePlayer;
import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
public class FakePlayerTests {
/**
* Try placing a sign with a fake player.
*/
@GameTest(templateName = FabricGameTest.EMPTY_STRUCTURE)
public void testFakePlayerPlaceSign(TestContext context) {
// This is for Fabric internal testing only, if you copy this to your mod you're on your own...
BlockPos basePos = new BlockPos(0, 1, 0);
BlockPos signPos = basePos.up();
context.setBlockState(basePos, Blocks.STONE.getDefaultState());
PlayerEntity fakePlayer = FakePlayer.get(context.getWorld());
BlockPos fakePlayerPos = context.getAbsolutePos(signPos.add(2, 0, 2));
fakePlayer.setPosition(fakePlayerPos.getX(), fakePlayerPos.getY(), fakePlayerPos.getZ());
ItemStack signStack = Items.OAK_SIGN.getDefaultStack();
fakePlayer.setStackInHand(Hand.MAIN_HAND, signStack);
Vec3d hitPos = context.getAbsolutePos(basePos).toCenterPos().add(0, 0.5, 0);
BlockHitResult hitResult = new BlockHitResult(hitPos, Direction.UP, context.getAbsolutePos(basePos), false);
signStack.useOnBlock(new ItemUsageContext(fakePlayer, Hand.MAIN_HAND, hitResult));
context.checkBlockState(signPos, x -> x.isOf(Blocks.OAK_SIGN), () -> "Sign was not placed");
context.assertTrue(signStack.isEmpty(), "Sign stack was not emptied");
context.complete();
}
}

View file

@ -14,6 +14,9 @@
"net.fabricmc.fabric.test.event.interaction.PlayerBreakBlockTests",
"net.fabricmc.fabric.test.event.interaction.PlayerPickBlockTests",
"net.fabricmc.fabric.test.event.interaction.UseEntityTests"
],
"fabric-gametest": [
"net.fabricmc.fabric.test.event.interaction.FakePlayerTests"
]
}
}