diff --git a/README.md b/README.md new file mode 100644 index 0000000..c39fdf9 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# Pegasus Framework + +## Getting started + +### Creating instance class + +Start by creating an instance class **with this constructor**, and implementing all lifecycle methods. + +All the instance logic goes on this class ! + +```java +public class SampleInstance extends Instance { + public SampleInstance(int id, JavaPlugin plugin, CommonOptions commonOptions, InstanceOptions instanceOptions, ScoreManager scoreManager) { + super(id, plugin, commonOptions, instanceOptions, scoreManager); + } +} +``` + +### Setting up instance behavior + +#### Main plugin class + +Make sure that your plugin extends of PegasusPlugin. + +```java +public class GameSamplePlugin extends PegasusPlugin{ + @Override + public void onEnable(){ + super.onEnable(); + // The initialization code goes here + } +} +``` +#### Creating the game world + +To create the game world, just create a new WorldBuilder, and set on it all the world options that you want ! + +After that, just call the ServerManager#addGameWorld(WorldBuilder) to get the newly created game world. + +_Note : If the game world already exists, the given settings will overwrite the older._ + +```java +public class GameSamplePlugin extends PegasusPlugin{ + @Override + public void onEnable(){ + super.onEnable(); + WorldBuilder gameWorldBuilder = new WorldBuilder("pegasus_sample"); + gameWorldBuilder.setGameMode(GameMode.CREATIVE) + .setDifficulty(Difficulty.PEACEFUL) + .addGameRule(GameRule.DO_DAYLIGHT_CYCLE, false) + .addGameRule(GameRule.DO_WEATHER_CYCLE, false) + .setWorldTime(6000) + .addPrevention(WorldPreventions.ALL) + .addPrevention(WorldPreventions.PREVENT_PVP); + PegasusWorld gameWorld = this.getServerManager().addGameWorld(gameWorldBuilder); + } +} +``` + +#### Creating the game settings + +Start by creating a new OptionsBuilder, and set all the settings you want. + +_Note : Some settings are mandatory to the game be runnable!_ + +After that, you can get the GameManager by calling the ServerManager#createGameManager(DataManager, OptionsBuilder, ScoreManager) method. + +```java +public class GameSamplePlugin extends PegasusPlugin{ + @Override + public void onEnable(){ + super.onEnable(); + // Game world creation code... + OptionsBuilder optionsBuilder = new OptionsBuilder() + .setGameType(GameType.SOLO) + .setWorld(gameWorld) + .setInstanceClass(SampleInstance.class) + .setRoundDurations(List.of(20)) + .setSpawnPoints(List.of(new RelativeLocation(0.5, 0, 0.5, 90, 0))) + .setSchematic(new Schematic(this, "instances_test", SchematicFlags.COPY_BIOMES)) + .setPreAllocatedInstances(1); + gameManager = this.getServerManager().createGameManager(new SampleDataManager(), optionsBuilder, new SampleScoreManager()); + } +} +``` + +#### Starting the game + +To start the game, just call the GameManager#start() method. + +## Lifecycles + +### Instance lifecycle and behavior + +| Instance state | Player game mode | Is player frozen | +|-------------------|------------------|------------------| +| CREATED | Not on instance | Not on instance | +| READY | SPECTATOR | NO | +| PRE_STARTED | SPECTATOR | NO | +| STARTED | SPECTATOR | NO | +| ROUND_PRE_STARTED | ADVENTURE | YES | +| ROUND_STARTED | SURVIVAL | NO | +| ROUND_ENDED | SPECTATOR | NO | +| ENDED | SPECTATOR | NO | +| CLOSED | Not on instance | Not on instance | + +### Instances Manager lifecycle + +| Instance Manager state | Changed when all instances are in state | +|------------------------|-----------------------------------------| +| READY | READY | +| STARTED | STARTED | +| ENDED | CLOSED | + +### Game Manager lifecycle + +| Game Manager state | Changed when... | +|--------------------|----------------------------------------------------------| +| CREATED | when new instance created | +| STARTED | when start method is called | +| ENDED | when stop method is called or Instances Manager is ENDED | + +## How to get which data ? + +### Instance +- Teams and players : Instance#getPlayerManager() +- Score : Instance#getScoreManager() diff --git a/pegasus-framework.iml b/pegasus-framework.iml index 3b0c98f..c974104 100644 --- a/pegasus-framework.iml +++ b/pegasus-framework.iml @@ -5,4 +5,15 @@ + + + + + PAPER + ADVENTURE + + 1 + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index dbaa1a1..0824601 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,10 @@ papermc https://repo.papermc.io/repository/maven-public/ + + enginehub-maven + https://maven.enginehub.org/repo/ + @@ -28,6 +32,24 @@ 1.20.4-R0.1-SNAPSHOT provided + + com.sk89q.worldedit + worldedit-core + 7.2.14 + provided + + + com.sk89q.worldedit + worldedit-bukkit + 7.2.14 + provided + + + org.junit.jupiter + junit-jupiter + 5.8.1 + test + diff --git a/src/main/java/fr/pegasus/papermc/PegasusPlugin.java b/src/main/java/fr/pegasus/papermc/PegasusPlugin.java new file mode 100644 index 0000000..e049677 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/PegasusPlugin.java @@ -0,0 +1,37 @@ +package fr.pegasus.papermc; + +import fr.pegasus.papermc.managers.ServerManager; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.logging.Logger; + +@SuppressWarnings("unused") +public class PegasusPlugin extends JavaPlugin { + + public static Logger logger = Logger.getLogger("PegasusFramework"); + private ServerManager serverManager; + + @Override + public void onLoad() { + super.onLoad(); + logger.info("Pegasus Framework has been loaded"); + } + + @Override + public void onEnable() { + super.onEnable(); + serverManager = new ServerManager(this); + serverManager.initLobby(); + logger.info("Pegasus Framework has been enabled"); + } + + @Override + public void onDisable() { + super.onDisable(); + logger.info("Pegasus Framework has been disabled"); + } + + public ServerManager getServerManager() { + return serverManager; + } +} diff --git a/src/main/java/fr/pegasus/papermc/games/GameManager.java b/src/main/java/fr/pegasus/papermc/games/GameManager.java new file mode 100644 index 0000000..06f15a2 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/GameManager.java @@ -0,0 +1,195 @@ +package fr.pegasus.papermc.games; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import fr.pegasus.papermc.PegasusPlugin; +import fr.pegasus.papermc.games.enums.GameManagerStates; +import fr.pegasus.papermc.games.enums.InstanceManagerStates; +import fr.pegasus.papermc.games.events.InstanceManagerStateChangedEvent; +import fr.pegasus.papermc.games.instances.GameType; +import fr.pegasus.papermc.games.instances.InstancesManager; +import fr.pegasus.papermc.games.options.CommonOptions; +import fr.pegasus.papermc.games.options.OptionsBuilder; +import fr.pegasus.papermc.scores.ScoreManager; +import fr.pegasus.papermc.teams.Team; +import fr.pegasus.papermc.teams.TeamManager; +import fr.pegasus.papermc.teams.loaders.DataManager; +import fr.pegasus.papermc.utils.PegasusPlayer; +import fr.pegasus.papermc.worlds.PegasusWorld; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class GameManager implements Listener { + + private final PegasusWorld lobbyWorld; + private final CommonOptions commonOptions; + private final TeamManager teamManager; + private final InstancesManager instancesManager; + + private GameManagerStates state = GameManagerStates.CREATED; + + /** + * Create a new GameManager + * @param plugin The plugin instance + * @param lobbyWorld The lobby world + * @param dataManager The data manager (for the {@link TeamManager}) + * @param optionsBuilder The options builder + * @param scoreManager The score manager + */ + public GameManager( + final @NotNull JavaPlugin plugin, + final @NotNull PegasusWorld lobbyWorld, + final @NotNull DataManager dataManager, + final @NotNull OptionsBuilder optionsBuilder, + final @NotNull ScoreManager scoreManager + ) { + this.lobbyWorld = lobbyWorld; + this.commonOptions = optionsBuilder.getCommonOptions(); + this.teamManager = new TeamManager(dataManager); + this.instancesManager = new InstancesManager(plugin, optionsBuilder, scoreManager); + plugin.getServer().getPluginManager().registerEvents(this, plugin); + } + + /** + * Constructor for test purposes only + */ + public GameManager(){ + PegasusPlugin.logger.warning("Constructor called for test purposes only"); + this.lobbyWorld = null; + this.commonOptions = null; + this.teamManager = null; + this.instancesManager = null; + } + + /** + * Start the game manager + * @param ignoreBalancedPlayers Ignore the balanced players check + * @return True if the game manager is started, false otherwise + */ + public boolean start(final boolean ignoreBalancedPlayers){ + if(state != GameManagerStates.CREATED) + return false; + state = GameManagerStates.STARTED; + Set teams = this.teamManager.getTeams(); + if(this.isEmptyTeam(teams)) + return false; + if(!checkMinimumTeams(teams, commonOptions.getGameType())) + return false; + if(!ignoreBalancedPlayers && !checkBalancedTeams(teams, commonOptions.getGameType())) + return false; + List> instanceTeams = this.generateTeams(teams); + this.instancesManager.dispatchTeams(instanceTeams); + return true; + } + + /** + * Stop the game manager and all its instances + */ + public void stop(){ + if(state != GameManagerStates.STARTED) + return; + this.instancesManager.stopInstances(); + state = GameManagerStates.ENDED; + } + + /** + * Generate the teams for each game type + * @param teams The teams to generate + * @return The generated teams + */ + public List> generateTeams(@NotNull final Set teams){ + List> instanceTeams = new ArrayList<>(); + switch (this.commonOptions.getGameType()){ + case SOLO -> { + for(Team team : teams) + for(PegasusPlayer pPlayer : team.players()) + instanceTeams.add(Lists.newArrayList(new Team(team.teamTag(), team.teamName(), Sets.newHashSet(pPlayer)))); + } + case TEAM_ONLY -> throw new UnsupportedOperationException("Team only game type is not supported yet"); + case TEAM_VS_TEAM -> throw new UnsupportedOperationException("Team vs team game type is not supported yet"); + case FFA -> throw new UnsupportedOperationException("FFA game type is not supported yet"); + } + return instanceTeams; + } + + /** + * Check if there is an empty team + * @param teams The teams to check + * @return True if there is an empty team, false otherwise + */ + private boolean isEmptyTeam(final @NotNull Set teams){ + for(Team t : teams) + if(t.players().isEmpty()) + return true; + return false; + } + + /** + * Check if the minimum number of teams is reached for each game type + * @param teams The teams to check + * @param gameType The game type + * @return True if the minimum number of teams is reached, false otherwise + */ + private boolean checkMinimumTeams(final @NotNull Set teams, final @NotNull GameType gameType){ + return switch (gameType) { + case SOLO, TEAM_ONLY, FFA -> !teams.isEmpty(); + case TEAM_VS_TEAM -> teams.size() >= 2; + }; + } + + /** + * Check if the teams are balanced for each game type + * @param teams The teams to check + * @param gameType The game type + * @return True if the teams are balanced, false otherwise + */ + private boolean checkBalancedTeams(final @NotNull Set teams, final @NotNull GameType gameType){ + return switch (gameType) { + case SOLO, TEAM_ONLY, FFA -> !teams.isEmpty(); + case TEAM_VS_TEAM -> teams.size() % 2 == 0; + }; + } + + /** + * Get the current game manager state + * @return The {@link GameManagerStates} + */ + public GameManagerStates getState() { + return this.state; + } + + /** + * Check if the player is in any of the instances + * @param player The {@link PegasusPlayer} to check + * @return True if the player is in any of the instances, false otherwise + */ + public boolean isPlayerInGame(@NotNull final PegasusPlayer player){ + return this.instancesManager.isPlayerInInstances(player); + } + + /** + * Handle the instance manager state changed event + * @param e The {@link InstanceManagerStateChangedEvent} + */ + @EventHandler + public void onInstanceManagerStateChanged(@NotNull final InstanceManagerStateChangedEvent e){ + PegasusPlugin.logger.info("Instance manager state changed from %s to %s".formatted(e.getOldState(), e.getNewState())); + if(e.getNewState() == InstanceManagerStates.ENDED){ + this.commonOptions.getWorld().getWorld().getPlayers().forEach(player -> { + player.getInventory().clear(); + player.teleport(this.lobbyWorld.getSpawnPoint()); + player.setRespawnLocation(this.lobbyWorld.getSpawnPoint()); + }); + state = GameManagerStates.ENDED; + } + if(e.getNewState() != InstanceManagerStates.READY || this.state != GameManagerStates.STARTED) + return; + this.instancesManager.startInstances(); + } +} diff --git a/src/main/java/fr/pegasus/papermc/games/enums/GameManagerStates.java b/src/main/java/fr/pegasus/papermc/games/enums/GameManagerStates.java new file mode 100644 index 0000000..5e43194 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/enums/GameManagerStates.java @@ -0,0 +1,7 @@ +package fr.pegasus.papermc.games.enums; + +public enum GameManagerStates { + CREATED, + STARTED, + ENDED, +} diff --git a/src/main/java/fr/pegasus/papermc/games/enums/InstanceManagerStates.java b/src/main/java/fr/pegasus/papermc/games/enums/InstanceManagerStates.java new file mode 100644 index 0000000..1024b16 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/enums/InstanceManagerStates.java @@ -0,0 +1,7 @@ +package fr.pegasus.papermc.games.enums; + +public enum InstanceManagerStates { + READY, + STARTED, + ENDED +} diff --git a/src/main/java/fr/pegasus/papermc/games/events/InstanceManagerStateChangedEvent.java b/src/main/java/fr/pegasus/papermc/games/events/InstanceManagerStateChangedEvent.java new file mode 100644 index 0000000..f53a53d --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/events/InstanceManagerStateChangedEvent.java @@ -0,0 +1,47 @@ +package fr.pegasus.papermc.games.events; + +import fr.pegasus.papermc.games.enums.InstanceManagerStates; +import fr.pegasus.papermc.games.instances.InstancesManager; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class InstanceManagerStateChangedEvent extends Event { + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final InstancesManager instanceManager; + private final InstanceManagerStates oldState; + private final InstanceManagerStates newState; + + public InstanceManagerStateChangedEvent( + final @NotNull InstancesManager instanceManager, + final @Nullable InstanceManagerStates oldState, + final @NotNull InstanceManagerStates newState + ){ + this.instanceManager = instanceManager; + this.oldState = oldState; + this.newState = newState; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } + + @Override + @NotNull + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public InstancesManager getInstanceManager() { + return instanceManager; + } + @Nullable + public InstanceManagerStates getOldState() { + return oldState; + } + public InstanceManagerStates getNewState() { + return newState; + } +} diff --git a/src/main/java/fr/pegasus/papermc/games/events/InstanceStateChangedEvent.java b/src/main/java/fr/pegasus/papermc/games/events/InstanceStateChangedEvent.java new file mode 100644 index 0000000..d90c351 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/events/InstanceStateChangedEvent.java @@ -0,0 +1,44 @@ +package fr.pegasus.papermc.games.events; + +import fr.pegasus.papermc.games.instances.Instance; +import fr.pegasus.papermc.games.instances.enums.InstanceStates; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class InstanceStateChangedEvent extends Event { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final Instance instance; + private final InstanceStates oldState; + private final InstanceStates newState; + + public InstanceStateChangedEvent(final @NotNull Instance instance, final @Nullable InstanceStates oldState, final @NotNull InstanceStates newState){ + this.instance = instance; + this.oldState = oldState; + this.newState = newState; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } + + @Override + @NotNull + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public Instance getInstance() { + return instance; + } + @Nullable + public InstanceStates getOldState() { + return oldState; + } + public InstanceStates getNewState() { + return newState; + } +} diff --git a/src/main/java/fr/pegasus/papermc/games/instances/GameType.java b/src/main/java/fr/pegasus/papermc/games/instances/GameType.java new file mode 100644 index 0000000..8efee91 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/instances/GameType.java @@ -0,0 +1,8 @@ +package fr.pegasus.papermc.games.instances; + +public enum GameType { + SOLO, + TEAM_ONLY, + TEAM_VS_TEAM, + FFA +} diff --git a/src/main/java/fr/pegasus/papermc/games/instances/Instance.java b/src/main/java/fr/pegasus/papermc/games/instances/Instance.java new file mode 100644 index 0000000..6d2dff2 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/instances/Instance.java @@ -0,0 +1,309 @@ +package fr.pegasus.papermc.games.instances; + +import com.sk89q.worldedit.WorldEditException; +import fr.pegasus.papermc.PegasusPlugin; +import fr.pegasus.papermc.games.events.InstanceStateChangedEvent; +import fr.pegasus.papermc.games.instances.enums.InstanceStates; +import fr.pegasus.papermc.games.options.CommonOptions; +import fr.pegasus.papermc.games.options.InstanceOptions; +import fr.pegasus.papermc.scores.ScoreManager; +import fr.pegasus.papermc.teams.PlayerManager; +import fr.pegasus.papermc.teams.Team; +import fr.pegasus.papermc.utils.Countdown; +import fr.pegasus.papermc.utils.PegasusPlayer; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +public abstract class Instance implements Listener { + + private final int id; + private final JavaPlugin plugin; + private final CommonOptions commonOptions; + private final InstanceOptions instanceOptions; + private final ScoreManager scoreManager; + + private final PlayerManager playerManager; + private InstanceStates state; + private final Location instanceLocation; + private int currentRound = 1; + + public Instance( + int id, + @NotNull final JavaPlugin plugin, + @NotNull final CommonOptions commonOptions, + @NotNull final InstanceOptions instanceOptions, + @NotNull final ScoreManager scoreManager + ) { + this.id = id; + this.plugin = plugin; + this.commonOptions = commonOptions; + this.instanceOptions = instanceOptions; + this.scoreManager = scoreManager; + this.playerManager = new PlayerManager(plugin); + this.instanceLocation = new Location(commonOptions.getWorld().getWorld(), id * 1000, 100, 0); + plugin.getServer().getPluginManager().registerEvents(this, plugin); + // Paste the schematic + try { + this.instanceOptions.getSchematic().paste(this.instanceLocation); + } catch (WorldEditException e) { + throw new RuntimeException(e); + } + this.updateState(InstanceStates.CREATED); + } + + // LIFECYCLE METHODS + + /** + * Affect and teleport the teams to this instance + * @param teams The teams to affect + */ + public final void affect(@NotNull final List teams){ + this.playerManager.setTeams(teams); + this.playerManager.setFrozenAll(true); + // Affect spawns and teleport players to it + this.playerManager.affectSpawns(this.instanceOptions.getSpawnPoints(), this.commonOptions.getGameType()); + for(PegasusPlayer pPlayer : this.playerManager.getPlayerSpawns().keySet()){ + if(!pPlayer.isOnline()){ + this.playerManager.playerDisconnect(pPlayer, this.state); + continue; + } + Player player = pPlayer.getPlayer(); + player.setRespawnLocation(this.playerManager.getPlayerSpawns().get(pPlayer).toAbsolute(this.instanceLocation)); + player.teleport(this.playerManager.getPlayerSpawns().get(pPlayer).toAbsolute(this.instanceLocation)); + } + this.updateState(InstanceStates.READY); + this.onReady(); + } + + /** + * Start the instance + */ + public final void start(){ + if(this.state != InstanceStates.READY) + throw new IllegalStateException("Instance %d is not ready to start".formatted(this.id)); + this.updateState(InstanceStates.PRE_STARTED); + // Pre-start code + this.playerManager.setGameModeAll(GameMode.SPECTATOR); + this.playerManager.setFrozenAll(false); + this.onPreStart(); + new Countdown(10, i -> this.playerManager.getGlobalAnnouncer().announceChat("Starting in %d seconds".formatted(i)), () -> { + this.updateState(InstanceStates.STARTED); + // Start code + this.onStart(); + this.startRound(); + }).start(this.plugin); + } + + /** + * Start a round + */ + public final void startRound(){ + if(state != InstanceStates.STARTED && state != InstanceStates.ROUND_ENDED) + throw new IllegalStateException("Instance %d is not ready to start a round".formatted(this.id)); + this.updateState(InstanceStates.ROUND_PRE_STARTED); + // Round pre-start code + this.playerManager.teleportAllToSpawns(this.instanceLocation); + this.playerManager.setGameModeAll(GameMode.ADVENTURE); + this.playerManager.setFrozenAll(true); + this.onRoundPreStart(); + new Countdown(5, i -> this.playerManager.getGlobalAnnouncer().announceChat("Round starting in %d seconds".formatted(i)), () -> { + this.updateState(InstanceStates.ROUND_STARTED); + // Round start code + this.playerManager.setFrozenAll(false); + this.playerManager.setGameModeAll(GameMode.SURVIVAL); + this.onRoundStart(); + new Countdown(this.instanceOptions.getRoundDurations().get(this.currentRound - 1), this::onTick, this::endRound).start(this.plugin); + }).start(this.plugin); + } + + /** + * End a round + */ + public final void endRound(){ + if(state != InstanceStates.ROUND_STARTED) + throw new IllegalStateException("Instance %d is not ready to end a round".formatted(this.id)); + this.updateState(InstanceStates.ROUND_ENDED); + // End code + this.playerManager.setGameModeAll(GameMode.SPECTATOR); + // + for(Map.Entry keySet : this.playerManager.getDisconnectedPlayers().entrySet()) + if(keySet.getValue() == InstanceStates.ROUND_STARTED) + this.playerManager.getDisconnectedPlayers().put(keySet.getKey(), InstanceStates.ROUND_PRE_STARTED); + this.onRoundEnd(); + this.currentRound++; + if(this.isGameEnd()) + new Countdown( + 3, + i -> this.playerManager.getGlobalAnnouncer().announceChat("Game end in %d seconds".formatted(i)), () -> + this.stop(false)) + .start(this.plugin); + else + new Countdown( + 3, + i -> this.playerManager.getGlobalAnnouncer().announceChat("Next round starting in %d seconds".formatted(i)), + this::startRound) + .start(this.plugin); + } + + /** + * Stop the instance + * @param force Force the instance to stop + */ + public final void stop(boolean force){ + if(state != InstanceStates.ROUND_ENDED) + throw new IllegalStateException("Instance %d is not ready to end".formatted(this.id)); + this.updateState(InstanceStates.ENDED); + this.onEnd(); + if(!force) + new Countdown(10, i -> this.playerManager.getGlobalAnnouncer().announceChat("Instance closing in %d seconds".formatted(i)), () -> { + this.unregisterEvents(); + this.updateState(InstanceStates.CLOSED); + }).start(this.plugin); + else{ + this.unregisterEvents(); + this.updateState(InstanceStates.CLOSED); + } + } + + public abstract void onReady(); + public abstract void onPreStart(); + public abstract void onStart(); + public abstract void onRoundPreStart(); + public abstract void onRoundStart(); + public abstract void onRoundEnd(); + public abstract void onEnd(); + public abstract void onTick(int remainingTime); + public abstract void onPlayerReconnect( + @NotNull final Player player, + @NotNull final InstanceStates disconnectState, + @NotNull final InstanceStates reconnectState + ); + + // UTILS + + /** + * Set the state of the instance + * @param state The new {@link InstanceStates} + */ + private void updateState(@NotNull final InstanceStates state){ + InstanceStates oldState = this.state; + this.state = state; + new InstanceStateChangedEvent(this, oldState, this.state).callEvent(); + } + + /** + * Check if the game is ended + * @return True if the game is ended + */ + private boolean isGameEnd(){ + return this.instanceOptions.getRoundDurations().size() < this.currentRound; + } + + /** + * Check if the player is in this instance + * @param pPlayer The player to check + * @return true if the player is in this instance + */ + private boolean isPlayerInInstance(@NotNull final PegasusPlayer pPlayer){ + for(PegasusPlayer pPlayerInInstance : this.playerManager.getPlayers()) + if(pPlayerInInstance.equals(pPlayer)) + return true; + return false; + } + + /** + * Check if the player is in this instance + * @param playerName The player name to check + * @return True if the player is in this instance + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean isPlayerInInstance(@NotNull final String playerName){ + if(this.state == InstanceStates.CREATED) + return false; + return this.isPlayerInInstance(new PegasusPlayer(playerName)); + } + + /** + * Unregister the events + */ + private void unregisterEvents(){ + PlayerJoinEvent.getHandlerList().unregister(this); + PlayerQuitEvent.getHandlerList().unregister(this); + PlayerTeleportEvent.getHandlerList().unregister(this); + } + + // GETTERS + + public final int getId() { + return id; + } + @Nullable + public final InstanceStates getState() { + return state; + } + public final ScoreManager getScoreManager() { + return scoreManager; + } + public final PlayerManager getPlayerManager() { + return playerManager; + } + + // EVENTS + + /** + * Handle the player reconnection + * @param e The {@link PlayerJoinEvent} + */ + @EventHandler(priority = EventPriority.HIGH) + public final void onPlayerJoin(@NotNull final PlayerJoinEvent e){ + if(!isPlayerInInstance(e.getPlayer().getName())) + return; + PegasusPlayer pPlayer = new PegasusPlayer(e.getPlayer().getName()); + Location teleportLocation = this.playerManager.getPlayerSpawns().get(pPlayer).toAbsolute(this.instanceLocation); + e.getPlayer().teleport(teleportLocation); + switch (this.state){ + case ROUND_PRE_STARTED -> e.getPlayer().setGameMode(GameMode.ADVENTURE); + case ROUND_STARTED -> e.getPlayer().setGameMode(GameMode.SURVIVAL); + default -> e.getPlayer().setGameMode(GameMode.SPECTATOR); + } + this.onPlayerReconnect(e.getPlayer(), this.playerManager.playerReconnect(pPlayer), this.state); + PegasusPlugin.logger.info("Player %s reconnected to instance %d".formatted(e.getPlayer().getName(), this.id)); + } + + /** + * Handle the player disconnection + * @param e The {@link PlayerQuitEvent} + */ + @EventHandler + public final void onPlayerQuit(@NotNull final PlayerQuitEvent e){ + if(!this.isPlayerInInstance(e.getPlayer().getName())) + return; + this.playerManager.playerDisconnect(new PegasusPlayer(e.getPlayer().getName()), this.state); + } + + /** + * Handle the player teleport + * @param e The {@link PlayerTeleportEvent} + */ + @EventHandler + public final void onPlayerTeleport(@NotNull final PlayerTeleportEvent e){ + if(!this.isPlayerInInstance(e.getPlayer().getName())) + return; + // Prevent spectator teleport for instance players + if(e.getCause() == PlayerTeleportEvent.TeleportCause.SPECTATE) + e.setCancelled(true); + } +} diff --git a/src/main/java/fr/pegasus/papermc/games/instances/InstancesManager.java b/src/main/java/fr/pegasus/papermc/games/instances/InstancesManager.java new file mode 100644 index 0000000..1bb5a7f --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/instances/InstancesManager.java @@ -0,0 +1,183 @@ +package fr.pegasus.papermc.games.instances; + +import fr.pegasus.papermc.PegasusPlugin; +import fr.pegasus.papermc.games.enums.InstanceManagerStates; +import fr.pegasus.papermc.games.events.InstanceManagerStateChangedEvent; +import fr.pegasus.papermc.games.events.InstanceStateChangedEvent; +import fr.pegasus.papermc.games.instances.enums.InstanceStates; +import fr.pegasus.papermc.games.options.OptionsBuilder; +import fr.pegasus.papermc.scores.ScoreManager; +import fr.pegasus.papermc.teams.Team; +import fr.pegasus.papermc.utils.PegasusPlayer; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +public class InstancesManager implements Listener { + + private final JavaPlugin plugin; + private final OptionsBuilder optionsBuilder; + private final ScoreManager scoreManager; + private final List instances; + private InstanceManagerStates state; + + /** + * Create a new InstancesManager + * @param plugin The plugin instance + * @param optionsBuilder The options builder + * @param scoreManager The score manager + */ + public InstancesManager( + final @NotNull JavaPlugin plugin, + final @NotNull OptionsBuilder optionsBuilder, + final @NotNull ScoreManager scoreManager + ){ + this.plugin = plugin; + this.optionsBuilder = optionsBuilder; + this.scoreManager = scoreManager; + this.instances = new ArrayList<>(); + plugin.getServer().getPluginManager().registerEvents(this, plugin); + // Pre allocate instances + this.allocateInstances(this.optionsBuilder.getGameOptions().getPreAllocatedInstances()); + } + + /** + * Allocate instances if needed + * @param count The number of instances to allocate + */ + public void allocateInstances(int count){ + if(count == 0 || count == this.instances.size()){ + PegasusPlugin.logger.info("No instances to allocate"); + return; + } + // If the count is less than the current instances count, unallocate the instances + if(count < this.instances.size()){ + PegasusPlugin.logger.info("Too many instances allocated, unallocating %s instances".formatted(this.instances.size() - count)); + this.instances.subList(count, this.instances.size()).clear(); + PegasusPlugin.logger.info("Unallocated %s instances".formatted(this.instances.size() - count)); + return; + } + // If the count is greater than the current instances count, allocate the missing instances + int toAllocate = count - this.instances.size(); + for(int i = this.instances.size(); i < count; i++){ + PegasusPlugin.logger.info("Allocating instance %d of %d".formatted(i + 1, count)); + Instance instance = createInstance(this.instances.size()); + this.instances.add(instance); + } + PegasusPlugin.logger.info("Allocated %s instances".formatted(toAllocate)); + } + + /** + * Create a new instance and add it to the instances list + * @return The created {@link Instance} + */ + private Instance createInstance(int id){ + try { + return (Instance) this.optionsBuilder.getGameOptions().getInstanceClass().getConstructors()[0].newInstance( + id, + this.plugin, + this.optionsBuilder.getCommonOptions(), + this.optionsBuilder.getInstanceOptions(), + this.scoreManager); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + /** + * Dispatch teams to instances + * @param teams The teams to dispatch (created in the GameManager) + */ + public void dispatchTeams(@NotNull final List> teams){ + // Allocate instances if needed + this.allocateInstances(teams.size()); + for(int i = 0; i < teams.size(); i++) + this.instances.get(i).affect(teams.get(i)); + } + + /** + * Start all instances + */ + public void startInstances(){ + this.instances.forEach(Instance::start); + } + + /** + * Stop all instances + */ + public void stopInstances(){ + this.instances.forEach(instance -> instance.stop(true)); + } + + /** + * Get the list of created instances + * @return The list of created {@link Instance} + */ + public List getInstances() { + return this.instances; + } + + /** + * Check if the player is in any of the instances + * @param player The {@link PegasusPlayer} to check + * @return True if the player is in any of the instances, false otherwise + */ + public boolean isPlayerInInstances(@NotNull final PegasusPlayer player){ + for(Instance instance : this.instances) + if(instance.getPlayerManager().getPlayers().contains(player)) + return true; + return false; + } + + /** + * Check if all instances have the target state + * @param targetState The target {@link InstanceStates} + * @return True if all instances have the target state, false otherwise + */ + private boolean isInstancesHasState(@NotNull final InstanceStates targetState){ + for(Instance instance : this.instances) + if(instance.getState() != targetState) + return false; + return true; + } + + /** + * Update the state of the instance manager + * @param newState The new {@link InstanceManagerStates} + */ + private void updateState(@NotNull final InstanceManagerStates newState){ + InstanceManagerStates oldState = this.state; + this.state = newState; + new InstanceManagerStateChangedEvent(this, oldState, this.state).callEvent(); + } + + /** + * Handle instance state changed event + * @param e The {@link InstanceStateChangedEvent} + */ + @EventHandler + public void onInstanceStateChanged(@NotNull final InstanceStateChangedEvent e){ + PegasusPlugin.logger.info("Instance %d changed state from %s to %s".formatted(e.getInstance().getId(), e.getOldState(), e.getNewState())); + switch (e.getNewState()){ + case READY -> { + if(this.isInstancesHasState(InstanceStates.READY)) + this.updateState(InstanceManagerStates.READY); + } + case STARTED -> { + if(this.isInstancesHasState(InstanceStates.STARTED)) + this.updateState(InstanceManagerStates.STARTED); + } + case CLOSED -> { + if(this.isInstancesHasState(InstanceStates.CLOSED)){ + this.instances.clear(); + this.updateState(InstanceManagerStates.ENDED); + } + } + } + } +} diff --git a/src/main/java/fr/pegasus/papermc/games/instances/enums/InstanceStates.java b/src/main/java/fr/pegasus/papermc/games/instances/enums/InstanceStates.java new file mode 100644 index 0000000..3cbbb57 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/instances/enums/InstanceStates.java @@ -0,0 +1,13 @@ +package fr.pegasus.papermc.games.instances.enums; + +public enum InstanceStates { + CREATED, // When the instance is created and the schematic is pasted + READY, // When the instance is ready to be started after the players are affected and teleported + PRE_STARTED, // When the start countdown is running + STARTED, // When the instance is started + ROUND_PRE_STARTED, // When the round start countdown is running + ROUND_STARTED, // When the round is started + ROUND_ENDED, // When the round is ended + ENDED, // When the instance is ended + CLOSED, // When the instance is closed, players are unaffected +} diff --git a/src/main/java/fr/pegasus/papermc/games/options/CommonOptions.java b/src/main/java/fr/pegasus/papermc/games/options/CommonOptions.java new file mode 100644 index 0000000..006bf93 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/options/CommonOptions.java @@ -0,0 +1,26 @@ +package fr.pegasus.papermc.games.options; + +import fr.pegasus.papermc.games.instances.GameType; +import fr.pegasus.papermc.worlds.PegasusWorld; + +public class CommonOptions { + + private GameType gameType; + private PegasusWorld world; + + public GameType getGameType() { + return gameType; + } + + public void setGameType(GameType gameType) { + this.gameType = gameType; + } + + public PegasusWorld getWorld() { + return world; + } + + public void setWorld(PegasusWorld world) { + this.world = world; + } +} diff --git a/src/main/java/fr/pegasus/papermc/games/options/GameOptions.java b/src/main/java/fr/pegasus/papermc/games/options/GameOptions.java new file mode 100644 index 0000000..449e3dd --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/options/GameOptions.java @@ -0,0 +1,25 @@ +package fr.pegasus.papermc.games.options; + +import fr.pegasus.papermc.games.instances.Instance; + +public class GameOptions { + + private Class instanceClass; + private int preAllocatedInstances = 0; + + public Class getInstanceClass() { + return instanceClass; + } + + public void setInstanceClass(Class instanceClass) { + this.instanceClass = instanceClass; + } + + public int getPreAllocatedInstances() { + return preAllocatedInstances; + } + + public void setPreAllocatedInstances(int preAllocatedInstances) { + this.preAllocatedInstances = preAllocatedInstances; + } +} diff --git a/src/main/java/fr/pegasus/papermc/games/options/InstanceOptions.java b/src/main/java/fr/pegasus/papermc/games/options/InstanceOptions.java new file mode 100644 index 0000000..18cb957 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/options/InstanceOptions.java @@ -0,0 +1,47 @@ +package fr.pegasus.papermc.games.options; + +import fr.pegasus.papermc.worlds.locations.RelativeLocation; +import fr.pegasus.papermc.worlds.schematics.Schematic; + +import java.util.List; +import java.util.Map; + +public class InstanceOptions { + private List roundDurations; + // TODO: Countdown duration + private List spawnPoints; + private Schematic schematic; + private Map customOptions; + + public List getRoundDurations() { + return roundDurations; + } + + public void setRoundDurations(List roundDurations) { + this.roundDurations = roundDurations; + } + + public List getSpawnPoints() { + return spawnPoints; + } + + public void setSpawnPoints(List spawnPoints) { + this.spawnPoints = spawnPoints; + } + + public Schematic getSchematic() { + return schematic; + } + + public void setSchematic(Schematic schematic) { + this.schematic = schematic; + } + + public Map getCustomOptions() { + return customOptions; + } + + public void setCustomOptions(Map customOptions) { + this.customOptions = customOptions; + } +} diff --git a/src/main/java/fr/pegasus/papermc/games/options/OptionsBuilder.java b/src/main/java/fr/pegasus/papermc/games/options/OptionsBuilder.java new file mode 100644 index 0000000..786e979 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/games/options/OptionsBuilder.java @@ -0,0 +1,82 @@ +package fr.pegasus.papermc.games.options; + +import fr.pegasus.papermc.games.instances.GameType; +import fr.pegasus.papermc.games.instances.Instance; +import fr.pegasus.papermc.worlds.PegasusWorld; +import fr.pegasus.papermc.worlds.locations.RelativeLocation; +import fr.pegasus.papermc.worlds.schematics.Schematic; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class OptionsBuilder { + + private final GameOptions gameOptions; + private final InstanceOptions instanceOptions; + private final CommonOptions commonOptions; + + public OptionsBuilder() { + this.gameOptions = new GameOptions(); + this.instanceOptions = new InstanceOptions(); + this.commonOptions = new CommonOptions(); + } + + public OptionsBuilder setInstanceClass(Class instanceClass){ + gameOptions.setInstanceClass(instanceClass); + return this; + } + public OptionsBuilder setPreAllocatedInstances(int preAllocatedInstances){ + gameOptions.setPreAllocatedInstances(preAllocatedInstances); + return this; + } + public OptionsBuilder setRoundDurations(List roundDurations){ + instanceOptions.setRoundDurations(roundDurations); + return this; + } + public OptionsBuilder setSpawnPoints(List spawnPoints){ + instanceOptions.setSpawnPoints(spawnPoints); + return this; + } + public OptionsBuilder setSchematic(Schematic schematic){ + instanceOptions.setSchematic(schematic); + return this; + } + public OptionsBuilder setCustomOptions(Map customOptions){ + instanceOptions.setCustomOptions(customOptions); + return this; + } + public OptionsBuilder setGameType(GameType gameType){ + commonOptions.setGameType(gameType); + return this; + } + public OptionsBuilder setWorld(PegasusWorld world){ + commonOptions.setWorld(world); + return this; + } + + public GameOptions getGameOptions(){ + if( + Objects.nonNull(gameOptions.getInstanceClass()) + ) + return gameOptions; + throw new IllegalStateException("GameOptions are not fully initialized"); + } + public InstanceOptions getInstanceOptions(){ + if( + Objects.nonNull(instanceOptions.getRoundDurations()) && !instanceOptions.getRoundDurations().isEmpty() && + Objects.nonNull(instanceOptions.getSpawnPoints()) && !instanceOptions.getSpawnPoints().isEmpty() && + Objects.nonNull(instanceOptions.getSchematic()) + ) + return instanceOptions; + throw new IllegalStateException("InstanceOptions are not fully initialized"); + } + public CommonOptions getCommonOptions(){ + if( + Objects.nonNull(commonOptions.getGameType()) && + Objects.nonNull(commonOptions.getWorld()) + ) + return commonOptions; + throw new IllegalStateException("CommonOptions are not fully initialized"); + } +} diff --git a/src/main/java/fr/pegasus/papermc/managers/ServerManager.java b/src/main/java/fr/pegasus/papermc/managers/ServerManager.java new file mode 100644 index 0000000..c0d89b6 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/managers/ServerManager.java @@ -0,0 +1,142 @@ +package fr.pegasus.papermc.managers; + +import fr.pegasus.papermc.PegasusPlugin; +import fr.pegasus.papermc.games.GameManager; +import fr.pegasus.papermc.games.options.OptionsBuilder; +import fr.pegasus.papermc.scores.ScoreManager; +import fr.pegasus.papermc.teams.loaders.DataManager; +import fr.pegasus.papermc.utils.PegasusPlayer; +import fr.pegasus.papermc.worlds.PegasusWorld; +import fr.pegasus.papermc.worlds.WorldBuilder; +import fr.pegasus.papermc.worlds.WorldPreventions; +import fr.pegasus.papermc.worlds.schematics.Schematic; +import org.bukkit.Difficulty; +import org.bukkit.GameMode; +import org.bukkit.GameRule; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +import static org.bukkit.Bukkit.getServer; + +public class ServerManager implements Listener { + + private final JavaPlugin plugin; + + private PegasusWorld lobbyWorld; + private final List gameWorlds; + private final List gameManagers; + + /** + * Create a new ServerManager + * @param plugin The plugin instance + */ + public ServerManager(final @NotNull JavaPlugin plugin){ + this.plugin = plugin; + this.gameWorlds = new ArrayList<>(); + this.gameManagers = new ArrayList<>(); + getServer().getPluginManager().registerEvents(this, plugin); + } + + /** + * Initialize the lobby world with the default schematic named "lobby" + */ + public void initLobby(){ + this.initLobby(new Schematic(this.plugin, "lobby")); + } + + /** + * Initialize the lobby world with the given schematic + * @param schematic The schematic to use for the lobby world + */ + public void initLobby(final @NotNull Schematic schematic){ + PegasusPlugin.logger.info("Initializing lobby world"); + // Create lobby world + this.lobbyWorld = new WorldBuilder("pegasus_lobby") + .setGameMode(GameMode.SURVIVAL) + .setDifficulty(Difficulty.PEACEFUL) + .addPrevention(WorldPreventions.ALL) + .addGameRule(GameRule.DO_DAYLIGHT_CYCLE, false) + .addGameRule(GameRule.DO_WEATHER_CYCLE, false) + .setDefaultSchematic(schematic) + .setWorldTime(6000) + .make(this.plugin); + // Unload default world + PegasusPlugin.logger.info("Unloading default world"); + String defaultWorldName = this.plugin.getServer().getWorlds().getFirst().getName(); + this.plugin.getServer().unloadWorld(defaultWorldName, false); + String defaultNetherWorldName = defaultWorldName + "_nether"; + this.plugin.getServer().unloadWorld(defaultNetherWorldName, false); + String defaultEndWorldName = defaultWorldName + "_the_end"; + this.plugin.getServer().unloadWorld(defaultEndWorldName, false); + PegasusPlugin.logger.info("Default world unloaded"); + } + + /** + * Add a game world to the server + * @param worldBuilder The world builder to use to create the game world + */ + public PegasusWorld addGameWorld(final @NotNull WorldBuilder worldBuilder){ + PegasusPlugin.logger.info("Adding game world %s".formatted(worldBuilder.getWorldName())); + PegasusWorld world = worldBuilder.make(this.plugin); + this.gameWorlds.add(world); + PegasusPlugin.logger.info("Game world %s added".formatted(worldBuilder.getWorldName())); + return world; + } + + /** + * Create a new GameManager + * @param dataManager The data manager to use + * @param optionsBuilder The game options to use + * @param scoreManager The score manager to use + * @return The new GameManager + */ + public GameManager createGameManager( + final @NotNull DataManager dataManager, + final @NotNull OptionsBuilder optionsBuilder, + final @NotNull ScoreManager scoreManager + ){ + PegasusPlugin.logger.info("Creating game manager"); + GameManager gameManager = new GameManager(this.plugin, this.lobbyWorld, dataManager, optionsBuilder, scoreManager); + this.gameManagers.add(gameManager); + PegasusPlugin.logger.info("Game manager created"); + return gameManager; + } + + /** + * Get the lobby world + * @return The lobby world + */ + public PegasusWorld getLobbyWorld() { + return this.lobbyWorld; + } + + /** + * Get the list of game worlds + * @return The list of game worlds + */ + public List getGameWorlds() { + return this.gameWorlds; + } + + /** + * Teleport the player to the lobby world when they join the server + * @param e The PlayerJoinEvent + */ + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerJoin(@NotNull final PlayerJoinEvent e){ + PegasusPlayer pPlayer = new PegasusPlayer(e.getPlayer().getName()); + for(GameManager gameManager : this.gameManagers) + if(gameManager.isPlayerInGame(pPlayer)) + return; + e.getPlayer().teleport(this.getLobbyWorld().getSpawnPoint()); + e.getPlayer().getInventory().clear(); + // Setup spectating hot bar + } +} diff --git a/src/main/java/fr/pegasus/papermc/samples/game/GameSamplePlugin.java b/src/main/java/fr/pegasus/papermc/samples/game/GameSamplePlugin.java new file mode 100644 index 0000000..d8ff6ae --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/samples/game/GameSamplePlugin.java @@ -0,0 +1,58 @@ +package fr.pegasus.papermc.samples.game; + +import fr.pegasus.papermc.PegasusPlugin; +import fr.pegasus.papermc.games.GameManager; +import fr.pegasus.papermc.games.instances.GameType; +import fr.pegasus.papermc.games.options.OptionsBuilder; +import fr.pegasus.papermc.worlds.PegasusWorld; +import fr.pegasus.papermc.worlds.WorldBuilder; +import fr.pegasus.papermc.worlds.WorldPreventions; +import fr.pegasus.papermc.worlds.locations.RelativeLocation; +import fr.pegasus.papermc.worlds.schematics.Schematic; +import fr.pegasus.papermc.worlds.schematics.SchematicFlags; +import io.papermc.paper.event.player.ChatEvent; +import org.bukkit.Difficulty; +import org.bukkit.GameMode; +import org.bukkit.GameRule; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +import java.util.List; + +public class GameSamplePlugin extends PegasusPlugin implements Listener { + + private static GameManager gameManager; + + @Override + public void onEnable() { + super.onEnable(); + + this.getServer().getPluginManager().registerEvents(this, this); + + WorldBuilder gameWorldBuilder = new WorldBuilder("pegasus_sample"); + gameWorldBuilder.setGameMode(GameMode.CREATIVE) + .setDifficulty(Difficulty.PEACEFUL) + .addGameRule(GameRule.DO_DAYLIGHT_CYCLE, false) + .addGameRule(GameRule.DO_WEATHER_CYCLE, false) + .setWorldTime(6000) + .addPrevention(WorldPreventions.ALL) + .addPrevention(WorldPreventions.PREVENT_PVP); + PegasusWorld gameWorld = this.getServerManager().addGameWorld(gameWorldBuilder); + + OptionsBuilder optionsBuilder = new OptionsBuilder() + .setGameType(GameType.SOLO) + .setWorld(gameWorld) + .setInstanceClass(SampleInstance.class) + .setRoundDurations(List.of(10)) + .setSpawnPoints(List.of(new RelativeLocation(0.5, 0, 0.5, 90, 0))) + .setSchematic(new Schematic(this, "instances_test", SchematicFlags.COPY_BIOMES)) + .setPreAllocatedInstances(1); + gameManager = this.getServerManager().createGameManager(new SampleDataManager(0, 0), optionsBuilder, new SampleScoreManager()); + } + + @SuppressWarnings("deprecation") + @EventHandler + public void onChatMessage(ChatEvent e){ + gameManager.start(false); + } +} diff --git a/src/main/java/fr/pegasus/papermc/samples/game/SampleDataManager.java b/src/main/java/fr/pegasus/papermc/samples/game/SampleDataManager.java new file mode 100644 index 0000000..fd3156b --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/samples/game/SampleDataManager.java @@ -0,0 +1,41 @@ +package fr.pegasus.papermc.samples.game; + +import com.google.common.collect.Sets; +import fr.pegasus.papermc.teams.Team; +import fr.pegasus.papermc.teams.loaders.DataManager; +import fr.pegasus.papermc.tools.PegasusRandom; +import fr.pegasus.papermc.utils.PegasusPlayer; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class SampleDataManager implements DataManager { + + private final int fakeTeamsCount; + private final int fakeTeamsPlayerCount; + + public SampleDataManager(int fakeTeamsCount, int fakeTeamsPlayerCount) { + this.fakeTeamsCount = fakeTeamsCount; + this.fakeTeamsPlayerCount = fakeTeamsPlayerCount; + } + + @Override + public Set loadTeams() { + Set teams = new HashSet<>(); + teams.add(new Team("T1", "Team 1", Sets.newHashSet(new PegasusPlayer("Xen0Xys")))); + PegasusRandom pRandom = new PegasusRandom(); + for(int i = 0; i < this.fakeTeamsCount; i++){ + Set pPlayers = new HashSet<>(); + for(int j = 0; j < this.fakeTeamsPlayerCount; j++) + pPlayers.add(new PegasusPlayer(pRandom.randomString(20))); + teams.add(new Team("T" + (i + 2), "Team " + (i + 2), pPlayers)); + } + return teams; + } + + @Override + public void uploadScores(Map scores) { + + } +} diff --git a/src/main/java/fr/pegasus/papermc/samples/game/SampleInstance.java b/src/main/java/fr/pegasus/papermc/samples/game/SampleInstance.java new file mode 100644 index 0000000..05dc043 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/samples/game/SampleInstance.java @@ -0,0 +1,70 @@ +package fr.pegasus.papermc.samples.game; + +import fr.pegasus.papermc.games.instances.Instance; +import fr.pegasus.papermc.games.instances.enums.InstanceStates; +import fr.pegasus.papermc.games.options.CommonOptions; +import fr.pegasus.papermc.games.options.InstanceOptions; +import fr.pegasus.papermc.scores.ScoreManager; +import fr.pegasus.papermc.utils.PegasusPlayer; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +public class SampleInstance extends Instance { + public SampleInstance(int id, JavaPlugin plugin, CommonOptions commonOptions, InstanceOptions instanceOptions, ScoreManager scoreManager) { + super(id, plugin, commonOptions, instanceOptions, scoreManager); + } + + @Override + public void onReady() { + + } + + @Override + public void onPreStart() { + + } + + @Override + public void onStart() { + + } + + @Override + public void onRoundPreStart() { + + } + + @Override + public void onRoundStart() { + for(PegasusPlayer pPlayer : this.getPlayerManager().getPlayers()){ + if(!pPlayer.isOnline()) + continue; + Player player = pPlayer.getPlayer(); + player.getInventory().addItem(new ItemStack(Material.BEDROCK, 1)); + } + } + + @Override + public void onRoundEnd() { + + } + + @Override + public void onEnd() { + + } + + @Override + public void onTick(int remainingTime) { + + } + + @Override + public void onPlayerReconnect(@NotNull Player player, @NotNull InstanceStates disconnectState, @NotNull InstanceStates reconnectState) { + if(disconnectState != InstanceStates.ROUND_STARTED && reconnectState == InstanceStates.ROUND_STARTED) + player.getInventory().addItem(new ItemStack(Material.BEDROCK, 1)); + } +} diff --git a/src/main/java/fr/pegasus/papermc/samples/game/SampleScoreManager.java b/src/main/java/fr/pegasus/papermc/samples/game/SampleScoreManager.java new file mode 100644 index 0000000..c0c2d97 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/samples/game/SampleScoreManager.java @@ -0,0 +1,14 @@ +package fr.pegasus.papermc.samples.game; + +import fr.pegasus.papermc.scores.ScoreManager; +import fr.pegasus.papermc.teams.Team; + +import java.util.HashMap; +import java.util.Map; + +public class SampleScoreManager implements ScoreManager { + @Override + public Map getScores() { + return new HashMap<>(); + } +} diff --git a/src/main/java/fr/pegasus/papermc/scores/ScoreManager.java b/src/main/java/fr/pegasus/papermc/scores/ScoreManager.java new file mode 100644 index 0000000..e262506 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/scores/ScoreManager.java @@ -0,0 +1,9 @@ +package fr.pegasus.papermc.scores; + +import fr.pegasus.papermc.teams.Team; + +import java.util.Map; + +public interface ScoreManager { + Map getScores(); +} diff --git a/src/main/java/fr/pegasus/papermc/teams/PlayerManager.java b/src/main/java/fr/pegasus/papermc/teams/PlayerManager.java new file mode 100644 index 0000000..6b844fb --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/teams/PlayerManager.java @@ -0,0 +1,183 @@ +package fr.pegasus.papermc.teams; + +import com.google.common.collect.Lists; +import fr.pegasus.papermc.games.instances.GameType; +import fr.pegasus.papermc.games.instances.enums.InstanceStates; +import fr.pegasus.papermc.tools.dispatcher.Dispatcher; +import fr.pegasus.papermc.tools.dispatcher.DispatcherAlgorithm; +import fr.pegasus.papermc.utils.Announcer; +import fr.pegasus.papermc.utils.PegasusPlayer; +import fr.pegasus.papermc.worlds.locations.RelativeLocation; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +public class PlayerManager implements Listener { + + private List teams; + private Map playerSpawns; + private final List frozenPlayers; + private final Map disconnectedPlayers; + + private final Announcer globalAnnouncer; + private final List teamAnnouncers; + + // TODO: Announcer management + + /** + * Create a new PlayerManager + * @param teams The teams to manage + */ + public PlayerManager(@NotNull final JavaPlugin plugin, @NotNull final List teams) { + this.teams = teams; + this.playerSpawns = new HashMap<>(); + this.frozenPlayers = new ArrayList<>(); + this.disconnectedPlayers = new HashMap<>(); + this.globalAnnouncer = new Announcer(); + this.teamAnnouncers = new ArrayList<>(); + plugin.getServer().getPluginManager().registerEvents(this, plugin); + } + + /** + * Create a new PlayerManager + * @param plugin The plugin instance + */ + public PlayerManager(@NotNull final JavaPlugin plugin) { + this.teams = new ArrayList<>(); + this.playerSpawns = new HashMap<>(); + this.frozenPlayers = new ArrayList<>(); + this.disconnectedPlayers = new HashMap<>(); + this.globalAnnouncer = new Announcer(); + this.teamAnnouncers = new ArrayList<>(); + plugin.getServer().getPluginManager().registerEvents(this, plugin); + } + + /** + * Set the teams managed by this PlayerManager + * @param teams The teams to manage + */ + public void setTeams(@NotNull final List teams){ + this.teams = teams; + this.globalAnnouncer.setPlayers(this.getPlayers()); + for(Team team : this.teams){ + Announcer announcer = new Announcer(team.players()); + this.teamAnnouncers.add(announcer); + } + } + + /** + * Affect the spawns to the players + * @param spawns The spawns to affect + * @param gameType The {@link GameType} of the game + */ + public void affectSpawns(@NotNull final List spawns, @NotNull final GameType gameType){ + Dispatcher dispatcher = new Dispatcher(DispatcherAlgorithm.ROUND_ROBIN); + switch (gameType){ + case SOLO -> { + this.playerSpawns = dispatcher.dispatch(Lists.newArrayList(this.getPlayers()), spawns); + } + default -> throw new UnsupportedOperationException("Not implemented yet"); + } + } + + /** + * Get the players managed by this PlayerManager + * @return The players + */ + public Set getPlayers(){ + Set players = new HashSet<>(); + for(Team team : this.teams) + players.addAll(team.players()); + return players; + } + + /** + * Get the teams managed by this PlayerManager + * @return The teams + */ + public List getTeams() { + return this.teams; + } + + /** + * Get the global announcer + * @return The global announcer + */ + public Announcer getGlobalAnnouncer() { + return globalAnnouncer; + } + + /** + * Get the announcers for each team + * @return The list of announcers + */ + public List getTeamAnnouncers() { + return teamAnnouncers; + } + + public Map getPlayerSpawns() { + return playerSpawns; + } + + public boolean isFrozen(@NotNull final PegasusPlayer player) { + return this.frozenPlayers.contains(player); + } + + public void setFrozen(@NotNull final PegasusPlayer player, boolean frozen) { + if(frozen) + this.frozenPlayers.add(player); + else + this.frozenPlayers.remove(player); + } + + public void setFrozenAll(boolean frozen) { + for(PegasusPlayer player : this.getPlayers()) + this.setFrozen(player, frozen); + } + + public void setGameMode(@NotNull final PegasusPlayer player, @NotNull final GameMode gameMode){ + if(!player.isOnline()) + return; + player.getPlayer().setGameMode(gameMode); + } + + public void setGameModeAll(@NotNull final GameMode gameMode){ + for(PegasusPlayer player : this.getPlayers()) + this.setGameMode(player, gameMode); + } + + public void playerDisconnect(@NotNull final PegasusPlayer player, @NotNull final InstanceStates currentState){ + this.disconnectedPlayers.put(player, currentState); + } + + public InstanceStates playerReconnect(@NotNull final PegasusPlayer player){ + InstanceStates state = this.disconnectedPlayers.get(player); + this.disconnectedPlayers.remove(player); + return state; + } + + public Map getDisconnectedPlayers() { + return disconnectedPlayers; + } + + @EventHandler + public void onPlayerMove(@NotNull final PlayerMoveEvent e){ + PegasusPlayer pPlayer = new PegasusPlayer(e.getPlayer()); + if(this.isFrozen(pPlayer) && e.getFrom().distance(e.getTo()) > 0) + e.setCancelled(true); + } + + public void teleportAllToSpawns(@NotNull final Location instanceLocation) { + for(PegasusPlayer pPlayer : this.playerSpawns.keySet()){ + if(!pPlayer.isOnline()) + continue; + pPlayer.getPlayer().teleport(this.playerSpawns.get(pPlayer).toAbsolute(instanceLocation)); + } + } +} diff --git a/src/main/java/fr/pegasus/papermc/teams/Team.java b/src/main/java/fr/pegasus/papermc/teams/Team.java new file mode 100644 index 0000000..ba7e054 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/teams/Team.java @@ -0,0 +1,10 @@ +package fr.pegasus.papermc.teams; + +import fr.pegasus.papermc.utils.PegasusPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +public record Team(@NotNull String teamTag, @NotNull String teamName, @NotNull Set players) { + +} diff --git a/src/main/java/fr/pegasus/papermc/teams/TeamManager.java b/src/main/java/fr/pegasus/papermc/teams/TeamManager.java new file mode 100644 index 0000000..cd9e84f --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/teams/TeamManager.java @@ -0,0 +1,37 @@ +package fr.pegasus.papermc.teams; + +import fr.pegasus.papermc.PegasusPlugin; +import fr.pegasus.papermc.teams.loaders.DataManager; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Set; + +public class TeamManager { + + private final DataManager dataManager; + private final Set teams; + + public TeamManager(final @NotNull DataManager dataManager) { + this.dataManager = dataManager; + this.teams = new HashSet<>(); + } + + private void loadTeams(){ + PegasusPlugin.logger.info("Loading teams from %s data manager".formatted(this.dataManager.getClass().getSimpleName())); + this.teams.addAll(this.dataManager.loadTeams()); + PegasusPlugin.logger.info("Loaded %d teams".formatted(this.teams.size())); + } + + public void reloadTeams() { + this.teams.clear(); + this.loadTeams(); + } + + public Set getTeams() { + if(!this.teams.isEmpty()) + return this.teams; + this.loadTeams(); + return teams; + } +} diff --git a/src/main/java/fr/pegasus/papermc/teams/loaders/DataManager.java b/src/main/java/fr/pegasus/papermc/teams/loaders/DataManager.java new file mode 100644 index 0000000..2cfeeb2 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/teams/loaders/DataManager.java @@ -0,0 +1,11 @@ +package fr.pegasus.papermc.teams.loaders; + +import fr.pegasus.papermc.teams.Team; + +import java.util.Map; +import java.util.Set; + +public interface DataManager { + Set loadTeams(); + void uploadScores(Map scores); +} diff --git a/src/main/java/fr/pegasus/papermc/tools/PegasusRandom.java b/src/main/java/fr/pegasus/papermc/tools/PegasusRandom.java new file mode 100644 index 0000000..70a8d30 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/tools/PegasusRandom.java @@ -0,0 +1,33 @@ +package fr.pegasus.papermc.tools; + +import java.util.Random; + +public class PegasusRandom { + private final Random random; + + public PegasusRandom(){ + this.random = new Random(); + } + + public PegasusRandom(long seed){ + this.random = new Random(seed); + } + + public int nextInt(int max){ + return this.nextInt(0, max); + } + + public int nextInt(int min, int max){ + return random.nextInt(max - min) + min; + } + + public String randomString(int length){ + String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder result = new StringBuilder(length); + for (int i = 0; i < length; i++) { + int index = random.nextInt(characters.length()); + result.append(characters.charAt(index)); + } + return result.toString(); + } +} diff --git a/src/main/java/fr/pegasus/papermc/tools/dispatcher/Dispatcher.java b/src/main/java/fr/pegasus/papermc/tools/dispatcher/Dispatcher.java new file mode 100644 index 0000000..5940f82 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/tools/dispatcher/Dispatcher.java @@ -0,0 +1,61 @@ +package fr.pegasus.papermc.tools.dispatcher; + +import fr.pegasus.papermc.tools.PegasusRandom; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Dispatcher { + private final DispatcherAlgorithm dispatchAlgorithm; + + /** + * Create a new Dispatcher + * @param dispatchAlgorithm The dispatch algorithm to use + */ + public Dispatcher(@NotNull DispatcherAlgorithm dispatchAlgorithm){ + this.dispatchAlgorithm = dispatchAlgorithm; + } + + /** + * Dispatch the values to the keys + * @param keys The keys + * @param values The values + * @return The final map + * @param The type of the keys + * @param The type of the values + */ + public Map dispatch(@NotNull List keys, @NotNull List values){ + Map finalMap = new HashMap<>(); + switch (dispatchAlgorithm){ + case ROUND_ROBIN -> { + for(int i = 0; i < keys.size(); i++) + finalMap.put(keys.get(i), values.get(i % (values.size()))); + } + case REVERSE_ROUND_ROBIN -> { + for(int i = keys.size() - 1; i >= 0; i--) + finalMap.put(keys.get(i), values.get(i % (values.size()))); + } + case RANDOM -> { + for(A key: keys) + finalMap.put(key, values.get(new PegasusRandom().nextInt(values.size()))); + } + case RANDOM_UNIQUE -> { + if(keys.size() > values.size()) + throw new RuntimeException("The number of keys must be less than or equal to the number of values (%d keys provided, %d values provided)" + .formatted(keys.size(), values.size())); + List valuesCopy = new ArrayList<>(values); + int random; + for(A key: keys){ + random = new PegasusRandom().nextInt(valuesCopy.size()); + finalMap.put(key, valuesCopy.get(random)); + valuesCopy.remove(random); + } + } + default -> throw new RuntimeException("Unknown dispatch algorithm"); + } + return finalMap; + } +} diff --git a/src/main/java/fr/pegasus/papermc/tools/dispatcher/DispatcherAlgorithm.java b/src/main/java/fr/pegasus/papermc/tools/dispatcher/DispatcherAlgorithm.java new file mode 100644 index 0000000..cf9fa92 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/tools/dispatcher/DispatcherAlgorithm.java @@ -0,0 +1,8 @@ +package fr.pegasus.papermc.tools.dispatcher; + +public enum DispatcherAlgorithm { + ROUND_ROBIN, + REVERSE_ROUND_ROBIN, + RANDOM, + RANDOM_UNIQUE +} diff --git a/src/main/java/fr/pegasus/papermc/utils/Announcer.java b/src/main/java/fr/pegasus/papermc/utils/Announcer.java new file mode 100644 index 0000000..f5df902 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/utils/Announcer.java @@ -0,0 +1,91 @@ +package fr.pegasus.papermc.utils; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.title.Title; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Set; + +public class Announcer { + + private Set players; + + /** + * Create a new Announcer + * @param players The players to announce to + */ + public Announcer(@NotNull final Set players) { + this.players = players; + } + + public Announcer(){ + this.players = new HashSet<>(); + } + + public void setPlayers(@NotNull final Set players){ + this.players = players; + } + + /** + * Announce a chat message to all players + * @param component The message to announce + */ + public void announceChat(@NotNull final TextComponent component){ + for(PegasusPlayer player : this.players){ + if(!player.isOnline()) + continue; + player.getPlayer().sendMessage(component); + } + } + + /** + * Announce a chat message to all players + * @param message The message to announce + */ + public void announceChat(@NotNull final String message){ + this.announceChat(Component.text(message)); + } + + /** + * Announce a title to all players + * @param title The title to announce + */ + public void announceTitle(@NotNull final Title title){ + for(PegasusPlayer player : this.players){ + if(!player.isOnline()) + continue; + player.getPlayer().showTitle(title); + } + } + + /** + * Announce a title to all players + * @param title The title to announce + * @param subtitle The subtitle to announce + */ + public void announceTitle(@NotNull final String title, @NotNull final String subtitle){ + this.announceTitle(Title.title(Component.text(title), Component.text(subtitle))); + } + + /** + * Announce an action bar to all players + * @param component The message to display on action bar + */ + public void announceActionBar(@NotNull final TextComponent component){ + for(PegasusPlayer player : this.players){ + if(!player.isOnline()) + continue; + player.getPlayer().sendActionBar(component); + } + } + + /** + * Announce an action bar to all players + * @param message The message to display on action bar + */ + public void announceActionBar(@NotNull final String message){ + this.announceActionBar(Component.text(message)); + } +} diff --git a/src/main/java/fr/pegasus/papermc/utils/Countdown.java b/src/main/java/fr/pegasus/papermc/utils/Countdown.java new file mode 100644 index 0000000..55fc8a5 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/utils/Countdown.java @@ -0,0 +1,44 @@ +package fr.pegasus.papermc.utils; + +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.function.Consumer; + +public class Countdown { + + private int duration; + private final Consumer announcer; + private final Runnable action; + + /** + * Create a countdown with a duration + * @param duration The duration of the countdown (in seconds) + * @param announcer The announcer function that will be called every second + * @param action The action that will be called when the countdown reaches 0 + */ + public Countdown(int duration, Consumer announcer, Runnable action){ + this.duration = duration; + this.announcer = announcer; + this.action = action; + } + + /** + * Start the countdown + * @param plugin The plugin instance + */ + public void start(JavaPlugin plugin){ + new BukkitRunnable() { + @Override + public void run() { + if(duration == 0){ + action.run(); + cancel(); + return; + } + announcer.accept(duration); + duration--; + } + }.runTaskTimer(plugin, 0, 20); + } +} diff --git a/src/main/java/fr/pegasus/papermc/utils/PegasusPlayer.java b/src/main/java/fr/pegasus/papermc/utils/PegasusPlayer.java new file mode 100644 index 0000000..fc967c0 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/utils/PegasusPlayer.java @@ -0,0 +1,67 @@ +package fr.pegasus.papermc.utils; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class PegasusPlayer { + private final String name; + + /** + * Create a new PegasusPlayer + * @param player The player + */ + public PegasusPlayer(final @NotNull Player player){ + this.name = player.getName(); + } + + /** + * Create a new PegasusPlayer + * @param name The player name + */ + public PegasusPlayer(final @NotNull String name) { + this.name = name; + } + + /** + * Check if the player is online + * @return True if the player is online, false otherwise + */ + public boolean isOnline(){ + return Objects.nonNull(Bukkit.getPlayer(this.name)); + } + + /** + * Get the player if online, throw a {@link RuntimeException} otherwise + * @return The player if online + */ + public Player getPlayer(){ + Player player = Bukkit.getPlayer(this.name); + if(Objects.isNull(player)) + throw new RuntimeException("Player %s is not online".formatted(this.name)); + return player; + } + + /** + * Check if an object is equals to this PegasusPlayer + * @param obj The object to compare + * @return True if the object is equals to this PegasusPlayer, false otherwise + */ + @Override + public boolean equals(Object obj) { + if(!(obj instanceof PegasusPlayer)) + return false; + return ((PegasusPlayer) obj).name.equals(this.name); + } + + /** + * Get the hash code of this PegasusPlayer + * @return The hash code of this PegasusPlayer + */ + @Override + public int hashCode() { + return this.name.hashCode(); + } +} diff --git a/src/main/java/fr/pegasus/papermc/worlds/PegasusWorld.java b/src/main/java/fr/pegasus/papermc/worlds/PegasusWorld.java new file mode 100644 index 0000000..7d80092 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/worlds/PegasusWorld.java @@ -0,0 +1,260 @@ +package fr.pegasus.papermc.worlds; + +import com.sk89q.worldedit.WorldEditException; +import fr.pegasus.papermc.worlds.generators.VoidGenerator; +import fr.pegasus.papermc.worlds.locations.RelativeLocation; +import fr.pegasus.papermc.worlds.schematics.Schematic; +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerPortalEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +@SuppressWarnings("unused") +public class PegasusWorld implements Listener { + + private final JavaPlugin plugin; + private final String worldName; + private final Schematic defaultSchematic; + private final GameMode gameMode; + private final RelativeLocation spawnLocation; + private final Set preventions; + + private World world; + + /** + * Create a new PegasusWorld + * @param plugin The plugin instance + * @param worldName The name of the world + * @param defaultSchematic The default schematic to paste when the world is generated if any + * @param difficulty The difficulty of the world + * @param gameMode The game mode of the world + * @param spawnLocation The spawn location of the world + * @param preventions The preventions of the world + * @param gameRules The game rules of the world + * @param worldTime The time of the world + */ + public PegasusWorld( + final @NotNull JavaPlugin plugin, + final @NotNull String worldName, + final @Nullable Schematic defaultSchematic, + final @NotNull Difficulty difficulty, + final @NotNull GameMode gameMode, + final @NotNull RelativeLocation spawnLocation, + final @NotNull Set preventions, + final @NotNull Map, Object> gameRules, + final int worldTime + ) { + this.plugin = plugin; + this.worldName = worldName; + this.defaultSchematic = defaultSchematic; + this.gameMode = gameMode; + this.spawnLocation = spawnLocation; + this.preventions = preventions; + this.world = this.getWorld(); + this.plugin.getServer().getPluginManager().registerEvents(this, plugin); + this.applyGameRules(gameRules); + this.world.setDifficulty(difficulty); + this.world.setTime(worldTime); + this.world.setThundering(false); + } + + /** + * Apply the game rules to the world + * @param gameRules The game rules to apply + * @param The type of the game rule + */ + @SuppressWarnings("unchecked") + private void applyGameRules(final @NotNull Map, Object> gameRules){ + for(Map.Entry, Object> gameRule : gameRules.entrySet()){ + this.world.setGameRule((GameRule) gameRule.getKey(), (T) gameRule.getValue()); + } + } + + /** + * Generate the world + * @return The generated world + */ + private World generateWorld(){ + VoidGenerator worldCreator = new VoidGenerator(); + this.world = worldCreator.generate(this.worldName); + if(Objects.nonNull(this.defaultSchematic)){ + try { + this.defaultSchematic.paste(this.getSpawnPoint()); + } catch (WorldEditException e) { + throw new RuntimeException(e); + } + } + return this.world; + } + + /** + * Get the world or generate it if it doesn't exist + * @return The world + */ + public World getWorld(){ + if(Objects.nonNull(this.world)) return this.world; + this.plugin.getLogger().info(String.format("Loading world %s...", this.worldName)); + this.world = Bukkit.getWorld(this.worldName); + if(this.world == null){ + this.plugin.getLogger().info(String.format("Generating world %s...", this.worldName)); + this.world = this.generateWorld(); + this.plugin.getLogger().info(String.format("World %s generated!", this.worldName)); + }else{ + this.plugin.getLogger().info(String.format("World %s loaded!", this.worldName)); + } + return this.world; + } + + /** + * Check if the world has a specific prevention + * @param prevention The prevention to check + * @return True if the world has the prevention, false otherwise + */ + private boolean checkPrevention(final @NotNull WorldPreventions prevention){ + return this.preventions.contains(prevention) ^ this.preventions.contains(WorldPreventions.ALL); + } + + /** + * Get the spawn point of the world + * @return The spawn point + */ + public Location getSpawnPoint(){ + return this.spawnLocation.toAbsolute(new Location(this.world, 0, 0, 0)); + } + + /** + * Get the name of the world + * @return The name of the world + */ + public String getWorldName() { + return worldName; + } + + /** + * Prevent entity damages if the world has the prevention + * @param e The {@link EntityDamageEvent} + */ + @EventHandler + public void onEntityDamaged(EntityDamageEvent e){ + if(e.getEntity().getWorld().equals(this.world)){ + if(this.checkPrevention(WorldPreventions.PREVENT_DAMAGES)){ + e.setCancelled(true); + } + } + } + + /** + * Prevent PvP and PvE if the world has the prevention + * @param e The {@link EntityDamageByEntityEvent} + */ + @EventHandler + public void onEntityDamageEntity(EntityDamageByEntityEvent e){ + if(e.getEntity().getWorld().equals(this.world)){ + if(e.getEntity() instanceof Player && e.getDamager() instanceof Player){ + if(this.checkPrevention(WorldPreventions.PREVENT_PVP)){ + e.setCancelled(true); + } + } + } + } + + /** + * Prevent block placement if the world has the prevention + * @param e The {@link BlockPlaceEvent} + */ + @EventHandler + public void onBlockPlaced(BlockPlaceEvent e){ + if(e.getPlayer().getWorld().equals(this.world)){ + if(this.checkPrevention(WorldPreventions.PREVENT_BUILD)){ + e.setCancelled(true); + } + } + } + + /** + * Prevent block breaking if the world has the prevention + * @param e The {@link BlockBreakEvent} + */ + @EventHandler + public void onBlockBreak(BlockBreakEvent e){ + if(e.getPlayer().getWorld().equals(this.world)){ + if(this.checkPrevention(WorldPreventions.PREVENT_BUILD)){ + e.setCancelled(true); + } + } + } + + /** + * Prevent food level change if the world has the prevention + * @param e The {@link FoodLevelChangeEvent} + */ + @EventHandler + public void onFoodLevelChange(FoodLevelChangeEvent e){ + if(e.getEntity() instanceof Player player){ + if(player.getWorld().equals(this.world)){ + if(this.checkPrevention(WorldPreventions.PREVENT_FOOD_LOSS)){ + e.setCancelled(true); + player.setFoodLevel(20); + player.setSaturation(20); + } + } + } + } + + /** + * Prevent portal use if the world has the prevention + * @param e The {@link PlayerPortalEvent} + */ + @EventHandler + public void onPlayerPortal(PlayerPortalEvent e){ + Player player = e.getPlayer(); + if (player.getWorld().equals(this.world)) { + if(this.checkPrevention(WorldPreventions.PREVENT_PORTAL_USE)){ + e.setCancelled(true); + } + } + } + + /** + * Set the game mode of the player when he joins the world + * @param e The {@link PlayerJoinEvent} + */ + @EventHandler + public void onPlayerJoin(PlayerJoinEvent e){ + Player player = e.getPlayer(); + if (player.getWorld().equals(this.world)) { + player.setGameMode(this.gameMode); + } + } + + /** + * Set the game mode of the player when he teleports to the world + * @param e The {@link PlayerTeleportEvent} + */ + @EventHandler + public void onPlayerTeleport(PlayerTeleportEvent e){ + Player player = e.getPlayer(); + if(e.getCause() != PlayerTeleportEvent.TeleportCause.SPECTATE){ + if(!e.getFrom().getWorld().equals(this.world)){ + if(e.getTo().getWorld().equals(this.world)){ + player.setGameMode(this.gameMode); + } + } + } + } +} diff --git a/src/main/java/fr/pegasus/papermc/worlds/WorldBuilder.java b/src/main/java/fr/pegasus/papermc/worlds/WorldBuilder.java new file mode 100644 index 0000000..ce001bb --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/worlds/WorldBuilder.java @@ -0,0 +1,147 @@ +package fr.pegasus.papermc.worlds; + +import fr.pegasus.papermc.worlds.locations.RelativeLocation; +import fr.pegasus.papermc.worlds.schematics.Schematic; +import org.bukkit.Difficulty; +import org.bukkit.GameMode; +import org.bukkit.GameRule; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class WorldBuilder { + + private final String worldName; + private Schematic defaultSchematic; + private Difficulty difficulty; + private GameMode gameMode; + private RelativeLocation spawnLocation; + private final Set preventions; + private final Map, Object> gameRules; + private int worldTime = 24000; + + /** + * Create a new world builder with default values + * @param worldName The name of the world + */ + public WorldBuilder(final @NotNull String worldName) { + this(worldName, null, Difficulty.NORMAL, GameMode.SURVIVAL, new RelativeLocation(0.5, 100, 0.5)); + } + + /** + * Create a new world builder with custom values + * @param worldName The name of the world + * @param defaultSchematic The default schematic of the world + * @param difficulty The difficulty of the world + * @param gameMode The game mode of the world + * @param spawnLocation The spawn location of the world + */ + private WorldBuilder( + final @NotNull String worldName, + final @Nullable Schematic defaultSchematic, + final @NotNull Difficulty difficulty, + final @NotNull GameMode gameMode, + final @NotNull RelativeLocation spawnLocation + ) { + this.worldName = worldName; + this.defaultSchematic = defaultSchematic; + this.difficulty = difficulty; + this.gameMode = gameMode; + this.spawnLocation = spawnLocation; + this.preventions = new HashSet<>(); + this.gameRules = new HashMap<>(); + } + + /** + * Set the default schematic of the world + * @param difficulty The difficulty of the world + * @return The current world builder + */ + public WorldBuilder setDifficulty(final @NotNull Difficulty difficulty) { + this.difficulty = difficulty; + return this; + } + + /** + * Set the game mode of the world + * @param gameMode The game mode of the world + * @return The current world builder + */ + public WorldBuilder setGameMode(final @NotNull GameMode gameMode) { + this.gameMode = gameMode; + return this; + } + + /** + * Add a game rule to the world + * @param gameRule The game rule to add + * @param value The value of the game rule + * @return The current world builder + */ + public WorldBuilder addGameRule(final @NotNull GameRule gameRule, final @NotNull Object value) { + this.gameRules.put(gameRule, value); + return this; + } + + /** + * Add a prevention to the world + * @param prevention The prevention to add + * @return The current world builder + */ + public WorldBuilder addPrevention(final @NotNull WorldPreventions prevention) { + this.preventions.add(prevention); + return this; + } + + /** + * Set the default schematic of the world + * @param defaultSchematic The default schematic of the world + * @return The current world builder + */ + public WorldBuilder setDefaultSchematic(final @NotNull Schematic defaultSchematic) { + this.defaultSchematic = defaultSchematic; + return this; + } + + /** + * Set the spawn location of the world + * @param spawnLocation The spawn location of the world + */ + public WorldBuilder setSpawnLocation(final @NotNull RelativeLocation spawnLocation) { + this.spawnLocation = spawnLocation; + return this; + } + + public WorldBuilder setWorldTime(int worldTime) { + this.worldTime = worldTime; + return this; + } + + /** + * Create a new {@link PegasusWorld} with the current values + * @param plugin The plugin to create the world with + * @return The created world as a {@link PegasusWorld} + */ + public PegasusWorld make(final @NotNull JavaPlugin plugin) { + return new PegasusWorld( + plugin, + this.worldName, + this.defaultSchematic, + this.difficulty, + this.gameMode, + this.spawnLocation, + this.preventions, + this.gameRules, + this.worldTime + ); + } + + public String getWorldName() { + return worldName; + } +} diff --git a/src/main/java/fr/pegasus/papermc/worlds/WorldPreventions.java b/src/main/java/fr/pegasus/papermc/worlds/WorldPreventions.java new file mode 100644 index 0000000..e34a041 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/worlds/WorldPreventions.java @@ -0,0 +1,10 @@ +package fr.pegasus.papermc.worlds; + +public enum WorldPreventions { + ALL, + PREVENT_PVP, + PREVENT_DAMAGES, + PREVENT_BUILD, + PREVENT_FOOD_LOSS, + PREVENT_PORTAL_USE +} diff --git a/src/main/java/fr/pegasus/papermc/worlds/generators/VoidGenerator.java b/src/main/java/fr/pegasus/papermc/worlds/generators/VoidGenerator.java new file mode 100644 index 0000000..6b1bbe0 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/worlds/generators/VoidGenerator.java @@ -0,0 +1,49 @@ +package fr.pegasus.papermc.worlds.generators; + +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.generator.ChunkGenerator; + +/** + * A void generator for creating empty worlds + */ +public class VoidGenerator extends ChunkGenerator { + + public World generate(String name){ + WorldCreator wc = new WorldCreator(name); + wc.generator(this); + wc.createWorld(); + return Bukkit.getWorld(name); + } + + @Override + public boolean shouldGenerateNoise() { + return false; + } + + @Override + public boolean shouldGenerateSurface() { + return false; + } + + @Override + public boolean shouldGenerateCaves() { + return false; + } + + @Override + public boolean shouldGenerateDecorations() { + return false; + } + + @Override + public boolean shouldGenerateMobs() { + return false; + } + + @Override + public boolean shouldGenerateStructures() { + return false; + } +} diff --git a/src/main/java/fr/pegasus/papermc/worlds/locations/RelativeLocation.java b/src/main/java/fr/pegasus/papermc/worlds/locations/RelativeLocation.java new file mode 100644 index 0000000..55d61ad --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/worlds/locations/RelativeLocation.java @@ -0,0 +1,90 @@ +package fr.pegasus.papermc.worlds.locations; + +import org.bukkit.Location; + +public class RelativeLocation { + + private final double x; + private final double y; + private final double z; + private final float yaw; + private final float pitch; + + /** + * Create a relative location + * @param x X value + * @param y Y value + * @param z Z value + */ + public RelativeLocation(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + this.yaw = 0; + this.pitch = 0; + } + + /** + * Create a relative location + * @param x X value + * @param y Y value + * @param z Z value + * @param yaw Yaw value + * @param pitch Pitch value + */ + public RelativeLocation(double x, double y, double z, float yaw, float pitch) { + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + } + + /** + * Get the relative location between two locations + * @param loc1 First location + * @param loc2 Second location + * @return A new relative location + */ + public static RelativeLocation getRelativeLocation(Location loc1, Location loc2){ + return new RelativeLocation( + loc1.getX() - loc2.getX(), + loc1.getY() - loc2.getY(), + loc1.getZ() - loc2.getZ(), + loc1.getYaw() - loc2.getYaw(), + loc1.getPitch() - loc2.getPitch() + ); + } + + /** + * Convert the relative location to an absolute location + * @param loc The base location + * @return A new absolute location + */ + public Location toAbsolute(Location loc){ + return new Location( + loc.getWorld(), + loc.getX() + x, + loc.getY() + y, + loc.getZ() + z, + loc.getYaw() + yaw, + loc.getPitch() + pitch + ); + } + + public double getX() { + return x; + } + public double getY() { + return y; + } + public double getZ() { + return z; + } + public float getPitch() { + return pitch; + } + public float getYaw() { + return yaw; + } +} diff --git a/src/main/java/fr/pegasus/papermc/worlds/schematics/Schematic.java b/src/main/java/fr/pegasus/papermc/worlds/schematics/Schematic.java new file mode 100644 index 0000000..171ac22 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/worlds/schematics/Schematic.java @@ -0,0 +1,121 @@ +package fr.pegasus.papermc.worlds.schematics; + +import com.google.common.collect.Sets; +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader; +import com.sk89q.worldedit.function.operation.Operation; +import com.sk89q.worldedit.function.operation.Operations; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.session.ClipboardHolder; +import com.sk89q.worldedit.session.PasteBuilder; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class Schematic { + + private final Clipboard clipboard; + private final File schematicFile; + private final Set flags; + + /** + * Create a new Schematic instance + * @param plugin The plugin instance + * @param name The name of the schematic file + * @param flags The flags to apply to the schematic + */ + public Schematic(final @NotNull Plugin plugin, final @NotNull String name, final SchematicFlags... flags) { + File pluginFile = new File("schematics/" + name + ".schem"); + this.schematicFile = new File(plugin.getDataFolder() + "/" + pluginFile.getPath()); + this.flags = Sets.newHashSet(flags); + if(!schematicFile.exists()) { + plugin.saveResource(pluginFile.getPath(), false); + } + try { + this.clipboard = this.load(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Create a new Schematic instance + * @param schematicFile The schematic file + * @param flags The flags to apply to the schematic + */ + public Schematic(final @NotNull File schematicFile, final SchematicFlags... flags) { + this.schematicFile = schematicFile; + this.flags = Sets.newHashSet(flags); + try { + this.clipboard = this.load(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Load the schematic from the file + * @return The clipboard + * @throws IOException If an error occurs while reading the file + */ + private Clipboard load() throws IOException { + Clipboard localClipboard; + ClipboardFormat format = ClipboardFormats.findByFile(this.schematicFile); + + assert format != null; + try (ClipboardReader reader = format.getReader(new FileInputStream(this.schematicFile))) { + localClipboard = reader.read(); + } + + return localClipboard; + } + + /** + * Paste the schematic at the given location + * @param location The location to paste the schematic + * @throws WorldEditException If an error occurs while pasting the schematic + */ + public void paste(final @NotNull Location location) throws WorldEditException { + try (EditSession editSession = WorldEdit.getInstance().newEditSession(BukkitAdapter.adapt(location.getWorld()))) { + ClipboardHolder clipboardHolder = new ClipboardHolder(this.clipboard); + PasteBuilder pasteBuilder = clipboardHolder.createPaste(editSession) + .to(BlockVector3.at(location.getX(), location.getY(), location.getZ())) + .ignoreAirBlocks(this.flags.contains(SchematicFlags.IGNORE_AIR)) + .copyEntities(this.flags.contains(SchematicFlags.COPY_ENTITIES)) + .copyBiomes(this.flags.contains(SchematicFlags.COPY_BIOMES)); + Operation operation = pasteBuilder.build(); + Operations.complete(operation); + } + } + + /** + * Get the block count of the schematic + * @return The block count + */ + public Map getBlockCount(){ + HashMap blockCount = new HashMap<>(); + for(BlockVector3 blockVector3: this.clipboard.getRegion()){ + Material material = BukkitAdapter.adapt(this.clipboard.getBlock(blockVector3).getBlockType()); + if(blockCount.containsKey(material)){ + blockCount.put(material, blockCount.get(material) + 1); + }else{ + blockCount.put(material, 1); + } + } + return blockCount; + } +} diff --git a/src/main/java/fr/pegasus/papermc/worlds/schematics/SchematicFlags.java b/src/main/java/fr/pegasus/papermc/worlds/schematics/SchematicFlags.java new file mode 100644 index 0000000..6aca7f6 --- /dev/null +++ b/src/main/java/fr/pegasus/papermc/worlds/schematics/SchematicFlags.java @@ -0,0 +1,7 @@ +package fr.pegasus.papermc.worlds.schematics; + +public enum SchematicFlags { + IGNORE_AIR, + COPY_ENTITIES, + COPY_BIOMES, +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index c0d17b1..f0e5d99 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,8 +1,9 @@ -main: "fr.pegasus.papermc.samples.PluginStarter" +main: "fr.pegasus.papermc.samples.game.GameSamplePlugin" name: "PegasusFramework" version: "0.1" api-version: "1.20" authors: ["Loïc MAES", "Xen0Xys"] -description: "A brand new plugin builder framework for PaperMC 1.20.4" \ No newline at end of file +description: "A brand new plugin builder framework for PaperMC 1.20.4" +depend: ["WorldEdit"] diff --git a/src/main/resources/schematics/instances_test.schem b/src/main/resources/schematics/instances_test.schem new file mode 100644 index 0000000..ed8186b Binary files /dev/null and b/src/main/resources/schematics/instances_test.schem differ diff --git a/src/main/resources/schematics/lobby.schem b/src/main/resources/schematics/lobby.schem new file mode 100644 index 0000000..680256f Binary files /dev/null and b/src/main/resources/schematics/lobby.schem differ diff --git a/src/test/java/game_manager/GameManagerTests.java b/src/test/java/game_manager/GameManagerTests.java new file mode 100644 index 0000000..e41cef3 --- /dev/null +++ b/src/test/java/game_manager/GameManagerTests.java @@ -0,0 +1,180 @@ +package game_manager; + +import com.google.common.collect.Sets; +import fr.pegasus.papermc.games.GameManager; +import fr.pegasus.papermc.games.instances.GameType; +import fr.pegasus.papermc.teams.Team; +import fr.pegasus.papermc.utils.PegasusPlayer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +public class GameManagerTests { + + private GameManager gameManager; + private Method isEmptyTeamMethod; + private Method checkMinimumTeamsMethod; + + @BeforeEach + public void setUp() throws NoSuchMethodException { + gameManager = new GameManager(); + isEmptyTeamMethod = GameManager.class.getDeclaredMethod( + "isEmptyTeam", Set.class); + isEmptyTeamMethod.setAccessible(true); + checkMinimumTeamsMethod = GameManager.class.getDeclaredMethod( + "checkMinimumTeams", Set.class, GameType.class); + checkMinimumTeamsMethod.setAccessible(true); + } + + @Nested + class IsEmptyTests{ + + private Set teams; + + @BeforeEach + public void setUp() { + teams = new HashSet<>(); + teams.add(new Team("T1", "Team 1", Sets.newHashSet(new PegasusPlayer("Player 1"), new PegasusPlayer("Player 2")))); + teams.add(new Team("T2", "Team 2", Sets.newHashSet(new PegasusPlayer("Player 3"), new PegasusPlayer("Player 4")))); + } + + @Test + public void testWithoutEmptyTeams() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) isEmptyTeamMethod.invoke(gameManager, teams); + assertFalse(value); + } + + @Test + public void testWithEmptyTeam() throws InvocationTargetException, IllegalAccessException { + teams.add(new Team("T3", "Team 3", new HashSet<>())); + boolean value = (boolean) isEmptyTeamMethod.invoke(gameManager, teams); + assertTrue(value); + } + + @Test + public void testWithOnePlayerTeam() throws InvocationTargetException, IllegalAccessException { + teams.add(new Team("T3", "Team 3", Sets.newHashSet(new PegasusPlayer("Player 5")))); + boolean value = (boolean) isEmptyTeamMethod.invoke(gameManager, teams); + assertFalse(value); + } + } + + @Nested + public class CheckMinimumTeamsTests{ + + @Nested + public class Empty{ + + private Set teams; + + @BeforeEach + public void setUp() { + teams = new HashSet<>(); + } + + @Test + public void testWithSolo() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.SOLO); + assertFalse(value); + } + + @Test + public void testWithTeamOnly() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.TEAM_ONLY); + assertFalse(value); + } + + @Test + public void testWithTeamVsTeam() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.TEAM_VS_TEAM); + assertFalse(value); + } + + @Test + public void testWithFFA() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.FFA); + assertFalse(value); + } + } + + @Nested + public class One{ + + private Set teams; + + @BeforeEach + public void setUp() { + teams = new HashSet<>(); + teams.add(new Team("T1", "Team 1", Sets.newHashSet(new PegasusPlayer("Player 1"), new PegasusPlayer("Player 2")))); + } + + @Test + public void testWithSolo() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.SOLO); + assertTrue(value); + } + + @Test + public void testWithTeamOnly() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.TEAM_ONLY); + assertTrue(value); + } + + @Test + public void testWithTeamVsTeam() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.TEAM_VS_TEAM); + assertFalse(value); + } + + @Test + public void testWithFFA() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.FFA); + assertTrue(value); + } + } + + @Nested + public class Two{ + + private Set teams; + + @BeforeEach + public void setUp() { + teams = new HashSet<>(); + teams.add(new Team("T1", "Team 1", Sets.newHashSet(new PegasusPlayer("Player 1"), new PegasusPlayer("Player 2")))); + teams.add(new Team("T2", "Team 2", Sets.newHashSet(new PegasusPlayer("Player 3"), new PegasusPlayer("Player 4")))); + } + + @Test + public void testWithSolo() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.SOLO); + assertTrue(value); + } + + @Test + public void testWithTeamOnly() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.TEAM_ONLY); + assertTrue(value); + } + + @Test + public void testWithTeamVsTeam() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.TEAM_VS_TEAM); + assertTrue(value); + } + + @Test + public void testWithFFA() throws InvocationTargetException, IllegalAccessException { + boolean value = (boolean) checkMinimumTeamsMethod.invoke(gameManager, teams, GameType.FFA); + assertTrue(value); + } + } + } +}