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..edb656935
--- /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.
+ *
+ * <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, boolean front) { }
+
+	@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<Boolean> 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"
     ]
   }
 }