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;