diff --git a/src/main/java/xyz/nucleoid/plasmid/api/game/event/GamePlayerEvents.java b/src/main/java/xyz/nucleoid/plasmid/api/game/event/GamePlayerEvents.java index b6c54df42..84956e3cb 100644 --- a/src/main/java/xyz/nucleoid/plasmid/api/game/event/GamePlayerEvents.java +++ b/src/main/java/xyz/nucleoid/plasmid/api/game/event/GamePlayerEvents.java @@ -8,6 +8,7 @@ import xyz.nucleoid.plasmid.api.game.player.JoinAcceptorResult; import xyz.nucleoid.plasmid.api.game.player.JoinOffer; import xyz.nucleoid.plasmid.api.game.player.JoinOfferResult; +import xyz.nucleoid.plasmid.api.game.player.RespawnResult; import xyz.nucleoid.plasmid.api.game.GameActivity; import xyz.nucleoid.plasmid.api.game.GameTexts; import xyz.nucleoid.stimuli.event.StimulusEvent; @@ -200,6 +201,42 @@ public final class GamePlayerEvents { } }); + /** + * Called when a player is about to respawn. + * This event is invoked before the new {@link ServerPlayerEntity} is created. + *

+ * Listeners should return {@link RespawnResult.Respawn} with the target world to keep the player in the game. + * Otherwise, the player will be thrown out of the game. + */ + public static final StimulusEvent REQUEST_RESPAWN = StimulusEvent.create(RequestRespawn.class, ctx -> (gameSpace, player) -> { + try { + for (var listener : ctx.getListeners()) { + var result = listener.onRequestRespawn(gameSpace, player); + if (!(result instanceof RespawnResult.Pass)) { + return result; + } + } + } catch (Throwable throwable) { + ctx.handleException(throwable); + } + return RespawnResult.PASS; + }); + + /** + * Called after the new player entity has been created and assigned to the correct world. + *

+ * Should be used to make the player ready (save the new reference, set inventory...). + */ + public static final StimulusEvent RESPAWN = StimulusEvent.create(Respawn.class, ctx -> (oldPlayer, respawnedPlayer, alive) -> { + try { + for (var listener : ctx.getListeners()) { + listener.onRespawn(oldPlayer, respawnedPlayer, alive); + } + } catch (Throwable throwable) { + ctx.handleException(throwable); + } + }); + public interface Add { void onAddPlayer(ServerPlayerEntity player); } @@ -229,4 +266,12 @@ public interface LeaveMessage { @Nullable Text onLeaveMessageCreation(ServerPlayerEntity player, @Nullable Text currentText, Text defaultText); } + + public interface RequestRespawn { + RespawnResult onRequestRespawn(GameSpace gameSpace, ServerPlayerEntity player); + } + + public interface Respawn { + void onRespawn(ServerPlayerEntity oldPlayer, ServerPlayerEntity respawnedPlayer, boolean alive); + } } diff --git a/src/main/java/xyz/nucleoid/plasmid/api/game/player/RespawnResult.java b/src/main/java/xyz/nucleoid/plasmid/api/game/player/RespawnResult.java new file mode 100644 index 000000000..e4b2978d6 --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/api/game/player/RespawnResult.java @@ -0,0 +1,37 @@ +package xyz.nucleoid.plasmid.api.game.player; + +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.TeleportTarget; + +public sealed interface RespawnResult permits RespawnResult.Pass, RespawnResult.Respawn, RespawnResult.Leave { + Pass PASS = new Pass(); + Leave LEAVE = new Leave(); + + final class Pass implements RespawnResult { + private Pass() { + } + } + + non-sealed interface Respawn extends RespawnResult { + TeleportTarget target(); + + static Respawn at(ServerWorld world, Vec3d pos) { + return at(world, pos, new Vec3d(0, 0, 0)); + } + static Respawn at(ServerWorld world, Vec3d pos, Vec3d velocity) { + return at(world, pos, velocity, 0, 0); + } + static Respawn at(ServerWorld world, Vec3d pos, Vec3d velocity, float yaw, float pitch) { + return at(new TeleportTarget(world, pos, velocity, yaw, pitch, TeleportTarget.NO_OP)); + } + static Respawn at(TeleportTarget target) { + return () -> target; + } + } + + final class Leave implements RespawnResult { + private Leave() { + } + } +} diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/ManagedGameSpace.java b/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/ManagedGameSpace.java index c551714b8..01d2a6b04 100644 --- a/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/ManagedGameSpace.java +++ b/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/ManagedGameSpace.java @@ -257,6 +257,11 @@ void onPlayerRemove(ServerPlayerEntity player) { } } + void onPlayerRespawn(ServerPlayerEntity oldPlayer, ServerPlayerEntity respawnedPlayer) { + this.manager.removePlayerFromGameSpace(this, oldPlayer); + this.manager.addPlayerToGameSpace(this, respawnedPlayer); + } + void onAddWorld(RuntimeWorldHandle worldHandle) { this.manager.addDimensionToGameSpace(this, worldHandle.asWorld().getRegistryKey()); } diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/ManagedGameSpacePlayers.java b/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/ManagedGameSpacePlayers.java index 6d88c581e..cf78f6f45 100644 --- a/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/ManagedGameSpacePlayers.java +++ b/src/main/java/xyz/nucleoid/plasmid/impl/game/manager/ManagedGameSpacePlayers.java @@ -155,6 +155,24 @@ public boolean remove(ServerPlayerEntity player) { return true; } + public void respawn(ServerPlayerEntity oldPlayer, ServerPlayerEntity respawnedPlayer) { + if (!this.set.contains(oldPlayer)) { + return; + } + + this.set.remove(oldPlayer); + this.set.add(respawnedPlayer); + + if (this.players.remove(oldPlayer)) { + this.players.add(respawnedPlayer); + } + if (this.spectators.remove(oldPlayer)) { + this.spectators.add(respawnedPlayer); + } + + this.space.onPlayerRespawn(oldPlayer, respawnedPlayer); + } + void clear() { this.set.clear(); } diff --git a/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/PlayerManagerMixin.java b/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/PlayerManagerMixin.java index 62a995f22..6c31a005c 100644 --- a/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/PlayerManagerMixin.java +++ b/src/main/java/xyz/nucleoid/plasmid/mixin/game/space/PlayerManagerMixin.java @@ -20,6 +20,7 @@ import net.minecraft.world.TeleportTarget; import net.minecraft.world.World; import net.minecraft.world.dimension.DimensionType; +import org.lwjgl.opengl.NVVertexArrayRange; import org.slf4j.Logger; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; @@ -27,9 +28,12 @@ import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.spongepowered.asm.mixin.injection.callback.LocalCapture; +import xyz.nucleoid.plasmid.api.game.event.GamePlayerEvents; +import xyz.nucleoid.plasmid.api.game.player.RespawnResult; import xyz.nucleoid.plasmid.impl.game.manager.GameSpaceManagerImpl; import xyz.nucleoid.plasmid.impl.player.isolation.PlayerManagerAccess; import xyz.nucleoid.plasmid.impl.player.isolation.PlayerResetter; @@ -70,6 +74,28 @@ private void removePlayer(ServerPlayerEntity player, CallbackInfo ci) { } } + @Redirect( + method = "respawnPlayer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/network/ServerPlayerEntity;getRespawnTarget(ZLnet/minecraft/world/TeleportTarget$PostDimensionTransition;)Lnet/minecraft/world/TeleportTarget;" + ) + ) + private TeleportTarget respawnPlayer(ServerPlayerEntity player, boolean alive, TeleportTarget.PostDimensionTransition postDimensionTransition) { + var gameSpace = GameSpaceManagerImpl.get().byPlayer(player); + if (gameSpace != null) { + RespawnResult result = gameSpace.getBehavior().invoker(GamePlayerEvents.REQUEST_RESPAWN).onRequestRespawn(gameSpace, player); + if (result instanceof RespawnResult.Respawn respawn) { + TeleportTarget target = respawn.target(); + if (target instanceof TeleportTarget(var world, var pos, var vel, var yaw, var pitch, var missing, var asPassenger, var rel, var post)) { + return new TeleportTarget(world, pos, vel, yaw, pitch, missing, asPassenger, rel, TeleportTarget.SEND_TRAVEL_THROUGH_PORTAL_PACKET /*mark*/); + } + } + } + + return player.getRespawnTarget(alive, postDimensionTransition); + } + @Inject( method = "respawnPlayer", at = @At( @@ -86,10 +112,16 @@ private void respawnPlayer( var gameSpace = GameSpaceManagerImpl.get().byPlayer(oldPlayer); if (gameSpace != null) { - gameSpace.getPlayers().remove(oldPlayer); + if (respawnTarget.postTeleportTransition() == TeleportTarget.SEND_TRAVEL_THROUGH_PORTAL_PACKET) { + gameSpace.getPlayers().respawn(oldPlayer, respawnedPlayer); + + gameSpace.getBehavior().invoker(GamePlayerEvents.RESPAWN).onRespawn(oldPlayer, respawnedPlayer, alive); + } else { + gameSpace.getPlayers().remove(oldPlayer); - this.plasmid$loadIntoPlayer(respawnedPlayer); - respawnedPlayer.setServerWorld(respawnWorld); + this.plasmid$loadIntoPlayer(respawnedPlayer); + respawnedPlayer.setServerWorld(respawnWorld); + } // this is later used to apply back to the respawned player, and we want to maintain that var interactionManager = respawnedPlayer.interactionManager;