diff --git a/src/main/java/com/lovetropics/minigames/client/lobby/manage/state/ClientLobbyPlayer.java b/src/main/java/com/lovetropics/minigames/client/lobby/manage/state/ClientLobbyPlayer.java index f956690a8..18db02909 100644 --- a/src/main/java/com/lovetropics/minigames/client/lobby/manage/state/ClientLobbyPlayer.java +++ b/src/main/java/com/lovetropics/minigames/client/lobby/manage/state/ClientLobbyPlayer.java @@ -20,7 +20,7 @@ private ClientLobbyPlayer(UUID uuid, @Nullable PlayerRole playingRole) { } public static ClientLobbyPlayer from(IGameLobby lobby, ServerPlayer player) { - IGamePhase currentPhase = lobby.getCurrentPhase(); + IGamePhase currentPhase = lobby.getActivePhase(); PlayerRole playingRole = currentPhase != null ? currentPhase.getRoleFor(player) : null; return new ClientLobbyPlayer(player.getUUID(), playingRole); } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java index d5dc9ccee..a8097cd92 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GameLobby.java @@ -1,7 +1,6 @@ package com.lovetropics.minigames.common.core.game.impl; import com.google.common.collect.Lists; -import com.google.common.collect.Maps; import com.lovetropics.minigames.client.lobby.state.ClientCurrentGame; import com.lovetropics.minigames.client.lobby.state.message.JoinedLobbyMessage; import com.lovetropics.minigames.client.lobby.state.message.LeftLobbyMessage; @@ -21,18 +20,15 @@ import com.lovetropics.minigames.common.core.game.player.PlayerIterable; import com.lovetropics.minigames.common.core.game.player.PlayerRoleSelections; import com.lovetropics.minigames.common.core.game.rewards.GameRewardsMap; -import com.lovetropics.minigames.common.core.game.state.IGameState; import com.lovetropics.minigames.common.core.game.util.GameTexts; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.Unit; import net.neoforged.neoforge.network.PacketDistributor; import javax.annotation.Nullable; -import java.util.Map; // TODO: do we want a different game lobby implementation for something like carnival games? /** @@ -95,8 +91,9 @@ public LobbyGameQueue getGameQueue() { @Nullable @Override - public IGamePhase getCurrentPhase() { - return state.getPhase(); + public IGamePhase getActivePhase() { + GamePhase phase = state.getTopPhase(); + return phase != null ? phase.getActivePhase() : null; } @Nullable @@ -242,16 +239,16 @@ void onPlayerRegister(ServerPlayer player) { stateListener.onPlayerJoin(this, player); - GamePhase phase = state.getPhase(); + GamePhase phase = state.getTopPhase(); if (phase != null) { - phase.onPlayerJoin(player); + phase.onPlayerJoin(player, false); } management.onPlayersChanged(); } void onPlayerLeave(ServerPlayer player, boolean loggingOut) { - GamePhase phase = state.getPhase(); + GamePhase phase = state.getTopPhase(); if (phase != null) { player = phase.onPlayerLeave(player, loggingOut); } @@ -305,7 +302,7 @@ void close(boolean serverStopping) { static final class ChatNotifyListener implements LobbyStateListener { @Override public void onPlayerJoin(IGameLobby lobby, ServerPlayer player) { - IGamePhase currentPhase = lobby.getCurrentPhase(); + IGamePhase currentPhase = lobby.getActivePhase(); if (currentPhase != null && currentPhase.phaseType() == GamePhaseType.WAITING) { onPlayerJoinGame(lobby, currentPhase); } @@ -313,7 +310,7 @@ public void onPlayerJoin(IGameLobby lobby, ServerPlayer player) { @Override public void onPlayerLeave(IGameLobby lobby, ServerPlayer player) { - IGamePhase currentPhase = lobby.getCurrentPhase(); + IGamePhase currentPhase = lobby.getActivePhase(); if (currentPhase != null && currentPhase.phaseType() == GamePhaseType.WAITING) { onPlayerLeaveGame(lobby, currentPhase); } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java index 5131be46b..f8f2ee467 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/GamePhase.java @@ -255,12 +255,14 @@ private void onSetPlayerRole(ServerPlayer player, @Nullable PlayerRole role, @Nu } } - void onPlayerJoin(ServerPlayer player) { + ServerPlayer onPlayerJoin(ServerPlayer player, boolean savePlayerDataToMemory) { try { - ServerPlayer newPlayer = addAndSpawnPlayer(player, null, false); + ServerPlayer newPlayer = addAndSpawnPlayer(player, getRoleFor(player), savePlayerDataToMemory); invoker(GamePlayerEvents.JOIN).onAdd(newPlayer); + return newPlayer; } catch (Exception e) { LoveTropics.LOGGER.warn("Failed to dispatch player join event", e); + return player; } } @@ -284,6 +286,18 @@ ServerPlayer onPlayerLeave(ServerPlayer player, boolean loggingOut) { return PlayerIsolation.INSTANCE.restore(player); } + void removePlayer(ServerPlayer player) { + addedPlayers.remove(player.getUUID()); + for (PlayerRole role : PlayerRole.ROLES) { + roles.get(role).remove(player); + } + try { + invoker(GamePlayerEvents.REMOVE).onRemove(player); + } catch (Exception e) { + LoveTropics.LOGGER.warn("Failed to dispatch player leave event", e); + } + } + public void cancelWithError(Exception exception) { LoveTropics.LOGGER.warn("Game canceled due to exception", exception); requestStop(GameStopReason.errored(Component.literal("Game stopped due to exception: " + exception))); @@ -359,4 +373,8 @@ public GameScheduler scheduler() { public long ticks() { return level().getGameTime() - startTime; } + + public IGamePhase getActivePhase() { + return this; + } } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/LobbyStateManager.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/LobbyStateManager.java index 44d3f0c9b..7db5021f1 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/LobbyStateManager.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/LobbyStateManager.java @@ -23,7 +23,7 @@ public GameInstance getGame() { } @Nullable - public GamePhase getPhase() { + public GamePhase getTopPhase() { return state.phase; } diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGameManager.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGameManager.java index f3e82c40d..3ad97670c 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGameManager.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGameManager.java @@ -104,7 +104,7 @@ public GameLobby getLobbyFor(Player player) { @Override public IGamePhase getGamePhaseFor(Player player) { GameLobby lobby = getLobbyFor(player); - return lobby != null ? lobby.getCurrentPhase() : null; + return lobby != null ? lobby.getActivePhase() : null; } @Nullable @@ -297,7 +297,12 @@ public static void onPlayerTryChangeDimension(EntityTravelToDimensionEvent event } private static boolean canTravelBetweenPhases(@Nullable IGamePhase from, @Nullable IGamePhase to) { - return to == null || from == to; + if (to == null) { + return true; + } else if (from == null) { + return false; + } + return from.lobby() == to.lobby(); } @SubscribeEvent diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java index cd3d2a896..7f431e128 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/impl/MultiGamePhase.java @@ -3,69 +3,78 @@ import com.google.common.collect.Lists; import com.lovetropics.minigames.common.content.river_race.event.RiverRaceEvents; import com.lovetropics.minigames.common.core.game.GamePhaseType; -import com.lovetropics.minigames.common.core.game.GameResult; import com.lovetropics.minigames.common.core.game.GameStopReason; import com.lovetropics.minigames.common.core.game.IGameDefinition; +import com.lovetropics.minigames.common.core.game.IGamePhase; import com.lovetropics.minigames.common.core.game.IGamePhaseDefinition; import com.lovetropics.minigames.common.core.game.PlayerIsolation; import com.lovetropics.minigames.common.core.game.behavior.BehaviorList; -import com.lovetropics.minigames.common.core.game.behavior.event.GameEventType; import com.lovetropics.minigames.common.core.game.behavior.event.GamePlayerEvents; import com.lovetropics.minigames.common.core.game.config.GameConfig; import com.lovetropics.minigames.common.core.game.map.GameMap; import com.lovetropics.minigames.common.core.game.player.PlayerRole; -import com.lovetropics.minigames.common.core.game.state.GameStateMap; -import com.lovetropics.minigames.common.core.map.MapRegions; +import com.lovetropics.minigames.common.core.game.player.PlayerSet; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceKey; -import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.util.Unit; -import net.minecraft.world.level.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.annotation.Nullable; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Queue; import java.util.concurrent.CompletableFuture; public class MultiGamePhase extends GamePhase { private static final Logger LOGGER = LogManager.getLogger(MultiGamePhase.class); + private final Queue subPhaseQueue = new ArrayDeque<>(); @Nullable - private GamePhase activePhase = null; - private final List subPhaseGames = new ArrayList<>(); + private GamePhase subPhase; + + // TODO: Big hack - can we do something better by splitting what we expose to game impls vs what we expose to the outside? + // Some behaviors such as spectator_chase check the spectator list when the player is removed - but that spectator list didn't get the player removed + private boolean hideRoles; protected MultiGamePhase(GameInstance game, IGameDefinition gameDefinition, IGamePhaseDefinition definition, GamePhaseType phaseType, GameMap map, BehaviorList behaviors) { super(game, gameDefinition, definition, phaseType, map, behaviors); } public void startSubPhase(GamePhase subPhase, final boolean saveInventory) { - activePhase = subPhase; + this.subPhase = subPhase; + MultiGameManager.INSTANCE.addGamePhaseToDimension(subPhase.dimension(), subPhase); subPhase.assignRolesFrom(this); + hideRoles = true; for (ServerPlayer player : allPlayers()) { - invoker(GamePlayerEvents.REMOVE).onRemove(player); + movePlayerToSubPhase(player); } - addedPlayers.clear(); - MultiGameManager.INSTANCE.addGamePhaseToDimension(subPhase.dimension(), subPhase); + hideRoles = false; subPhase.start(saveInventory); } - public void returnHere(){ + private void movePlayerToSubPhase(ServerPlayer player) { + invoker(GamePlayerEvents.REMOVE).onRemove(player); + addedPlayers.remove(player.getUUID()); + } + + private void returnHere(GamePhase fromSubPhase) { List shuffledPlayers = Lists.newArrayList(allPlayers()); Collections.shuffle(shuffledPlayers); - for (ServerPlayer player : shuffledPlayers) { - returnPlayerToParentPhase(player, getRoleFor(player)); + returnPlayerToParentPhase(fromSubPhase, player); } } - private void returnPlayerToParentPhase(ServerPlayer player, @Nullable PlayerRole role) { - // [Cojo] Added this event just in case we want to know when a player returns from a microgame, can remove if there's no usecase for it + private ServerPlayer returnPlayerToParentPhase(GamePhase fromSubPhase, ServerPlayer player) { + fromSubPhase.removePlayer(player); + addedPlayers.add(player.getUUID()); + + PlayerRole role = getRoleFor(player); invoker(GamePlayerEvents.RETURN).onReturn(player.getUUID(), role); ServerPlayer newPlayer = PlayerIsolation.INSTANCE.reloadPlayerFromMemory(game, player); @@ -73,115 +82,97 @@ private void returnPlayerToParentPhase(ServerPlayer player, @Nullable PlayerRole invoker(GamePlayerEvents.ADD).onAdd(newPlayer); invoker(GamePlayerEvents.SET_ROLE).onSetRole(newPlayer, role, null); - addedPlayers.add(player.getUUID()); + return newPlayer; } @Override - public ResourceKey dimension() { - if(activePhase != null){ - return activePhase.dimension(); - } - return super.dimension(); - } - - @Override - public T invoker(GameEventType type) { - // Figure out what our sub-game phase is active and do that instead - if(activePhase != null){ - return activePhase.invoker(type); - } - return events.invoker(type); + public GameInstance game(){ + return game; } @Override - public GameInstance game(){ - return game; + public IGamePhase getActivePhase() { + return Objects.requireNonNullElse(subPhase, this); } @Nullable @Override GameStopReason tick() { - // Also tick our current sub-game phase - if(activePhase != null){ - if(activePhase.tick() != null){ - MultiGameManager.INSTANCE.removeGamePhaseFromDimension(activePhase.dimension(), activePhase); - activePhase.destroy(); - activePhase = null; + if (subPhase != null) { + if (subPhase.tick() != null) { + GamePhase lastPhase = subPhase; + destroySubGame(); startNextQueuedMicrogame(false).whenComplete((newGame, throwable) -> { - if(!newGame){ - returnHere(); + if (throwable != null || !newGame) { + returnHere(lastPhase); } - if(throwable != null){ - LOGGER.info("Failed to start next queued micro-game {}", throwable.getMessage()); + if (throwable != null) { + LOGGER.error("Failed to start next queued micro-game", throwable); } }); + return null; } - } else { - return super.tick(); - } - return null; - } - - @Override - public GameResult requestStop(GameStopReason reason) { - if(activePhase != null){ - if(reason.isFinished()){ - return activePhase.requestStop(GameStopReason.canceled()); - } - return activePhase.requestStop(reason); } - return super.requestStop(reason); + return super.tick(); } @Override - public MapRegions mapRegions() { - if(activePhase != null){ - return activePhase.mapRegions(); + ServerPlayer onPlayerJoin(ServerPlayer player, boolean savePlayerDataToMemory) { + player = super.onPlayerJoin(player, savePlayerDataToMemory); + if (subPhase != null) { + // Let the top-level game decide how the player can join, and then just pass them along + subPhase.assignRolesFrom(this); + movePlayerToSubPhase(player); + return subPhase.onPlayerJoin(player, true); } - return super.mapRegions(); + return player; } - @Override - public ServerLevel level() { - if(activePhase != null){ - return activePhase.level(); + ServerPlayer onPlayerLeave(ServerPlayer player, boolean loggingOut) { + if (subPhase != null) { + // To ensure that the top-level game gets notified properly, we need to pull the player out step-by-step + player = returnPlayerToParentPhase(subPhase, player); + } + return super.onPlayerLeave(player, loggingOut); + } + + private void destroySubGame() { + if (subPhase != null) { + subPhase.destroy(); + MultiGameManager.INSTANCE.removeGamePhaseFromDimension(subPhase.dimension(), subPhase); + subPhase = null; } - return super.level(); } @Override - public GameStateMap state() { - if(activePhase != null){ - return activePhase.state(); - } - return super.state(); + void destroy() { + destroySubGame(); + super.destroy(); } @Override - void destroy() { - if(activePhase != null){ - activePhase.destroy(); - activePhase = null; - return; + public PlayerSet getPlayersWithRole(PlayerRole role) { + if (hideRoles) { + return PlayerSet.EMPTY; } - super.destroy(); + return super.getPlayersWithRole(role); } public void clearQueuedGames() { - subPhaseGames.clear(); + subPhaseQueue.clear(); } public void queueGames(List games) { - subPhaseGames.addAll(games); + subPhaseQueue.addAll(games); } public CompletableFuture startNextQueuedMicrogame(final boolean saveInventory) { // No queued games left - if (subPhaseGames.isEmpty()) { + if (subPhaseQueue.isEmpty()) { return CompletableFuture.completedFuture(false); } - final GameConfig nextGame = subPhaseGames.removeFirst(); + final GameConfig nextGame = subPhaseQueue.remove(); return GamePhase.create(game(), nextGame, nextGame.getPlayingPhase(), GamePhaseType.PLAYING).thenApply(result -> { if (result.isOk()) { startSubPhase(result.getOk(), saveInventory); diff --git a/src/main/java/com/lovetropics/minigames/common/core/game/lobby/IGameLobby.java b/src/main/java/com/lovetropics/minigames/common/core/game/lobby/IGameLobby.java index d40e5982c..4da773a05 100644 --- a/src/main/java/com/lovetropics/minigames/common/core/game/lobby/IGameLobby.java +++ b/src/main/java/com/lovetropics/minigames/common/core/game/lobby/IGameLobby.java @@ -23,22 +23,22 @@ public interface IGameLobby { @Nullable default IGame getCurrentGame() { - IGamePhase phase = getCurrentPhase(); + IGamePhase phase = getActivePhase(); return phase != null ? phase.game() : null; } @Nullable default IGameDefinition getCurrentGameDefinition() { - IGamePhase phase = getCurrentPhase(); + IGamePhase phase = getActivePhase(); return phase != null ? phase.definition() : null; } @Nullable - IGamePhase getCurrentPhase(); + IGamePhase getActivePhase(); @Nullable default ClientCurrentGame getClientCurrentGame() { - IGamePhase phase = getCurrentPhase(); + IGamePhase phase = getActivePhase(); return phase != null ? ClientCurrentGame.create(phase) : null; } diff --git a/src/test/java/com/lovetropics/minigames/gametests/ActionTriggerTests.java b/src/test/java/com/lovetropics/minigames/gametests/ActionTriggerTests.java index d8d88f7ee..850537464 100644 --- a/src/test/java/com/lovetropics/minigames/gametests/ActionTriggerTests.java +++ b/src/test/java/com/lovetropics/minigames/gametests/ActionTriggerTests.java @@ -97,7 +97,7 @@ public void testStartTrigger(final LTGameTestHelper helper) { .thenExecute(helper.startGame(lobby)) .thenIdle(20) .thenExecute(() -> helper.assertReceivedPacket(player, 0, ClientboundSystemChatPacket.class, it -> it.content().equals(Component.literal("hello world!")))) - .thenExecute(() -> lobby.getCurrentPhase().invoker(GameActionEvents.APPLY_TO_PLAYER).apply(GameActionContext.EMPTY, player)) + .thenExecute(() -> lobby.getActivePhase().invoker(GameActionEvents.APPLY_TO_PLAYER).apply(GameActionContext.EMPTY, player)) .thenExecute(() -> helper.assertReceivedPacket(player, 1, ClientboundSoundPacket.class, it -> it.getSound().value() == SoundEvents.ALLAY_HURT && it.getVolume() == 0.5f && it.getPitch() == 0.5f)) .thenSucceed(); } @@ -111,7 +111,7 @@ public void testStopTrigger(final LTGameTestHelper helper) { helper.startSequence() .thenExecute(helper.startGame(lobby)) .thenIdle(20) - .thenExecute(() -> lobby.getCurrentPhase().requestStop(GameStopReason.finished())) + .thenExecute(() -> lobby.getActivePhase().requestStop(GameStopReason.finished())) .thenExecute(() -> helper.assertPlayerInventoryContainsAt(player, 0, new ItemStack(Items.OAK_PLANKS, 13))) .thenSucceed(); } diff --git a/src/test/java/com/lovetropics/minigames/gametests/TweakTests.java b/src/test/java/com/lovetropics/minigames/gametests/TweakTests.java index a60f5ea9b..fc92fd363 100644 --- a/src/test/java/com/lovetropics/minigames/gametests/TweakTests.java +++ b/src/test/java/com/lovetropics/minigames/gametests/TweakTests.java @@ -59,7 +59,7 @@ public void testMaxHealth(final LTGameTestHelper helper) { helper.startSequence() .thenExecute(helper.startGame(lobby)) .thenIdle(5) - .thenExecute(() -> lobby.getCurrentPhase().setPlayerRole(player, PlayerRole.PARTICIPANT)) + .thenExecute(() -> lobby.getActivePhase().setPlayerRole(player, PlayerRole.PARTICIPANT)) .thenIdle(5) .thenExecute(() -> helper.assertEntityMaxHealth(player, 30f)) .thenSucceed(); @@ -122,7 +122,7 @@ public void testDisableHunger(final LTGameTestHelper helper) { helper.startSequence() .thenExecute(helper.startGame(lobby)) .thenIdle(5) - .thenExecute(() -> lobby.getCurrentPhase().setPlayerRole(player, PlayerRole.PARTICIPANT)) + .thenExecute(() -> lobby.getActivePhase().setPlayerRole(player, PlayerRole.PARTICIPANT)) .thenExecuteFor(50, player::jumpFromGround) .thenIdle(5) diff --git a/src/test/java/com/lovetropics/minigames/gametests/api/LTGameTestHelper.java b/src/test/java/com/lovetropics/minigames/gametests/api/LTGameTestHelper.java index cc6974290..16e4cf67f 100644 --- a/src/test/java/com/lovetropics/minigames/gametests/api/LTGameTestHelper.java +++ b/src/test/java/com/lovetropics/minigames/gametests/api/LTGameTestHelper.java @@ -600,15 +600,15 @@ public void testStructureLoaded(GameTestInfo pTestInfo) { @Override public void testPassed(GameTestInfo pTest, GameTestRunner pRunner) { - if (lobby.getCurrentPhase() != null) - lobby.getCurrentPhase().requestStop(GameStopReason.finished()); + if (lobby.getActivePhase() != null) + lobby.getActivePhase().requestStop(GameStopReason.finished()); lobby.getManagement().close(); } @Override public void testFailed(GameTestInfo pTest, GameTestRunner pRunner) { - if (lobby.getCurrentPhase() != null) - lobby.getCurrentPhase().requestStop(GameStopReason.canceled()); + if (lobby.getActivePhase() != null) + lobby.getActivePhase().requestStop(GameStopReason.canceled()); lobby.getManagement().close(); } diff --git a/src/test/java/com/lovetropics/minigames/gametests/api/TestGameLobby.java b/src/test/java/com/lovetropics/minigames/gametests/api/TestGameLobby.java index c49767fce..4e860b0cd 100644 --- a/src/test/java/com/lovetropics/minigames/gametests/api/TestGameLobby.java +++ b/src/test/java/com/lovetropics/minigames/gametests/api/TestGameLobby.java @@ -48,8 +48,8 @@ public IGame getCurrentGame() { @Override @Nullable - public IGamePhase getCurrentPhase() { - return delegate.getCurrentPhase(); + public IGamePhase getActivePhase() { + return delegate.getActivePhase(); } @Override