diff --git a/fabric-events-interaction-v0/src/main/java/net/fabricmc/fabric/api/entity/FakePlayer.java b/fabric-events-interaction-v0/src/main/java/net/fabricmc/fabric/api/entity/FakePlayer.java
new file mode 100644
index 000000000..8c0bf3440
--- /dev/null
+++ b/fabric-events-interaction-v0/src/main/java/net/fabricmc/fabric/api/entity/FakePlayer.java
@@ -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.
+ *
+ *
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.
+ *
+ *
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:
+ *
{@code
+ * UUID humanPlayerUuid = ...;
+ * String humanPlayerName = ...;
+ * GameProfile fakeProfile = new GameProfile(humanPlayerUuid, "[Block Breaker of " + humanPlayerName + "]");
+ * }
+ * If a fake player does not belong to a specific player, the {@link #DEFAULT_UUID default UUID} should be used.
+ *
+ * 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).
+ *
+ *
Instances are reused for the same world parameter.
+ *
+ *
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.
+ *
+ *
Instances are reused for the same parameters.
+ *
+ *
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 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) { }
+}
diff --git a/fabric-events-interaction-v0/src/main/java/net/fabricmc/fabric/impl/event/interaction/FakePlayerNetworkHandler.java b/fabric-events-interaction-v0/src/main/java/net/fabricmc/fabric/impl/event/interaction/FakePlayerNetworkHandler.java
new file mode 100644
index 000000000..ba3fe0fe5
--- /dev/null
+++ b/fabric-events-interaction-v0/src/main/java/net/fabricmc/fabric/impl/event/interaction/FakePlayerNetworkHandler.java
@@ -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) { }
+}
diff --git a/fabric-events-interaction-v0/src/main/java/net/fabricmc/fabric/mixin/event/interaction/PlayerAdvancementTrackerMixin.java b/fabric-events-interaction-v0/src/main/java/net/fabricmc/fabric/mixin/event/interaction/PlayerAdvancementTrackerMixin.java
new file mode 100644
index 000000000..ddd507b97
--- /dev/null
+++ b/fabric-events-interaction-v0/src/main/java/net/fabricmc/fabric/mixin/event/interaction/PlayerAdvancementTrackerMixin.java
@@ -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 ci) {
+ if (owner instanceof FakePlayer) {
+ // Prevent granting advancements to fake players.
+ ci.setReturnValue(false);
+ }
+ }
+}
diff --git a/fabric-events-interaction-v0/src/main/resources/fabric-events-interaction-v0.mixins.json b/fabric-events-interaction-v0/src/main/resources/fabric-events-interaction-v0.mixins.json
index 3a1133a5f..9156d082b 100644
--- a/fabric-events-interaction-v0/src/main/resources/fabric-events-interaction-v0.mixins.json
+++ b/fabric-events-interaction-v0/src/main/resources/fabric-events-interaction-v0.mixins.json
@@ -3,6 +3,7 @@
"package": "net.fabricmc.fabric.mixin.event.interaction",
"compatibilityLevel": "JAVA_16",
"mixins": [
+ "PlayerAdvancementTrackerMixin",
"ServerPlayerEntityMixin",
"ServerPlayerInteractionManagerMixin",
"ServerPlayNetworkHandlerMixin"
diff --git a/fabric-events-interaction-v0/src/testmod/java/net/fabricmc/fabric/test/event/interaction/FakePlayerTests.java b/fabric-events-interaction-v0/src/testmod/java/net/fabricmc/fabric/test/event/interaction/FakePlayerTests.java
new file mode 100644
index 000000000..5917e8dca
--- /dev/null
+++ b/fabric-events-interaction-v0/src/testmod/java/net/fabricmc/fabric/test/event/interaction/FakePlayerTests.java
@@ -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();
+ }
+}
diff --git a/fabric-events-interaction-v0/src/testmod/resources/fabric.mod.json b/fabric-events-interaction-v0/src/testmod/resources/fabric.mod.json
index 69a32bdb1..ce5baf877 100644
--- a/fabric-events-interaction-v0/src/testmod/resources/fabric.mod.json
+++ b/fabric-events-interaction-v0/src/testmod/resources/fabric.mod.json
@@ -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"
]
}
}