mirror of
https://github.com/FabricMC/fabric.git
synced 2024-11-28 18:46:16 -05:00
Add a Fake Player API (#3005)
This commit is contained in:
parent
1e1ae72503
commit
76ba65ebd7
6 changed files with 310 additions and 0 deletions
|
@ -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) { }
|
||||||
|
}
|
|
@ -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) { }
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
"package": "net.fabricmc.fabric.mixin.event.interaction",
|
"package": "net.fabricmc.fabric.mixin.event.interaction",
|
||||||
"compatibilityLevel": "JAVA_16",
|
"compatibilityLevel": "JAVA_16",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
|
"PlayerAdvancementTrackerMixin",
|
||||||
"ServerPlayerEntityMixin",
|
"ServerPlayerEntityMixin",
|
||||||
"ServerPlayerInteractionManagerMixin",
|
"ServerPlayerInteractionManagerMixin",
|
||||||
"ServerPlayNetworkHandlerMixin"
|
"ServerPlayNetworkHandlerMixin"
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,9 @@
|
||||||
"net.fabricmc.fabric.test.event.interaction.PlayerBreakBlockTests",
|
"net.fabricmc.fabric.test.event.interaction.PlayerBreakBlockTests",
|
||||||
"net.fabricmc.fabric.test.event.interaction.PlayerPickBlockTests",
|
"net.fabricmc.fabric.test.event.interaction.PlayerPickBlockTests",
|
||||||
"net.fabricmc.fabric.test.event.interaction.UseEntityTests"
|
"net.fabricmc.fabric.test.event.interaction.UseEntityTests"
|
||||||
|
],
|
||||||
|
"fabric-gametest": [
|
||||||
|
"net.fabricmc.fabric.test.event.interaction.FakePlayerTests"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue