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" ] } }