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 extends Instance> instanceClass;
+ private int preAllocatedInstances = 0;
+
+ public Class extends Instance> getInstanceClass() {
+ return instanceClass;
+ }
+
+ public void setInstanceClass(Class extends Instance> 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 extends Instance> 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);
+ }
+ }
+ }
+}