Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.GameComponents;
import xyz.nucleoid.stimuli.event.StimulusEvent;
Expand Down Expand Up @@ -200,6 +201,42 @@ public final class GamePlayerEvents {
}
});

/**
* Called when a player is about to respawn.
* This event is invoked before the new {@link ServerPlayer} is created.
* <p>
* 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<RequestRespawn> REQUEST_RESPAWN = StimulusEvent.create(RequestRespawn.class, ctx -> player -> {
try {
for (var listener : ctx.getListeners()) {
var result = listener.onRequestRespawn(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.
* <p>
* Should be used to make the player ready (save the new reference, set inventory...).
*/
public static final StimulusEvent<Respawn> 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(ServerPlayer player);
}
Expand Down Expand Up @@ -229,4 +266,12 @@ public interface LeaveMessage {
@Nullable
Component onLeaveMessageCreation(ServerPlayer player, @Nullable Component currentText, Component defaultText);
}

public interface RequestRespawn {
RespawnResult onRequestRespawn(ServerPlayer player);
}

public interface Respawn {
void onRespawn(ServerPlayer oldPlayer, ServerPlayer respawnedPlayer, boolean alive);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package xyz.nucleoid.plasmid.api.game.player;

import net.minecraft.network.protocol.game.*;
import xyz.nucleoid.plasmid.api.util.PlayerUtil;

import java.util.Collections;
Expand All @@ -8,10 +9,6 @@
import java.util.stream.StreamSupport;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket;
import net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket;
import net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket;
import net.minecraft.network.protocol.game.ClientboundSetTitlesAnimationPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
Expand Down Expand Up @@ -97,4 +94,13 @@ default void addStatusEffect(MobEffectInstance effect) {
player.addEffect(effect);
}
}

@Override
default void respawn() {
for (var player : this) {
if (!player.isDeadOrDying()) continue;

player.connection.handleClientCommand(new ServerboundClientCommandPacket(ServerboundClientCommandPacket.Action.PERFORM_RESPAWN));
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/xyz/nucleoid/plasmid/api/game/player/PlayerOps.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.Packet;
import net.minecraft.server.MinecraftServer;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.effect.MobEffectInstance;
Expand Down Expand Up @@ -98,4 +99,15 @@ default void showTitle(Component title, int lengthTicks) {
* @param effect the status effect to add
*/
void addStatusEffect(MobEffectInstance effect);

/**
* Respawns all dead players associated with this {@link PlayerOps} who are dead.
*
* @implNote Make sure this method is not called the same tick a player dies. To defer execution of a method by some number of ticks, you can use the {@link MinecraftServer#schedule(Runnable)} method:
* <pre> {@code
* MinecraftServer server = gameSpace.getServer();
* server.schedule(new TickTask(server.getTickCount() + 1, () -> gameSpace.getPlayers().respawn()));
* }</pre>
*/
void respawn();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package xyz.nucleoid.plasmid.api.game.player;

import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.portal.TeleportTransition;
import net.minecraft.world.phys.Vec3;

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 {
TeleportTransition.PostTeleportTransition MARKER = (entity) -> {};

TeleportTransition target();

static Respawn at(ServerLevel world, Vec3 pos) {
return at(world, pos, new Vec3(0, 0, 0));
}
static Respawn at(ServerLevel world, Vec3 pos, Vec3 velocity) {
return at(world, pos, velocity, 0, 0);
}
static Respawn at(ServerLevel world, Vec3 pos, Vec3 velocity, float yaw, float pitch) {
return at(new TeleportTransition(world, pos, velocity, yaw, pitch, MARKER));
}
static Respawn at(TeleportTransition target) {
return () -> target;
}
}

final class Leave implements RespawnResult {
private Leave() {
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ void onPlayerRemove(ServerPlayer player) {
}
}

void onPlayerRespawn(ServerPlayer oldPlayer, ServerPlayer respawnedPlayer) {
this.manager.removePlayerFromGameSpace(this, oldPlayer);
this.manager.addPlayerToGameSpace(this, respawnedPlayer);
}

void onAddLevel(RuntimeLevelHandle worldHandle) {
this.manager.addDimensionToGameSpace(this, worldHandle.asLevel().dimension());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ public boolean remove(ServerPlayer player) {
return true;
}

public void respawn(ServerPlayer oldPlayer, ServerPlayer 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import net.minecraft.network.protocol.game.ClientboundRespawnPacket;
import net.minecraft.network.protocol.game.ClientboundSetHeldSlotPacket;
import net.minecraft.network.protocol.game.CommonPlayerSpawnInfo;
import net.minecraft.network.protocol.game.ServerboundClientCommandPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
Expand Down Expand Up @@ -74,6 +75,11 @@ public void teleportOut(ServerPlayer player) {
}

private void teleport(ServerPlayer player, Function<ServerPlayer, ServerLevel> recreate, boolean in) {
if (!in && player.isDeadOrDying()) {
player.connection.handleClientCommand(new ServerboundClientCommandPacket(ServerboundClientCommandPacket.Action.PERFORM_RESPAWN));
player = player.connection.player;
}

var playerManager = this.server.getPlayerList();
var playerManagerAccess = (PlayerManagerAccess) playerManager;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,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;
Expand Down Expand Up @@ -77,6 +80,25 @@ private void removePlayer(ServerPlayer player, CallbackInfo ci) {
}
}

@Redirect(
method = "respawn",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/server/level/ServerPlayer;findRespawnPositionAndUseSpawnBlock(ZLnet/minecraft/world/level/portal/TeleportTransition$PostTeleportTransition;)Lnet/minecraft/world/level/portal/TeleportTransition;"
)
)
private TeleportTransition respawnPlayer(ServerPlayer player, boolean consumeSpawnBlock, TeleportTransition.PostTeleportTransition postTeleportTransition) {
var gameSpace = GameSpaceManagerImpl.get().byPlayer(player);
if (gameSpace != null) {
RespawnResult result = gameSpace.getBehavior().invoker(GamePlayerEvents.REQUEST_RESPAWN).onRequestRespawn(player);
if (result instanceof RespawnResult.Respawn respawn) {
return respawn.target();
}
}

return player.findRespawnPositionAndUseSpawnBlock(consumeSpawnBlock, postTeleportTransition);
}

@Inject(
method = "respawn",
at = @At(
Expand All @@ -93,10 +115,14 @@ private void respawnPlayer(
var gameSpace = GameSpaceManagerImpl.get().byPlayer(oldPlayer);

if (gameSpace != null) {
gameSpace.getPlayers().remove(oldPlayer);
if (respawnTarget.postTeleportTransition() == RespawnResult.Respawn.MARKER) {
gameSpace.getPlayers().respawn(oldPlayer, respawnedPlayer);
} else {
gameSpace.getPlayers().remove(oldPlayer);

this.plasmid$loadIntoPlayer(respawnedPlayer);
respawnedPlayer.setServerLevel(respawnLevel);
this.plasmid$loadIntoPlayer(respawnedPlayer);
respawnedPlayer.setServerLevel(respawnLevel);
}

// this is later used to apply back to the respawned player, and we want to maintain that
var interactionManager = respawnedPlayer.gameMode;
Expand All @@ -106,6 +132,19 @@ private void respawnPlayer(
}
}

@Inject(
method = "respawn",
at = @At("RETURN")
)
private void fireRespawnEvent(ServerPlayer oldPlayer, boolean alive, Entity.RemovalReason removalReason, CallbackInfoReturnable<ServerPlayer> cir) {
ServerPlayer respawnedPlayer = cir.getReturnValue();
var gameSpace = GameSpaceManagerImpl.get().byPlayer(respawnedPlayer);

if (gameSpace != null) {
gameSpace.getBehavior().invoker(GamePlayerEvents.RESPAWN).onRespawn(oldPlayer, respawnedPlayer, alive);
}
}

@Override
public void plasmid$savePlayerData(ServerPlayer player) {
this.save(player);
Expand Down