From 6ffc7f3b620d8d0312375089a014d79dcd5bfda4 Mon Sep 17 00:00:00 2001 From: Pugzy Date: Sun, 23 May 2021 19:52:32 +0100 Subject: [PATCH] Implement websocket matches Co-authored-by: Pablete1234 Signed-off-by: Pablete1234 --- .gitignore | 3 + pom.xml | 11 +- src/main/java/rip/bolt/ingame/Ingame.java | 60 +++- .../rip/bolt/ingame/api/APIException.java | 14 + .../java/rip/bolt/ingame/api/APIManager.java | 31 +- .../java/rip/bolt/ingame/api/DateModule.java | 3 +- .../ingame/api/DefaultCallAdapterFactory.java | 5 +- .../ingame/api/definitions/BoltMatch.java | 33 +- .../ingame/api/definitions/BoltPGMMap.java | 11 +- .../definitions}/MatchStatus.java | 14 +- .../ingame/api/definitions/Participation.java | 4 + .../bolt/ingame/api/definitions/Series.java | 26 +- .../bolt/ingame/api/definitions/Stats.java | 13 +- .../rip/bolt/ingame/api/definitions/Team.java | 15 +- .../rip/bolt/ingame/api/definitions/User.java | 21 +- .../api/definitions/pug/PugCommand.java | 190 +++++++++++ .../ingame/api/definitions/pug/PugLobby.java | 138 ++++++++ .../ingame/api/definitions/pug/PugMatch.java | 75 +++++ .../api/definitions/pug/PugMessage.java | 43 +++ .../api/definitions/pug/PugPermissions.java | 22 ++ .../ingame/api/definitions/pug/PugPlayer.java | 54 +++ .../api/definitions/pug/PugResponse.java | 32 ++ .../ingame/api/definitions/pug/PugState.java | 7 + .../ingame/api/definitions/pug/PugTeam.java | 61 ++++ .../api/definitions/pug/PugVisibility.java | 7 + ...dAdminCommands.java => AdminCommands.java} | 91 +++++- .../bolt/ingame/commands/ForfeitCommands.java | 22 +- .../rip/bolt/ingame/commands/PugCommands.java | 187 +++++++++++ .../bolt/ingame/commands/RequeueCommands.java | 21 +- .../java/rip/bolt/ingame/config/AppData.java | 10 + .../ingame/events/BoltMatchResponseEvent.java | 53 +++ .../events/BoltMatchStatusChangeEvent.java | 2 +- .../rip/bolt/ingame/managers/GameManager.java | 70 ++++ .../KnockbackManager.java | 3 +- .../bolt/ingame/managers/MatchManager.java | 309 ++++++++++++++++++ .../{ranked => managers}/RankManager.java | 42 +-- .../bolt/ingame/managers/StatsManager.java | 65 ++++ .../{ranked => managers}/TabManager.java | 2 +- .../rip/bolt/ingame/pugs/BoltWebSocket.java | 147 +++++++++ .../rip/bolt/ingame/pugs/ManagedTeam.java | 63 ++++ .../rip/bolt/ingame/pugs/PugListener.java | 155 +++++++++ .../java/rip/bolt/ingame/pugs/PugManager.java | 283 ++++++++++++++++ .../rip/bolt/ingame/pugs/PugTeamManager.java | 171 ++++++++++ .../bolt/ingame/ranked/ForfeitManager.java | 164 ---------- .../rip/bolt/ingame/ranked/RankedManager.java | 291 +++-------------- .../bolt/ingame/ranked/RequeueManager.java | 2 +- .../bolt/ingame/ranked/SpectatorManager.java | 6 +- .../rip/bolt/ingame/ranked/StatsManager.java | 46 --- .../ranked/{ => forfeit}/CancelManager.java | 4 +- .../ingame/ranked/forfeit/ForfeitManager.java | 66 ++++ .../ingame/ranked/forfeit/ForfeitPoll.java | 61 ++++ .../ingame/ranked/forfeit/LeaveAnnouncer.java | 56 ++++ .../ranked/{ => forfeit}/PlayerWatcher.java | 19 +- .../{ranked => setup}/MatchPreloader.java | 2 +- .../ingame/{ranked => setup}/MatchSearch.java | 12 +- .../bolt/ingame/utils/AudienceProvider.java | 21 ++ .../rip/bolt/ingame/utils/CancelReason.java | 5 +- .../rip/bolt/ingame/utils/CommandsUtil.java | 14 + .../rip/bolt/ingame/utils/MapInfoParser.java | 72 ++++ .../rip/bolt/ingame/utils/PartyProvider.java | 47 +++ .../bolt/ingame/utils/RankedTeamTabEntry.java | 15 +- .../rip/bolt/ingame/utils/TeamsProvider.java | 34 ++ src/main/resources/config.yml | 6 + 63 files changed, 2951 insertions(+), 581 deletions(-) create mode 100644 src/main/java/rip/bolt/ingame/api/APIException.java rename src/main/java/rip/bolt/ingame/{ranked => api/definitions}/MatchStatus.java (71%) create mode 100644 src/main/java/rip/bolt/ingame/api/definitions/pug/PugCommand.java create mode 100644 src/main/java/rip/bolt/ingame/api/definitions/pug/PugLobby.java create mode 100644 src/main/java/rip/bolt/ingame/api/definitions/pug/PugMatch.java create mode 100644 src/main/java/rip/bolt/ingame/api/definitions/pug/PugMessage.java create mode 100644 src/main/java/rip/bolt/ingame/api/definitions/pug/PugPermissions.java create mode 100644 src/main/java/rip/bolt/ingame/api/definitions/pug/PugPlayer.java create mode 100644 src/main/java/rip/bolt/ingame/api/definitions/pug/PugResponse.java create mode 100644 src/main/java/rip/bolt/ingame/api/definitions/pug/PugState.java create mode 100644 src/main/java/rip/bolt/ingame/api/definitions/pug/PugTeam.java create mode 100644 src/main/java/rip/bolt/ingame/api/definitions/pug/PugVisibility.java rename src/main/java/rip/bolt/ingame/commands/{RankedAdminCommands.java => AdminCommands.java} (57%) create mode 100644 src/main/java/rip/bolt/ingame/commands/PugCommands.java create mode 100644 src/main/java/rip/bolt/ingame/events/BoltMatchResponseEvent.java create mode 100644 src/main/java/rip/bolt/ingame/managers/GameManager.java rename src/main/java/rip/bolt/ingame/{ranked => managers}/KnockbackManager.java (93%) create mode 100644 src/main/java/rip/bolt/ingame/managers/MatchManager.java rename src/main/java/rip/bolt/ingame/{ranked => managers}/RankManager.java (84%) create mode 100644 src/main/java/rip/bolt/ingame/managers/StatsManager.java rename src/main/java/rip/bolt/ingame/{ranked => managers}/TabManager.java (96%) create mode 100644 src/main/java/rip/bolt/ingame/pugs/BoltWebSocket.java create mode 100644 src/main/java/rip/bolt/ingame/pugs/ManagedTeam.java create mode 100644 src/main/java/rip/bolt/ingame/pugs/PugListener.java create mode 100644 src/main/java/rip/bolt/ingame/pugs/PugManager.java create mode 100644 src/main/java/rip/bolt/ingame/pugs/PugTeamManager.java delete mode 100644 src/main/java/rip/bolt/ingame/ranked/ForfeitManager.java delete mode 100644 src/main/java/rip/bolt/ingame/ranked/StatsManager.java rename src/main/java/rip/bolt/ingame/ranked/{ => forfeit}/CancelManager.java (97%) create mode 100644 src/main/java/rip/bolt/ingame/ranked/forfeit/ForfeitManager.java create mode 100644 src/main/java/rip/bolt/ingame/ranked/forfeit/ForfeitPoll.java create mode 100644 src/main/java/rip/bolt/ingame/ranked/forfeit/LeaveAnnouncer.java rename src/main/java/rip/bolt/ingame/ranked/{ => forfeit}/PlayerWatcher.java (93%) rename src/main/java/rip/bolt/ingame/{ranked => setup}/MatchPreloader.java (98%) rename src/main/java/rip/bolt/ingame/{ranked => setup}/MatchSearch.java (81%) create mode 100644 src/main/java/rip/bolt/ingame/utils/AudienceProvider.java create mode 100644 src/main/java/rip/bolt/ingame/utils/CommandsUtil.java create mode 100644 src/main/java/rip/bolt/ingame/utils/MapInfoParser.java create mode 100644 src/main/java/rip/bolt/ingame/utils/PartyProvider.java create mode 100644 src/main/java/rip/bolt/ingame/utils/TeamsProvider.java diff --git a/.gitignore b/.gitignore index 3db7d8b..64284a4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ local.properties .classpath .project +# VSCode +.vscode + # External tool builders .externalToolBuilders/ diff --git a/pom.xml b/pom.xml index bf41fd0..0acc4cf 100644 --- a/pom.xml +++ b/pom.xml @@ -47,15 +47,15 @@ provided - com.github.applenick + com.github.Pablete1234 PGM community-SNAPSHOT provided - com.github.PGMDev + com.github.bolt-rip Events - 070c088acd + team-refactor-SNAPSHOT provided @@ -78,6 +78,11 @@ taskchain-bukkit 3.7.2 + + org.java-websocket + Java-WebSocket + 1.5.1 + diff --git a/src/main/java/rip/bolt/ingame/Ingame.java b/src/main/java/rip/bolt/ingame/Ingame.java index 141270e..1e40203 100644 --- a/src/main/java/rip/bolt/ingame/Ingame.java +++ b/src/main/java/rip/bolt/ingame/Ingame.java @@ -3,16 +3,24 @@ import co.aikar.taskchain.BukkitTaskChainFactory; import co.aikar.taskchain.TaskChain; import co.aikar.taskchain.TaskChainFactory; -import dev.pgm.events.Tournament; +import dev.pgm.events.EventsPlugin; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; import rip.bolt.ingame.api.APIManager; +import rip.bolt.ingame.commands.AdminCommands; import rip.bolt.ingame.commands.ForfeitCommands; -import rip.bolt.ingame.commands.RankedAdminCommands; +import rip.bolt.ingame.commands.PugCommands; import rip.bolt.ingame.commands.RequeueCommands; -import rip.bolt.ingame.ranked.RankedManager; +import rip.bolt.ingame.managers.MatchManager; +import rip.bolt.ingame.utils.AudienceProvider; +import rip.bolt.ingame.utils.MapInfoParser; +import rip.bolt.ingame.utils.PartyProvider; +import rip.bolt.ingame.utils.TeamsProvider; import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.map.MapInfo; +import tc.oc.pgm.api.map.MapOrder; import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.party.Party; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.command.graph.CommandExecutor; import tc.oc.pgm.command.graph.MatchPlayerProvider; @@ -20,13 +28,17 @@ import tc.oc.pgm.lib.app.ashcon.intake.bukkit.graph.BasicBukkitCommandGraph; import tc.oc.pgm.lib.app.ashcon.intake.fluent.DispatcherNode; import tc.oc.pgm.lib.app.ashcon.intake.parametric.AbstractModule; +import tc.oc.pgm.teams.TeamMatchModule; +import tc.oc.pgm.util.Audience; public class Ingame extends JavaPlugin { private static TaskChainFactory taskChainFactory; - private RankedManager rankedManager; + private MatchManager matchManager; private APIManager apiManager; + private PugCommands pugCommands; + private static Ingame plugin; @Override @@ -38,22 +50,25 @@ public void onEnable() { apiManager = new APIManager(); - rankedManager = new RankedManager(this); + matchManager = new MatchManager(this); - Bukkit.getPluginManager().registerEvents(rankedManager, this); - Bukkit.getPluginManager().registerEvents(rankedManager.getPlayerWatcher(), this); - Bukkit.getPluginManager().registerEvents(rankedManager.getRankManager(), this); - Bukkit.getPluginManager().registerEvents(rankedManager.getRequeueManager(), this); - Bukkit.getPluginManager().registerEvents(rankedManager.getSpectatorManager(), this); - Bukkit.getPluginManager().registerEvents(rankedManager.getKnockbackManager(), this); + Bukkit.getPluginManager().registerEvents(matchManager, this); + Bukkit.getPluginManager().registerEvents(matchManager.getRankManager(), this); BasicBukkitCommandGraph g = new BasicBukkitCommandGraph(new CommandModule()); DispatcherNode node = g.getRootDispatcherNode(); - node.registerCommands(new RequeueCommands(rankedManager)); - node.registerCommands(new ForfeitCommands(rankedManager)); + node.registerCommands(new RequeueCommands(matchManager)); + node.registerCommands(new ForfeitCommands(matchManager)); + + node.registerNode("ingame").registerCommands(new AdminCommands(matchManager)); + + DispatcherNode pugNode = node.registerNode("pug"); + pugCommands = new PugCommands(matchManager); + pugNode.registerCommands(pugCommands); + pugNode.registerNode("team").registerCommands(pugCommands.getTeamCommands()); + + pugCommands.setCommandList(pugNode.getDispatcher().getAliases()); - DispatcherNode subNode = node.registerNode("ingame"); - subNode.registerCommands(new RankedAdminCommands(rankedManager)); new CommandExecutor(this, g).register(); System.out.println("[Ingame] Ingame is now enabled!"); @@ -77,8 +92,12 @@ public APIManager getApiManager() { return apiManager; } - public RankedManager getRankedManager() { - return rankedManager; + public MatchManager getMatchManager() { + return matchManager; + } + + public PugCommands getPugCommands() { + return pugCommands; } public static Ingame get() { @@ -95,12 +114,17 @@ protected void configure() { private void configureInstances() { bind(PGM.class).toInstance(PGM.get()); - bind(Tournament.class).toInstance(Tournament.get()); + bind(EventsPlugin.class).toInstance(EventsPlugin.get()); + bind(MapOrder.class).toInstance(PGM.get().getMapOrder()); } private void configureProviders() { bind(MatchPlayer.class).toProvider(new MatchPlayerProvider()); bind(Match.class).toProvider(new MatchProvider()); + bind(Party.class).toProvider(new PartyProvider()); + bind(TeamMatchModule.class).toProvider(new TeamsProvider()); + bind(Audience.class).toProvider(new AudienceProvider()); + bind(MapInfo.class).toProvider(new MapInfoParser()); } } } diff --git a/src/main/java/rip/bolt/ingame/api/APIException.java b/src/main/java/rip/bolt/ingame/api/APIException.java new file mode 100644 index 0000000..1cdb1bd --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/APIException.java @@ -0,0 +1,14 @@ +package rip.bolt.ingame.api; + +public class APIException extends RuntimeException { + private final int code; + + public APIException(String message, int code) { + super(message + " (" + code + ")"); + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/src/main/java/rip/bolt/ingame/api/APIManager.java b/src/main/java/rip/bolt/ingame/api/APIManager.java index 0b47da1..b7eedd6 100644 --- a/src/main/java/rip/bolt/ingame/api/APIManager.java +++ b/src/main/java/rip/bolt/ingame/api/APIManager.java @@ -14,10 +14,13 @@ public class APIManager { private final String serverId; + public final APIService apiService; + public final ObjectMapper objectMapper; public APIManager() { serverId = AppData.API.getServerName(); + objectMapper = new ObjectMapper().registerModule(new DateModule()); ObjectMapper objectMapper = new ObjectMapper().registerModule(new DateModule()); objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); @@ -57,19 +60,31 @@ public BoltResponse postPlayerRequeue(UUID uuid) { public BoltMatch postMatch(BoltMatch match) { int retries = 40; + + final int PRECONDITION_FAILED = 412; + for (int i = 0; i < retries; ) { try { return apiService.postMatch(match.getId(), match); + } catch (APIException ex) { + ex.printStackTrace(); + if (ex.getCode() == PRECONDITION_FAILED && i > 2) return match; } catch (Exception ex) { - i += 1; - - System.out.println( - "Failed to report match end, retrying in " + (i * 5) + "s (" + i + "/" + retries + ")"); ex.printStackTrace(); - try { - Thread.sleep(i * 5000L); - } catch (InterruptedException ignore) { - } + } + + i += 1; + System.out.println( + "[Ingame] Failed to report match end, retrying in " + + (i * 5) + + "s (" + + i + + "/" + + retries + + ")"); + try { + Thread.sleep(i * 5000L); + } catch (InterruptedException ignore) { } } return null; diff --git a/src/main/java/rip/bolt/ingame/api/DateModule.java b/src/main/java/rip/bolt/ingame/api/DateModule.java index 0d976e0..44c3e29 100644 --- a/src/main/java/rip/bolt/ingame/api/DateModule.java +++ b/src/main/java/rip/bolt/ingame/api/DateModule.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; @@ -30,7 +29,7 @@ public void serialize( private static class InstantDeserializer extends JsonDeserializer { @Override public Instant deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException, JsonProcessingException { + throws IOException { return Instant.parse(jsonParser.getValueAsString()); } } diff --git a/src/main/java/rip/bolt/ingame/api/DefaultCallAdapterFactory.java b/src/main/java/rip/bolt/ingame/api/DefaultCallAdapterFactory.java index f7abd67..a0ff346 100644 --- a/src/main/java/rip/bolt/ingame/api/DefaultCallAdapterFactory.java +++ b/src/main/java/rip/bolt/ingame/api/DefaultCallAdapterFactory.java @@ -25,7 +25,8 @@ private static String getErrorMessage(final retrofit2.Response response) { try (ResponseBody errorBody = response.errorBody()) { return Objects.isNull(errorBody) ? response.message() : errorBody.string(); } catch (IOException e) { - throw new RuntimeException("could not read error body", e); + e.printStackTrace(); + return "Failed to read error message"; } } @@ -57,7 +58,7 @@ public Object adapt(final Call call) { if (response.code() == NOT_FOUND) { return null; } - throw new RuntimeException(getErrorMessage(response)); + throw new APIException(getErrorMessage(response), response.code()); } return response.body(); } diff --git a/src/main/java/rip/bolt/ingame/api/definitions/BoltMatch.java b/src/main/java/rip/bolt/ingame/api/definitions/BoltMatch.java index 359afb3..910b2db 100644 --- a/src/main/java/rip/bolt/ingame/api/definitions/BoltMatch.java +++ b/src/main/java/rip/bolt/ingame/api/definitions/BoltMatch.java @@ -5,12 +5,14 @@ import java.util.Collection; import java.util.List; import java.util.UUID; -import rip.bolt.ingame.ranked.MatchStatus; +import java.util.stream.Collectors; +import rip.bolt.ingame.api.definitions.pug.PugMatch; @JsonIgnoreProperties(ignoreUnknown = true) public class BoltMatch { private String id; + private String lobbyId; private Series series; private BoltPGMMap map; @@ -29,6 +31,15 @@ public BoltMatch(String matchId) { this.id = matchId; } + public BoltMatch(String lobbyId, Series series, PugMatch pugMatch) { + this.id = pugMatch.getId(); + this.lobbyId = lobbyId; + this.series = series; + this.map = pugMatch.getMap(); + this.teams = pugMatch.getTeamIds().stream().map(Team::new).collect(Collectors.toList()); + this.status = pugMatch.getStatus(); + } + public String getId() { return id; } @@ -37,6 +48,14 @@ public void setId(String id) { this.id = id; } + public String getLobbyId() { + return lobbyId; + } + + public void setLobbyId(String lobbyId) { + this.lobbyId = lobbyId; + } + public Series getSeries() { return series; } @@ -93,6 +112,15 @@ public void setStatus(MatchStatus status) { this.status = status; } + public Participation getParticipation(UUID uuid) { + return teams.stream() + .map(Team::getParticipations) + .flatMap(Collection::stream) + .filter(participation -> participation.getUser().getUUID().equals(uuid)) + .findFirst() + .orElse(null); + } + public User getUser(UUID uuid) { return teams.stream() .map(Team::getParticipations) @@ -110,6 +138,9 @@ public String toString() { .append("Match ID: ") .append(getId()) .append("\n") + .append("Lobby ID: ") + .append(getLobbyId()) + .append("\n") .append("Series: ") .append(getSeries()) .append("\n") diff --git a/src/main/java/rip/bolt/ingame/api/definitions/BoltPGMMap.java b/src/main/java/rip/bolt/ingame/api/definitions/BoltPGMMap.java index 9a2b282..e199154 100644 --- a/src/main/java/rip/bolt/ingame/api/definitions/BoltPGMMap.java +++ b/src/main/java/rip/bolt/ingame/api/definitions/BoltPGMMap.java @@ -7,6 +7,7 @@ public class BoltPGMMap { private Integer id; private String name; + private String slug; public BoltPGMMap() {} @@ -34,8 +35,16 @@ public void setName(String name) { this.name = name; } + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + @Override public String toString() { - return "{" + " id='" + getId() + "'" + ", name='" + getName() + "'" + "}"; + return "BoltPGMMap{" + "id=" + id + ", name='" + name + '\'' + ", slug='" + slug + '\'' + '}'; } } diff --git a/src/main/java/rip/bolt/ingame/ranked/MatchStatus.java b/src/main/java/rip/bolt/ingame/api/definitions/MatchStatus.java similarity index 71% rename from src/main/java/rip/bolt/ingame/ranked/MatchStatus.java rename to src/main/java/rip/bolt/ingame/api/definitions/MatchStatus.java index 51b6a4b..91028ba 100644 --- a/src/main/java/rip/bolt/ingame/ranked/MatchStatus.java +++ b/src/main/java/rip/bolt/ingame/api/definitions/MatchStatus.java @@ -1,11 +1,11 @@ -package rip.bolt.ingame.ranked; +package rip.bolt.ingame.api.definitions; public enum MatchStatus { CREATED, LOADED, + CANCELLED, STARTED, - ENDED, - CANCELLED; + ENDED; public boolean canTransitionTo(MatchStatus next) { switch (this) { @@ -23,6 +23,14 @@ public boolean canTransitionTo(MatchStatus next) { } } + public boolean isPreGame() { + return this == CREATED || this == LOADED; + } + + public boolean isFinished() { + return this == ENDED || this == CANCELLED; + } + public String toString() { return this.name(); } diff --git a/src/main/java/rip/bolt/ingame/api/definitions/Participation.java b/src/main/java/rip/bolt/ingame/api/definitions/Participation.java index 28259a1..f73e14c 100644 --- a/src/main/java/rip/bolt/ingame/api/definitions/Participation.java +++ b/src/main/java/rip/bolt/ingame/api/definitions/Participation.java @@ -11,6 +11,10 @@ public class Participation { public Participation() {} + public Participation(User user) { + this.user = user; + } + public User getUser() { return user; } diff --git a/src/main/java/rip/bolt/ingame/api/definitions/Series.java b/src/main/java/rip/bolt/ingame/api/definitions/Series.java index 632d445..a48f885 100644 --- a/src/main/java/rip/bolt/ingame/api/definitions/Series.java +++ b/src/main/java/rip/bolt/ingame/api/definitions/Series.java @@ -7,6 +7,7 @@ public class Series { private Integer id; private String name; + private Service service; private Boolean hideObservers = false; private BoltKnockback knockback; @@ -29,6 +30,14 @@ public void setName(String name) { this.name = name; } + public Service getService() { + return service; + } + + public void setService(Service service) { + this.service = service; + } + public boolean getHideObservers() { return hideObservers; } @@ -47,6 +56,21 @@ public void setKnockback(BoltKnockback knockback) { @Override public String toString() { - return name + " (" + id + "): hideObservers=" + hideObservers; + return name + + " (" + + id + + "): " + + "hideObservers=" + + hideObservers + + ", " + + "service=" + + service; + } + + public enum Service { + RANKED, + PUG, + TM, + DRAFT, } } diff --git a/src/main/java/rip/bolt/ingame/api/definitions/Stats.java b/src/main/java/rip/bolt/ingame/api/definitions/Stats.java index 10900b0..1a2990b 100644 --- a/src/main/java/rip/bolt/ingame/api/definitions/Stats.java +++ b/src/main/java/rip/bolt/ingame/api/definitions/Stats.java @@ -15,6 +15,7 @@ public class Stats { private double damageReceivedBow; private int arrowsHit; private int arrowsShot; + private double score; public Stats() {} @@ -27,7 +28,8 @@ public Stats( double damageReceived, double damageReceivedBow, int arrowsHit, - int arrowsShot) { + int arrowsShot, + double score) { this.kills = kills; this.deaths = deaths; this.killstreak = killstreak; @@ -37,6 +39,7 @@ public Stats( this.damageReceivedBow = damageReceivedBow; this.arrowsHit = arrowsHit; this.arrowsShot = arrowsShot; + this.score = score; } public int getKills() { @@ -110,4 +113,12 @@ public int getArrowsShot() { public void setArrowsShot(int arrowsShot) { this.arrowsShot = arrowsShot; } + + public double getScore() { + return score; + } + + public void setScore(double score) { + this.score = score; + } } diff --git a/src/main/java/rip/bolt/ingame/api/definitions/Team.java b/src/main/java/rip/bolt/ingame/api/definitions/Team.java index cba1972..bd15942 100644 --- a/src/main/java/rip/bolt/ingame/api/definitions/Team.java +++ b/src/main/java/rip/bolt/ingame/api/definitions/Team.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import dev.pgm.events.team.TournamentPlayer; import dev.pgm.events.team.TournamentTeam; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -18,12 +19,16 @@ public class Team implements TournamentTeam { private Integer id; private String name; private String mmr; + + private Double score; + private List participations; public Team() {} public Team(int id) { this.id = id; + this.participations = new ArrayList<>(); } public Integer getId() { @@ -51,6 +56,14 @@ public void setMmr(String mmr) { this.mmr = mmr; } + public Double getScore() { + return score; + } + + public void setScore(Double score) { + this.score = score; + } + public List getParticipations() { return participations; } @@ -79,7 +92,7 @@ public void forEachPlayer(Consumer func) { public String toString() { return "Team " + getName() - + ": " + + ": \n " + getParticipations().stream() .map(Participation::getUser) .map(User::toString) diff --git a/src/main/java/rip/bolt/ingame/api/definitions/User.java b/src/main/java/rip/bolt/ingame/api/definitions/User.java index 7d38fff..fbcdde0 100644 --- a/src/main/java/rip/bolt/ingame/api/definitions/User.java +++ b/src/main/java/rip/bolt/ingame/api/definitions/User.java @@ -12,6 +12,7 @@ public class User implements TournamentPlayer { private UUID uuid; + private String username; private Ranking ranking; @JsonProperty(access = Access.WRITE_ONLY) @@ -19,6 +20,11 @@ public class User implements TournamentPlayer { public User() {} + public User(UUID uuid, String username) { + this.uuid = uuid; + this.username = username; + } + public UUID getUuid() { return uuid; } @@ -27,6 +33,14 @@ public void setUuid(UUID uuid) { this.uuid = uuid; } + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + public List getHistory() { return history; } @@ -52,7 +66,12 @@ public void setRanking(Ranking ranking) { @JsonIgnore public String getRank() { - return this.getRanking().getRank().getId(); + if (this.ranking == null) return null; + + BoltRank rank = this.ranking.getRank(); + if (rank == null) return null; + + return rank.getId(); } @Override diff --git a/src/main/java/rip/bolt/ingame/api/definitions/pug/PugCommand.java b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugCommand.java new file mode 100644 index 0000000..4ccb34a --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugCommand.java @@ -0,0 +1,190 @@ +package rip.bolt.ingame.api.definitions.pug; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; +import rip.bolt.ingame.api.definitions.BoltMatch; +import rip.bolt.ingame.api.definitions.BoltPGMMap; +import rip.bolt.ingame.config.AppData; +import tc.oc.pgm.api.map.MapInfo; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PugCommand { + + public static final int JOIN_OBS = 0; + public static final int JOIN_TEAM = 1; + public static final int USE_CHAT = 2; + + public static final int SET_MAP = 3; + public static final int SET_TEAM_NAME = 4; + public static final int MOVE_PLAYER = 5; + + public static final int START_MATCH = 6; + public static final int CYCLE_MATCH = 7; + + public static final int SET_VISIBILITY = 8; + public static final int MANAGE_TEAMS = 9; + public static final int LOAD_PREMADE_TEAM = 10; + public static final int CHANGE_NAME = 11; + public static final int SET_TEAM_SIZE = 12; + public static final int SET_PUG_STATUS = 13; + + public static final int SET_PERMISSIONS = 16; + public static final int SET_MODERATOR = 17; + + public static final int SET_PLAYER_STATUS = 20; + public static final int SET_MATCH_STATUS = 21; + + private final int cmd; + private final Object data; + + public PugCommand(int cmd, Object data) { + this.cmd = cmd; + this.data = data; + } + + public int getCmd() { + return cmd; + } + + public Object getData() { + return data; + } + + public static PugCommand joinObs(Player player) { + return PugCommand.of(JOIN_OBS, player); + } + + public static PugCommand joinTeam(Player player, PugTeam team) { + return PugCommand.of(JOIN_TEAM, player, "id", team.getId()); + } + + public static PugCommand sendMessage(Player player, String message) { + return PugCommand.of(USE_CHAT, player, "message", message); + } + + public static PugCommand setMap(Player player, BoltPGMMap map) { + return PugCommand.of(SET_MAP, player, "id", map.getId()); + } + + public static PugCommand setMap(Player player, MapInfo map) { + return PugCommand.of(SET_MAP, player, "name", map.getName()); + } + + public static PugCommand setTeamName(Player player, PugTeam team, String name) { + return new Builder(SET_TEAM_NAME, player).set("id", team.getId()).set("name", name).build(); + } + + public static PugCommand movePlayer(Player sender, Player player, @Nullable PugTeam team) { + String id = team == null ? null : team.getId(); + return new Builder(MOVE_PLAYER, sender).set("uuid", player.getUniqueId()).set("id", id).build(); + } + + public static PugCommand startMatch(Player sender, Duration time) { + if (time == null) time = Duration.ofSeconds(20); + return PugCommand.of(START_MATCH, sender, "duration", time.toMillis() / 1000); + } + + public static PugCommand cycleMatch(Player sender) { + return PugCommand.of(CYCLE_MATCH, sender); + } + + public static PugCommand cycleMatch(Player player, BoltPGMMap map) { + return PugCommand.of(CYCLE_MATCH, player, "id", map.getId()); + } + + public static PugCommand cycleMatch(Player sender, MapInfo map) { + return PugCommand.of(CYCLE_MATCH, sender, "name", map.getName()); + } + + private static PugCommand setVisibility() { + // Intentionally private. Not usable in-game. + return new PugCommand(SET_VISIBILITY, null); + } + + public static PugCommand shuffle(Player sender) { + return PugCommand.of(MANAGE_TEAMS, sender, "action", "shuffle"); + } + + public static PugCommand balance(Player sender) { + return PugCommand.of(MANAGE_TEAMS, sender, "action", "balance"); + } + + public static PugCommand clear(Player sender) { + return PugCommand.of(MANAGE_TEAMS, sender, "action", "clear"); + } + + public static PugCommand setPugName(Player sender, String name) { + return PugCommand.of(CHANGE_NAME, sender, "name", name); + } + + public static PugCommand setTeamSize(Player sender, int format) { + return PugCommand.of(SET_TEAM_SIZE, sender, "format", format); + } + + private static PugCommand setPugStatus() { + // Intentionally private. Not usable in-game. + return PugCommand.of(SET_PUG_STATUS, null); + } + + private static PugCommand setPermissions() { + // Intentionally private. Not usable in-game. + return PugCommand.of(SET_PERMISSIONS, null); + } + + private static PugCommand setModerator() { + // Intentionally private. Not usable in-game. + return PugCommand.of(SET_MODERATOR, null); + } + + public static PugCommand setPlayerStatus(PugPlayer player, boolean online) { + return new Builder(SET_PLAYER_STATUS) + .set("username", player.getUsername()) + .set("uuid", player.getUuid()) + .set("game", online) + .build(); + } + + public static PugCommand setMatchStatus(BoltMatch match) { + return new Builder(SET_MATCH_STATUS) + .set("match_id", match.getId()) + .set("server", AppData.API.getServerName()) + .set("status", match.getStatus().name()) + .build(); + } + + // Helper methods + private static PugCommand of(int cmd, Player sender) { + return new Builder(cmd, sender).build(); + } + + private static PugCommand of(int cmd, Player sender, String k1, Object v1) { + return new Builder(cmd, sender).set(k1, v1).build(); + } + + private static class Builder { + private final int cmd; + private final Map data = new HashMap<>(); + + public Builder(int cmd) { + this(cmd, null); + } + + public Builder(int cmd, Player player) { + this.cmd = cmd; + if (player != null) data.put("user", new PugPlayer(player.getUniqueId(), player.getName())); + } + + public PugCommand.Builder set(String key, Object obj) { + data.put(key, obj); + return this; + } + + public PugCommand build() { + return new PugCommand(cmd, data); + } + } +} diff --git a/src/main/java/rip/bolt/ingame/api/definitions/pug/PugLobby.java b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugLobby.java new file mode 100644 index 0000000..d836f09 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugLobby.java @@ -0,0 +1,138 @@ +package rip.bolt.ingame.api.definitions.pug; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import rip.bolt.ingame.api.definitions.BoltPGMMap; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PugLobby { + + private String id; + private String name; + private BoltPGMMap selectedMap; + private PugMatch match; + private PugPlayer owner; + private PugState state; + private List mods; + private List teams; + private List observers; + + private PugVisibility visibility; + private PugPermissions permissions; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public BoltPGMMap getSelectedMap() { + return selectedMap; + } + + public void setSelectedMap(BoltPGMMap selectedMap) { + this.selectedMap = selectedMap; + } + + public PugMatch getMatch() { + return match; + } + + public void setMatch(PugMatch match) { + this.match = match; + } + + public PugPlayer getOwner() { + return owner; + } + + public void setOwner(PugPlayer owner) { + this.owner = owner; + } + + public PugState getState() { + return state; + } + + public void setState(PugState state) { + this.state = state; + } + + public List getMods() { + return mods; + } + + public void setMods(List mods) { + this.mods = mods; + } + + @JsonIgnore + public List getPlayers() { + return Stream.concat(teams.stream().flatMap(t -> t.getPlayers().stream()), observers.stream()) + .collect(Collectors.toList()); + } + + public List getTeams() { + return teams; + } + + public void setTeams(List teams) { + this.teams = teams; + } + + public List getObservers() { + return observers; + } + + public void setObservers(List observers) { + this.observers = observers; + } + + public PugVisibility getVisibility() { + return visibility; + } + + public void setVisibility(PugVisibility visibility) { + this.visibility = visibility; + } + + public PugPermissions getPermissions() { + return permissions; + } + + public void setPermissions(PugPermissions permissions) { + this.permissions = permissions; + } + + @Override + public String toString() { + return "PugLobby{" + + "id='" + + id + + '\'' + + ", selectedMap=" + + selectedMap + + ", owner=" + + owner + + ", mods=" + + mods + + ", teams=" + + teams + + ", observers=" + + observers + + '}'; + } +} diff --git a/src/main/java/rip/bolt/ingame/api/definitions/pug/PugMatch.java b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugMatch.java new file mode 100644 index 0000000..4c8f218 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugMatch.java @@ -0,0 +1,75 @@ +package rip.bolt.ingame.api.definitions.pug; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.time.Instant; +import java.util.List; +import rip.bolt.ingame.api.definitions.BoltPGMMap; +import rip.bolt.ingame.api.definitions.MatchStatus; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PugMatch { + + private String id; + private BoltPGMMap map; + private String server; + private MatchStatus status; + private Instant createdAt; + private Instant startedAt; + private List teamIds; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public BoltPGMMap getMap() { + return map; + } + + public void setMap(BoltPGMMap map) { + this.map = map; + } + + public String getServer() { + return server; + } + + public void setServer(String server) { + this.server = server; + } + + public MatchStatus getStatus() { + return status; + } + + public void setStatus(MatchStatus status) { + this.status = status; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getStartedAt() { + return startedAt; + } + + public void setStartedAt(Instant startedAt) { + this.startedAt = startedAt; + } + + public List getTeamIds() { + return teamIds; + } + + public void setTeamIds(List teamIds) { + this.teamIds = teamIds; + } +} diff --git a/src/main/java/rip/bolt/ingame/api/definitions/pug/PugMessage.java b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugMessage.java new file mode 100644 index 0000000..4c18593 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugMessage.java @@ -0,0 +1,43 @@ +package rip.bolt.ingame.api.definitions.pug; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PugMessage { + + private PugPlayer player; + private String[] message; + private Type type; + + public String[] getMessage() { + return message; + } + + public void setMessage(String[] message) { + this.message = message; + } + + public PugPlayer getPlayer() { + return player; + } + + public void setPlayer(PugPlayer player) { + this.player = player; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public enum Type { + PLAYER_WEB, + PLAYER_INGAME, + SYSTEM, + SYSTEM_WEB, + SYSTEM_KO + } +} diff --git a/src/main/java/rip/bolt/ingame/api/definitions/pug/PugPermissions.java b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugPermissions.java new file mode 100644 index 0000000..165c1e7 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugPermissions.java @@ -0,0 +1,22 @@ +package rip.bolt.ingame.api.definitions.pug; + +public class PugPermissions { + private int all; + private int mod; + + public int getAll() { + return all; + } + + public void setAll(int all) { + this.all = all; + } + + public int getMod() { + return mod; + } + + public void setMod(int mod) { + this.mod = mod; + } +} diff --git a/src/main/java/rip/bolt/ingame/api/definitions/pug/PugPlayer.java b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugPlayer.java new file mode 100644 index 0000000..eab2faf --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugPlayer.java @@ -0,0 +1,54 @@ +package rip.bolt.ingame.api.definitions.pug; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.pgm.events.team.TournamentPlayer; +import java.util.UUID; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PugPlayer implements TournamentPlayer { + + private UUID uuid; + private String username; + + public PugPlayer() {} + + public PugPlayer(UUID uuid, String username) { + this.uuid = uuid; + this.username = username; + } + + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public String toString() { + return "PugPlayer{" + "uuid=" + uuid + ", username='" + username + '\'' + '}'; + } + + @Override + @JsonIgnore + @JsonProperty("UUID") + public UUID getUUID() { + return uuid; + } + + @Override + public boolean canVeto() { + return false; + } +} diff --git a/src/main/java/rip/bolt/ingame/api/definitions/pug/PugResponse.java b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugResponse.java new file mode 100644 index 0000000..08a8bab --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugResponse.java @@ -0,0 +1,32 @@ +package rip.bolt.ingame.api.definitions.pug; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.JsonNode; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PugResponse { + + private JsonNode lobby; + private PugMessage chat; + + public JsonNode getLobby() { + return lobby; + } + + public void setLobby(JsonNode lobby) { + this.lobby = lobby; + } + + public PugMessage getChat() { + return chat; + } + + public void setChat(PugMessage chat) { + this.chat = chat; + } + + @Override + public String toString() { + return lobby.toString(); + } +} diff --git a/src/main/java/rip/bolt/ingame/api/definitions/pug/PugState.java b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugState.java new file mode 100644 index 0000000..5955455 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugState.java @@ -0,0 +1,7 @@ +package rip.bolt.ingame.api.definitions.pug; + +public enum PugState { + WAITING, + RUNNING, + FINISHED +} diff --git a/src/main/java/rip/bolt/ingame/api/definitions/pug/PugTeam.java b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugTeam.java new file mode 100644 index 0000000..e8405c2 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugTeam.java @@ -0,0 +1,61 @@ +package rip.bolt.ingame.api.definitions.pug; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PugTeam { + + private String id; + private String name; + private int maxPlayers; + private List players; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getMaxPlayers() { + return maxPlayers; + } + + public void setMaxPlayers(int maxPlayers) { + this.maxPlayers = maxPlayers; + } + + public List getPlayers() { + return players; + } + + public void setPlayers(List players) { + this.players = players; + } + + // Check team id only as Events DefaultTeamManager + // finds team looking this equals any registered tm team + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PugTeam pugTeam = (PugTeam) o; + return Objects.equals(id, pugTeam.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/src/main/java/rip/bolt/ingame/api/definitions/pug/PugVisibility.java b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugVisibility.java new file mode 100644 index 0000000..79f8704 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/api/definitions/pug/PugVisibility.java @@ -0,0 +1,7 @@ +package rip.bolt.ingame.api.definitions.pug; + +enum PugVisibility { + PUBLIC, + UNLISTED, + PRIVATE +} diff --git a/src/main/java/rip/bolt/ingame/commands/RankedAdminCommands.java b/src/main/java/rip/bolt/ingame/commands/AdminCommands.java similarity index 57% rename from src/main/java/rip/bolt/ingame/commands/RankedAdminCommands.java rename to src/main/java/rip/bolt/ingame/commands/AdminCommands.java index 98c7165..3356985 100644 --- a/src/main/java/rip/bolt/ingame/commands/RankedAdminCommands.java +++ b/src/main/java/rip/bolt/ingame/commands/AdminCommands.java @@ -1,8 +1,10 @@ package rip.bolt.ingame.commands; +import static net.kyori.adventure.text.Component.newline; import static net.kyori.adventure.text.Component.text; import javax.annotation.Nullable; +import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import net.md_5.bungee.api.ChatColor; import org.bukkit.Bukkit; @@ -10,10 +12,12 @@ import org.bukkit.entity.Player; import rip.bolt.ingame.Ingame; import rip.bolt.ingame.api.definitions.BoltMatch; +import rip.bolt.ingame.api.definitions.MatchStatus; import rip.bolt.ingame.api.definitions.Punishment; import rip.bolt.ingame.config.AppData; -import rip.bolt.ingame.ranked.MatchStatus; -import rip.bolt.ingame.ranked.RankedManager; +import rip.bolt.ingame.managers.GameManager; +import rip.bolt.ingame.managers.MatchManager; +import rip.bolt.ingame.pugs.PugManager; import rip.bolt.ingame.utils.CancelReason; import rip.bolt.ingame.utils.Messages; import tc.oc.pgm.api.match.Match; @@ -24,12 +28,12 @@ import tc.oc.pgm.lib.app.ashcon.intake.parametric.annotation.Text; import tc.oc.pgm.util.Audience; -public class RankedAdminCommands { +public class AdminCommands { - private final RankedManager ranked; + private final MatchManager matchManager; - public RankedAdminCommands(RankedManager ranked) { - this.ranked = ranked; + public AdminCommands(MatchManager ranked) { + this.matchManager = ranked; } @Command( @@ -43,7 +47,7 @@ public void poll(CommandSender sender, Match match, @Switch('r') boolean repeat) throw new CommandException( ChatColor.RED + "You may not run this command while a game is running!"); - ranked.manualPoll(repeat); + matchManager.manualPoll(repeat); Audience.get(sender) .sendMessage( @@ -55,12 +59,12 @@ public void poll(CommandSender sender, Match match, @Switch('r') boolean repeat) desc = "Clear the currently stored Bolt match", perms = "ingame.staff.clear") public void clear(CommandSender sender) throws CommandException { - BoltMatch match = ranked.getMatch(); + BoltMatch match = matchManager.getMatch(); if (match == null) throw new CommandException( ChatColor.RED + "Unable to clear as no ranked match currently stored."); - ranked.manualReset(); + matchManager.manualReset(); Audience.get(sender) .sendMessage( @@ -74,7 +78,7 @@ public void clear(CommandSender sender) throws CommandException { desc = "View info about the current Bolt match", perms = "ingame.staff.match") public void match(CommandSender sender) throws CommandException { - BoltMatch boltMatch = ranked.getMatch(); + BoltMatch boltMatch = matchManager.getMatch(); if (boltMatch == null) throw new CommandException(ChatColor.RED + "No Bolt match currently loaded."); @@ -88,15 +92,36 @@ public void match(CommandSender sender) throws CommandException { desc = "View the status of the API polling", perms = "ingame.staff.status") public void status(CommandSender sender) throws CommandException { - boolean polling = ranked.getPoll().isSyncTaskRunning(); + GameManager gameTypeManager = matchManager.getGameManager(); + String gameManager = gameTypeManager.getClass().getSimpleName(); + TextComponent managerType = + text("Game manager is ", NamedTextColor.GRAY) + .append(text(gameManager, NamedTextColor.AQUA)); + + boolean polling = matchManager.getPoll().isSyncTaskRunning(); + TextComponent apiPolling = + text("API polling is ", NamedTextColor.GRAY) + .append( + text( + polling ? "running" : "not running", + polling ? NamedTextColor.GREEN : NamedTextColor.RED)); + + boolean websocket = false; + if (gameTypeManager instanceof PugManager) { + websocket = ((PugManager) gameTypeManager).getBoltWebSocket().isOpen(); + } + + TextComponent websocketConnected = + text("Websocket is ", NamedTextColor.GRAY) + .append( + text( + websocket ? "connected" : "not connected", + websocket ? NamedTextColor.GREEN : NamedTextColor.RED)); Audience.get(sender) .sendMessage( - text("API polling is ", NamedTextColor.GRAY) - .append( - text( - polling ? "running" : "not running", - polling ? NamedTextColor.GREEN : NamedTextColor.RED))); + managerType.append( + newline().append(apiPolling.append(newline().append(websocketConnected))))); } @Command( @@ -104,7 +129,7 @@ public void status(CommandSender sender) throws CommandException { desc = "Report the current Bolt match as cancelled", perms = "ingame.staff.cancel") public void cancel(CommandSender sender, Match match) throws CommandException { - BoltMatch boltMatch = ranked.getMatch(); + BoltMatch boltMatch = matchManager.getMatch(); if (boltMatch == null) throw new CommandException(ChatColor.RED + "No Bolt match currently loaded."); @@ -112,7 +137,7 @@ public void cancel(CommandSender sender, Match match) throws CommandException { throw new CommandException(ChatColor.RED + "Unable to transition to the cancelled state."); } - ranked.cancel(match, CancelReason.MANUAL_CANCEL); + matchManager.cancel(match, CancelReason.MANUAL_CANCEL); Audience.get(sender) .sendMessage( @@ -133,4 +158,34 @@ public void ban(CommandSender sender, Player target, @Text @Nullable String reas .runTaskAsynchronously( Ingame.get(), () -> Ingame.get().getApiManager().postPlayerPunishment(punishment)); } + + @Command( + aliases = "reconnect", + desc = "Reconnect to the matches websocket", + perms = "ingame.staff.reconnect") + public void reconnect(CommandSender sender) throws CommandException { + GameManager gameManager = matchManager.getGameManager(); + if (!(gameManager instanceof PugManager)) + throw new CommandException(ChatColor.RED + "The current match type does not support that."); + + Audience.get(sender) + .sendMessage(text("Reconnecting to match websocket.. ", NamedTextColor.GRAY)); + + ((PugManager) gameManager).connect(matchManager.getMatch()); + } + + @Command( + aliases = "disconnect", + desc = "Disconnect from the matches websocket", + perms = "ingame.staff.reconnect") + public void disconnect(CommandSender sender) throws CommandException { + GameManager gameManager = matchManager.getGameManager(); + if (!(gameManager instanceof PugManager)) + throw new CommandException(ChatColor.RED + "The current match type does not support that."); + + Audience.get(sender) + .sendMessage(text("Disconnecting from match websocket.. ", NamedTextColor.GRAY)); + + ((PugManager) gameManager).disconnect(); + } } diff --git a/src/main/java/rip/bolt/ingame/commands/ForfeitCommands.java b/src/main/java/rip/bolt/ingame/commands/ForfeitCommands.java index ec27ff2..bc050f3 100644 --- a/src/main/java/rip/bolt/ingame/commands/ForfeitCommands.java +++ b/src/main/java/rip/bolt/ingame/commands/ForfeitCommands.java @@ -4,8 +4,11 @@ import net.md_5.bungee.api.ChatColor; import rip.bolt.ingame.config.AppData; -import rip.bolt.ingame.ranked.ForfeitManager; +import rip.bolt.ingame.managers.GameManager; +import rip.bolt.ingame.managers.MatchManager; import rip.bolt.ingame.ranked.RankedManager; +import rip.bolt.ingame.ranked.forfeit.ForfeitManager; +import rip.bolt.ingame.ranked.forfeit.ForfeitPoll; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.match.MatchPhase; import tc.oc.pgm.api.party.Competitor; @@ -15,12 +18,10 @@ public class ForfeitCommands { - private final RankedManager ranked; - private final ForfeitManager forfeits; + private final MatchManager matchManager; - public ForfeitCommands(RankedManager ranked) { - this.ranked = ranked; - this.forfeits = this.ranked.getPlayerWatcher().getForfeitManager(); + public ForfeitCommands(MatchManager matchManager) { + this.matchManager = matchManager; } @Command( @@ -31,6 +32,13 @@ public void forfeit(MatchPlayer sender, Match match) throws CommandException { throw new CommandException( ChatColor.RED + "The forfeit command is not enabled on this server."); + GameManager gameManager = matchManager.getGameManager(); + if (!(gameManager instanceof RankedManager)) + throw new CommandException(ChatColor.RED + "The current match type does not support that."); + + RankedManager rankedManager = (RankedManager) gameManager; + ForfeitManager forfeits = rankedManager.getPlayerWatcher().getForfeitManager(); + if (match.getPhase() != MatchPhase.RUNNING) throw new CommandException(ChatColor.RED + "You may only run this command during a match."); @@ -43,7 +51,7 @@ public void forfeit(MatchPlayer sender, Match match) throws CommandException { throw new CommandException( ChatColor.YELLOW + "It's too early to forfeit this match, you can still win!"); - ForfeitManager.ForfeitPoll poll = forfeits.getForfeitPoll(team); + ForfeitPoll poll = forfeits.getForfeitPoll(team); if (poll.getVoted().contains(sender.getId())) throw new CommandException(ChatColor.RED + "You have already voted to forfeit this match."); diff --git a/src/main/java/rip/bolt/ingame/commands/PugCommands.java b/src/main/java/rip/bolt/ingame/commands/PugCommands.java new file mode 100644 index 0000000..121ebef --- /dev/null +++ b/src/main/java/rip/bolt/ingame/commands/PugCommands.java @@ -0,0 +1,187 @@ +package rip.bolt.ingame.commands; + +import static tc.oc.pgm.util.text.TextException.exception; + +import java.time.Duration; +import java.util.Collection; +import javax.annotation.Nullable; +import org.bukkit.entity.Player; +import rip.bolt.ingame.api.definitions.pug.PugCommand; +import rip.bolt.ingame.api.definitions.pug.PugTeam; +import rip.bolt.ingame.managers.GameManager; +import rip.bolt.ingame.managers.MatchManager; +import rip.bolt.ingame.pugs.PugManager; +import tc.oc.pgm.api.Permissions; +import tc.oc.pgm.api.map.MapInfo; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.party.Competitor; +import tc.oc.pgm.api.party.Party; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.lib.app.ashcon.intake.Command; +import tc.oc.pgm.lib.app.ashcon.intake.bukkit.parametric.Type; +import tc.oc.pgm.lib.app.ashcon.intake.bukkit.parametric.annotation.Fallback; +import tc.oc.pgm.lib.app.ashcon.intake.bukkit.parametric.annotation.Sender; +import tc.oc.pgm.lib.app.ashcon.intake.parametric.annotation.Default; +import tc.oc.pgm.lib.app.ashcon.intake.parametric.annotation.Text; +import tc.oc.pgm.teams.Team; +import tc.oc.pgm.teams.TeamMatchModule; + +public class PugCommands { + + private final MatchManager matchManager; + + private Collection commandList; + + public PugCommands(MatchManager matchManager) { + this.matchManager = matchManager; + } + + public TeamCommands getTeamCommands() { + return new TeamCommands(); + } + + public Collection getCommandList() { + return commandList; + } + + public void setCommandList(Collection commandList) { + this.commandList = commandList; + } + + private PugManager needPugManager() { + GameManager gm = matchManager.getGameManager(); + if (!(gm instanceof PugManager)) + throw exception("This command is only available on pug matches"); + return (PugManager) gm; + } + + @Command( + aliases = {"leave", "obs", "spectator", "spec"}, + desc = "Leave the match", + perms = Permissions.LEAVE) + public void leave(@Sender Player sender) { + needPugManager().write(PugCommand.joinObs(sender)); + } + + @Command( + aliases = {"join", "play"}, + desc = "Join the match", + usage = "[team] - defaults to random") + public void join(@Sender MatchPlayer player, Match match, @Nullable Party team) { + PugManager pm = needPugManager(); + if (team != null && !(team instanceof Competitor)) { + leave(player.getBukkit()); // This supports /join obs + return; + } + + // If no team is specified, find emptiest + if (team == null) { + final TeamMatchModule tmm = match.getModule(TeamMatchModule.class); + if (tmm != null) team = tmm.getEmptiestJoinableTeam(player, false).getTeam(); + } + + PugTeam pugTeam = pm.findPugTeam(team); + + if (pugTeam != null) pm.write(PugCommand.joinTeam(player.getBukkit(), pugTeam)); + else throw exception("command.teamNotFound"); + } + + @Command( + aliases = {"start", "begin"}, + desc = "Start the match") + public void start(@Sender Player sender, @Default("20s") Duration duration) { + needPugManager().write(PugCommand.startMatch(sender, duration)); + } + + @Command( + aliases = {"setnext", "sn"}, + desc = "Change the next map", + usage = "[map name]") + public void setNext(@Sender Player sender, @Fallback(Type.NULL) @Text MapInfo map) { + if (map == null) throw exception("Map not found!"); + needPugManager().write(PugCommand.setMap(sender, map)); + } + + @Command(aliases = "cycle", desc = "Cycle to the next match") + public void cycle( + @Sender Player sender, @Default("5s") Duration duration, @Nullable MapInfo map) { + PugManager pm = needPugManager(); + if (map != null) pm.write(PugCommand.cycleMatch(sender, map)); + else pm.write(PugCommand.cycleMatch(sender)); + } + + @Command(aliases = "recycle", desc = "Reload (cycle to) the current map", usage = "[seconds]") + public void recycle(@Sender Player sender, @Default("5s") Duration duration) { + PugManager pm = needPugManager(); + if (matchManager.getMatch() == null || matchManager.getMatch().getMap() == null) { + throw exception("Could not find current map to recycle, use cycle instead"); + } + + pm.write(PugCommand.cycleMatch(sender, matchManager.getMatch().getMap())); + } + + public class TeamCommands { + + @Command(aliases = "force", desc = "Force a player onto a team") + public void force(@Sender Player sender, Player player, @Nullable Party team) { + PugManager pm = needPugManager(); + + PugTeam pugTeam; + if (team != null && !(team instanceof Competitor)) { + pugTeam = null; // Moving to obs + } else { + // Team could be null, for FFA, but it'd return a pugTeam regardless. + pugTeam = pm.findPugTeam(team); + if (pugTeam == null) throw exception("command.teamNotFound"); + } + pm.write(PugCommand.movePlayer(sender, player, pugTeam)); + } + + @Command( + aliases = {"balance"}, + desc = "Balance teams according to MMR") + public void balance(@Sender Player sender) { + needPugManager().write(PugCommand.balance(sender)); + } + + @Command( + aliases = {"shuffle"}, + desc = "Shuffle players among the teams") + public void shuffle(@Sender Player sender) { + needPugManager().write(PugCommand.shuffle(sender)); + } + + @Command( + aliases = {"clear"}, + desc = "Clear all teams") + public void clear(@Sender Player sender) { + needPugManager().write(PugCommand.clear(sender)); + } + + @Command( + aliases = {"alias"}, + desc = "Rename a team", + usage = " ") + public void alias(@Sender Player sender, Party team, @Text String newName) { + PugManager pm = needPugManager(); + + if (newName.length() > 32) newName = newName.substring(0, 32); + + if (!(team instanceof Team)) throw exception("command.teamNotFound"); + + PugTeam pugTeam = pm.findPugTeam(team); + if (pugTeam == null) throw exception("command.teamNotFound"); + pm.write(PugCommand.setTeamName(sender, pugTeam, newName)); + } + + @Command( + aliases = {"size"}, + desc = "Set the max players on a team", + usage = "<*> ") + public void size(@Sender Player sender, String ignore, Integer max) { + PugManager pm = needPugManager(); + int teams = pm.getLobby().getTeams().size(); + pm.write(PugCommand.setTeamSize(sender, max * teams)); + } + } +} diff --git a/src/main/java/rip/bolt/ingame/commands/RequeueCommands.java b/src/main/java/rip/bolt/ingame/commands/RequeueCommands.java index 13b84a6..c2f8ef4 100644 --- a/src/main/java/rip/bolt/ingame/commands/RequeueCommands.java +++ b/src/main/java/rip/bolt/ingame/commands/RequeueCommands.java @@ -2,10 +2,11 @@ import net.md_5.bungee.api.ChatColor; import rip.bolt.ingame.Ingame; +import rip.bolt.ingame.api.definitions.MatchStatus; import rip.bolt.ingame.config.AppData; -import rip.bolt.ingame.ranked.MatchStatus; +import rip.bolt.ingame.managers.GameManager; +import rip.bolt.ingame.managers.MatchManager; import rip.bolt.ingame.ranked.RankedManager; -import rip.bolt.ingame.ranked.RequeueManager; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.match.MatchPhase; import tc.oc.pgm.api.player.MatchPlayer; @@ -14,10 +15,10 @@ public class RequeueCommands { - private final RequeueManager requeue; + private final MatchManager matchManager; - public RequeueCommands(RankedManager ranked) { - this.requeue = ranked.getRequeueManager(); + public RequeueCommands(MatchManager matchManager) { + this.matchManager = matchManager; } @Command(aliases = "requeue", desc = "Requeue for another ranked match") @@ -27,14 +28,20 @@ public void requeue(MatchPlayer sender, Match match) throws CommandException { ChatColor.RED + "The requeue command is not enabled on this server."); } + GameManager gameManager = matchManager.getGameManager(); + if (!(gameManager instanceof RankedManager)) + throw new CommandException(ChatColor.RED + "The current match type does not support that."); + + RankedManager manager = (RankedManager) gameManager; + boolean finished = match.getPhase() == MatchPhase.FINISHED; boolean cancelled = - Ingame.get().getRankedManager().getMatch().getStatus().equals(MatchStatus.CANCELLED); + Ingame.get().getMatchManager().getMatch().getStatus().equals(MatchStatus.CANCELLED); if (!(finished || cancelled)) throw new CommandException( ChatColor.RED + "You may only run this command after a match has ended."); - requeue.requestRequeue(sender); + manager.getRequeueManager().requestRequeue(sender); } } diff --git a/src/main/java/rip/bolt/ingame/config/AppData.java b/src/main/java/rip/bolt/ingame/config/AppData.java index cefa762..b65cad0 100644 --- a/src/main/java/rip/bolt/ingame/config/AppData.java +++ b/src/main/java/rip/bolt/ingame/config/AppData.java @@ -35,6 +35,12 @@ public static String getProfile() { } } + public static class Socket { + public static String getUrl() { + return Ingame.get().getConfig().getString("socket.url"); + } + } + public static long absentSecondsLimit() { return Ingame.get().getConfig().getLong("absence-time-seconds", 120); } @@ -70,4 +76,8 @@ public static Duration matchStartDuration() { public static boolean customTabEnabled() { return Ingame.get().getConfig().getBoolean("custom-tab-enabled", true); } + + public static boolean publiclyLogPugs() { + return Ingame.get().getConfig().getBoolean("publicly-log-pugs", false); + } } diff --git a/src/main/java/rip/bolt/ingame/events/BoltMatchResponseEvent.java b/src/main/java/rip/bolt/ingame/events/BoltMatchResponseEvent.java new file mode 100644 index 0000000..943d218 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/events/BoltMatchResponseEvent.java @@ -0,0 +1,53 @@ +package rip.bolt.ingame.events; + +import javax.annotation.Nullable; +import org.bukkit.event.HandlerList; +import rip.bolt.ingame.api.definitions.BoltMatch; +import rip.bolt.ingame.api.definitions.MatchStatus; +import tc.oc.pgm.api.match.Match; + +public class BoltMatchResponseEvent extends BoltMatchEvent { + + private static final HandlerList handlers = new HandlerList(); + + private final Match pgmMatch; + private final BoltMatch responseMatch; + @Nullable private final MatchStatus oldStatus; + + public BoltMatchResponseEvent( + Match pgmMatch, + BoltMatch boltMatch, + BoltMatch responseMatch, + @Nullable MatchStatus oldStatus) { + super(boltMatch); + this.pgmMatch = pgmMatch; + this.responseMatch = responseMatch; + this.oldStatus = oldStatus; + } + + public Match getPgmMatch() { + return pgmMatch; + } + + public BoltMatch getResponseMatch() { + return responseMatch; + } + + @Nullable + public MatchStatus getOldStatus() { + return oldStatus; + } + + public boolean hasMatchFinished() { + return oldStatus != null && !oldStatus.isFinished() && responseMatch.getStatus().isFinished(); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/src/main/java/rip/bolt/ingame/events/BoltMatchStatusChangeEvent.java b/src/main/java/rip/bolt/ingame/events/BoltMatchStatusChangeEvent.java index 69f1ad1..3911a4c 100644 --- a/src/main/java/rip/bolt/ingame/events/BoltMatchStatusChangeEvent.java +++ b/src/main/java/rip/bolt/ingame/events/BoltMatchStatusChangeEvent.java @@ -3,7 +3,7 @@ import javax.annotation.Nullable; import org.bukkit.event.HandlerList; import rip.bolt.ingame.api.definitions.BoltMatch; -import rip.bolt.ingame.ranked.MatchStatus; +import rip.bolt.ingame.api.definitions.MatchStatus; public class BoltMatchStatusChangeEvent extends BoltMatchEvent { diff --git a/src/main/java/rip/bolt/ingame/managers/GameManager.java b/src/main/java/rip/bolt/ingame/managers/GameManager.java new file mode 100644 index 0000000..b570223 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/managers/GameManager.java @@ -0,0 +1,70 @@ +package rip.bolt.ingame.managers; + +import java.util.function.Function; +import org.bukkit.Bukkit; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import rip.bolt.ingame.Ingame; +import rip.bolt.ingame.api.definitions.BoltMatch; +import rip.bolt.ingame.pugs.PugManager; +import rip.bolt.ingame.ranked.RankedManager; + +public abstract class GameManager implements Listener { + + public final MatchManager matchManager; + + protected GameManager(MatchManager matchManager) { + this.matchManager = matchManager; + } + + public static GameManager of(MatchManager matchManager, BoltMatch match) { + GameManager old = matchManager.getGameManager(); + GameManager newManager = of(match).apply(matchManager); + + if (old != newManager) { + old.disable(); + newManager.enable(matchManager); + } + newManager.setup(match); + return newManager; + } + + private static Function of(BoltMatch match) { + switch (match.getSeries().getService()) { + case PUG: + case TM: + case DRAFT: + return PugManager::of; + case RANKED: + default: + return RankedManager::new; + } + } + + /** Called when the game manager is created. */ + public void enable(MatchManager manager) { + Bukkit.getPluginManager().registerEvents(this, Ingame.get()); + } + + public abstract void setup(BoltMatch match); + + /** Called when the game manager is removed. */ + public void disable() { + HandlerList.unregisterAll(this); + } + + public static class NoopManager extends GameManager { + public NoopManager(MatchManager matchManager) { + super(matchManager); + } + + @Override + public void enable(MatchManager manager) {} + + @Override + public void setup(BoltMatch match) {} + + @Override + public void disable() {} + } +} diff --git a/src/main/java/rip/bolt/ingame/ranked/KnockbackManager.java b/src/main/java/rip/bolt/ingame/managers/KnockbackManager.java similarity index 93% rename from src/main/java/rip/bolt/ingame/ranked/KnockbackManager.java rename to src/main/java/rip/bolt/ingame/managers/KnockbackManager.java index a0dae89..6714d9f 100644 --- a/src/main/java/rip/bolt/ingame/ranked/KnockbackManager.java +++ b/src/main/java/rip/bolt/ingame/managers/KnockbackManager.java @@ -1,4 +1,4 @@ -package rip.bolt.ingame.ranked; +package rip.bolt.ingame.managers; import java.util.Objects; import javax.annotation.Nullable; @@ -7,6 +7,7 @@ import org.bukkit.event.Listener; import org.github.paperspigot.PaperSpigotConfig; import rip.bolt.ingame.api.definitions.BoltKnockback; +import rip.bolt.ingame.api.definitions.MatchStatus; import rip.bolt.ingame.events.BoltMatchStatusChangeEvent; public class KnockbackManager implements Listener { diff --git a/src/main/java/rip/bolt/ingame/managers/MatchManager.java b/src/main/java/rip/bolt/ingame/managers/MatchManager.java new file mode 100644 index 0000000..09f2fa3 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/managers/MatchManager.java @@ -0,0 +1,309 @@ +package rip.bolt.ingame.managers; + +import com.google.common.collect.Iterables; +import dev.pgm.events.EventsPlugin; +import dev.pgm.events.format.RoundReferenceHolder; +import dev.pgm.events.format.TournamentFormat; +import dev.pgm.events.format.TournamentFormatImpl; +import dev.pgm.events.format.TournamentRoundOptions; +import dev.pgm.events.format.rounds.single.SingleRound; +import dev.pgm.events.format.rounds.single.SingleRoundOptions; +import dev.pgm.events.format.winner.BestOfCalculation; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.Objects; +import net.md_5.bungee.api.ChatColor; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; +import rip.bolt.ingame.Ingame; +import rip.bolt.ingame.api.definitions.BoltMatch; +import rip.bolt.ingame.api.definitions.MatchStatus; +import rip.bolt.ingame.api.definitions.Team; +import rip.bolt.ingame.config.AppData; +import rip.bolt.ingame.events.BoltMatchResponseEvent; +import rip.bolt.ingame.events.BoltMatchStatusChangeEvent; +import rip.bolt.ingame.pugs.ManagedTeam; +import rip.bolt.ingame.setup.MatchPreloader; +import rip.bolt.ingame.setup.MatchSearch; +import rip.bolt.ingame.utils.CancelReason; +import rip.bolt.ingame.utils.PGMMapUtils; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.match.MatchPhase; +import tc.oc.pgm.api.match.event.MatchFinishEvent; +import tc.oc.pgm.api.match.event.MatchLoadEvent; +import tc.oc.pgm.api.match.event.MatchStartEvent; +import tc.oc.pgm.api.party.Competitor; +import tc.oc.pgm.restart.CancelRestartEvent; +import tc.oc.pgm.restart.StartRestartCountdownEvent; +import tc.oc.pgm.result.TieVictoryCondition; + +public class MatchManager implements Listener { + + private final RankManager rankManager; + private final StatsManager statsManager; + private final TabManager tabManager; + + private final MatchSearch poll; + + private GameManager gameManager; + private TournamentFormat format; + + private BoltMatch match; + private String pgmMatchId; + private Match pgmMatch; + + private BoltMatch deferredMatch; // While restarting, this will be used to store pending match + private boolean isRestarting = false; + + private Duration cycleTime = Duration.ofSeconds(0); + private CancelReason cancelReason = null; + + public MatchManager(Plugin plugin) { + gameManager = new GameManager.NoopManager(this); + rankManager = new RankManager(this); + statsManager = new StatsManager(this); + tabManager = new TabManager(plugin); + + MatchPreloader.create(); + + poll = new MatchSearch(this::setupMatch); + poll.startIn(Duration.ofSeconds(5)); + } + + public void setupMatch(BoltMatch match) { + if (isRestarting) { + this.deferredMatch = match; + return; + } + + if (!this.isMatchValid(match)) return; + + this.match = match; + this.cancelReason = null; + poll.stop(); + + gameManager = GameManager.of(this, match); + format = + new TournamentFormatImpl( + EventsPlugin.get().getTeamManager(), + new TournamentRoundOptions( + false, + false, + false, + Duration.ofMinutes(30), + Duration.ofSeconds(30), + Duration.ofSeconds(40), + new BestOfCalculation<>(1)), + new RoundReferenceHolder()); + SingleRound ranked = + new SingleRound( + format, + new SingleRoundOptions( + "ranked", + cycleTime, + AppData.matchStartDuration(), + match.getMap().getName(), + 1, + true, + true)); + format.addRound(ranked); + this.cycleTime = Duration.ofSeconds(5); + + Ingame.get() + .getServer() + .getPluginManager() + .callEvent(new BoltMatchStatusChangeEvent(match, null, MatchStatus.CREATED)); + + Bukkit.broadcastMessage(ChatColor.YELLOW + "A new match is starting on this server!"); + EventsPlugin.get() + .getTournamentManager() + .createTournament(PGM.get().getMatchManager().getMatches().next(), format); + } + + private void updateMatch(BoltMatch newMatch, MatchStatus oldStatus) { + BoltMatch oldMatch = this.match; + if (oldMatch == null + || newMatch == null + || !Objects.equals(oldMatch.getId(), newMatch.getId()) + || !Objects.equals(newMatch.getId(), pgmMatchId)) { + return; + } + + this.match = newMatch; + + Ingame.get() + .getServer() + .getPluginManager() + .callEvent(new BoltMatchResponseEvent(pgmMatch, oldMatch, newMatch, oldStatus)); + } + + private boolean isMatchValid(BoltMatch match) { + return match != null + && match.getId() != null + && !match.getId().isEmpty() + && match.getMap() != null + && (match.getStatus().equals(MatchStatus.CREATED) + || match.getStatus().equals(MatchStatus.LOADED) + || match.getStatus().equals(MatchStatus.STARTED)) + && (this.match == null || !Objects.equals(this.match.getId(), match.getId())); + } + + public BoltMatch getMatch() { + return match; + } + + public RankManager getRankManager() { + return rankManager; + } + + public MatchSearch getPoll() { + return poll; + } + + public GameManager getGameManager() { + return gameManager; + } + + public Match getPGMMatch() { + return pgmMatch; + } + + public String getPGMMatchId() { + return pgmMatchId; + } + + public CancelReason getCancelReason() { + return cancelReason; + } + + public void manualPoll(boolean repeat) { + if (repeat) { + poll.startIn(0L); + return; + } + + poll.trigger(true); + } + + public void manualReset() { + this.match = null; + } + + public void cancel(Match match, CancelReason reason) { + this.cancelReason = reason; + this.postMatchStatus(match, MatchStatus.CANCELLED); + + // Cancel countdowns if match has not started + if (match.getPhase().equals(MatchPhase.STARTING)) { + match.getCountdown().cancelAll(); + } + + // Check if match is in progress + if (match.getPhase().equals(MatchPhase.RUNNING)) { + // Add tie victory condition if in progress + match.addVictoryCondition(new TieVictoryCondition()); + match.finish(); + } + + this.getPoll().startIn(Duration.ofSeconds(15)); + } + + @EventHandler + public void onMatchLoad(MatchLoadEvent event) { + // If match has not started set as current + if (match != null && !match.getStatus().isFinished()) { + this.pgmMatchId = this.match.getId(); + } + + this.pgmMatch = event.getMatch(); + + postMatchStatus(event.getMatch(), MatchStatus.LOADED); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onMatchStart(MatchStartEvent event) { + postMatchStatus(event.getMatch(), MatchStatus.STARTED); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onMatchFinish(MatchFinishEvent event) { + if (cancelReason == null) { + postMatchStatus(event.getMatch(), MatchStatus.ENDED); + } + + poll.startIn(Duration.ofSeconds(30)); + } + + public void postMatchStatus(Match match, MatchStatus status) { + if (this.match == null) return; + + Instant now = Instant.now(); + MatchStatus oldStatus = this.match.getStatus(); + + Ingame.newSharedChain("match") + .syncFirst(() -> transition(match, this.match, status, now)) + .abortIfNull() + .async(Ingame.get().getApiManager()::postMatch) + .syncLast(newMatch -> updateMatch(newMatch, oldStatus)) + .execute(); + } + + private BoltMatch transition( + Match match, BoltMatch boltMatch, MatchStatus newStatus, Instant transitionAt) { + MatchStatus oldStatus = boltMatch.getStatus(); + if (!oldStatus.canTransitionTo(newStatus)) return null; + + switch (newStatus) { + case LOADED: + break; + case STARTED: + boltMatch.setMap(PGMMapUtils.getBoltPGMMap(boltMatch, match)); + boltMatch.setStartedAt(transitionAt); + break; + case ENDED: + statsManager.handleMatchUpdate(boltMatch, match); + boltMatch.setEndedAt(transitionAt); + Collection winners = match.getWinners(); + if (winners.size() == 1) { + format + .teamManager() + .tournamentTeam(Iterables.getOnlyElement(winners)) + .map(t -> t instanceof ManagedTeam ? ((ManagedTeam) t).getBoltTeam() : t) + .filter(t -> t instanceof Team) + .map(t -> (Team) t) + .ifPresent(winner -> boltMatch.setWinner(new Team(winner.getId()))); + } + break; + case CANCELLED: + boltMatch.setEndedAt(transitionAt); + break; + } + + boltMatch.setStatus(newStatus); + Ingame.get() + .getServer() + .getPluginManager() + .callEvent(new BoltMatchStatusChangeEvent(boltMatch, oldStatus, newStatus)); + + return boltMatch; + } + + @EventHandler + public void onRestartStart(StartRestartCountdownEvent event) { + this.isRestarting = true; + } + + @EventHandler + public void onRestartCancel(CancelRestartEvent event) { + this.isRestarting = false; + if (deferredMatch != null) { + setupMatch(deferredMatch); + deferredMatch = null; + } + } +} diff --git a/src/main/java/rip/bolt/ingame/ranked/RankManager.java b/src/main/java/rip/bolt/ingame/managers/RankManager.java similarity index 84% rename from src/main/java/rip/bolt/ingame/ranked/RankManager.java rename to src/main/java/rip/bolt/ingame/managers/RankManager.java index 21b587f..2bb30a9 100644 --- a/src/main/java/rip/bolt/ingame/ranked/RankManager.java +++ b/src/main/java/rip/bolt/ingame/managers/RankManager.java @@ -1,15 +1,9 @@ -package rip.bolt.ingame.ranked; +package rip.bolt.ingame.managers; import static net.kyori.adventure.text.Component.empty; import static net.kyori.adventure.text.Component.text; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -26,21 +20,21 @@ import rip.bolt.ingame.Ingame; import rip.bolt.ingame.api.definitions.BoltMatch; import rip.bolt.ingame.api.definitions.MatchResult; +import rip.bolt.ingame.api.definitions.MatchStatus; import rip.bolt.ingame.api.definitions.Participation; import rip.bolt.ingame.api.definitions.Team; import rip.bolt.ingame.api.definitions.User; import rip.bolt.ingame.config.AppData; +import rip.bolt.ingame.events.BoltMatchResponseEvent; import rip.bolt.ingame.utils.Messages; import tc.oc.pgm.api.PGM; import tc.oc.pgm.api.event.NameDecorationChangeEvent; import tc.oc.pgm.api.match.Match; -import tc.oc.pgm.api.match.MatchManager; import tc.oc.pgm.api.match.event.MatchStatsEvent; import tc.oc.pgm.api.party.Competitor; import tc.oc.pgm.api.party.Party; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.events.PlayerPartyChangeEvent; -import tc.oc.pgm.stats.PlayerStats; import tc.oc.pgm.util.bukkit.OnlinePlayerMapAdapter; public class RankManager implements Listener { @@ -53,10 +47,10 @@ public class RankManager implements Listener { private static final LegacyComponentSerializer SERIALIZER = LegacyComponentSerializer.legacySection(); - private final RankedManager manager; + private final MatchManager manager; private final OnlinePlayerMapAdapter permissions; - public RankManager(RankedManager manager) { + public RankManager(MatchManager manager) { this.manager = manager; this.permissions = new OnlinePlayerMapAdapter<>(Ingame.get()); } @@ -68,21 +62,30 @@ public void updatePlayer(@Nonnull MatchPlayer mp, @Nullable Party party) { User user = match == null ? null : match.getUser(mp.getId()); if (perm != null) mp.getBukkit().removeAttachment(perm); - if (user != null && party instanceof Competitor) + if (user != null && user.getRank() != null && party instanceof Competitor) permissions.put( player, player.addAttachment(Ingame.get(), "pgm.group." + user.getRank(), true)); Bukkit.getPluginManager().callEvent(new NameDecorationChangeEvent(mp.getId())); } - public void handleMatchUpdate(@Nonnull BoltMatch oldMatch, @Nonnull BoltMatch newMatch) { - MatchManager matchManager = PGM.get().getMatchManager(); + @EventHandler(priority = EventPriority.NORMAL) + public void onBoltMatchResponse(BoltMatchResponseEvent event) { + if (event.hasMatchFinished() && event.getResponseMatch().getStatus() == MatchStatus.ENDED) { + handleMatchUpdate(event.getBoltMatch(), event.getResponseMatch(), event.getPgmMatch()); + } + } + + public void handleMatchUpdate( + @Nonnull BoltMatch oldMatch, @Nonnull BoltMatch newMatch, Match match) { + tc.oc.pgm.api.match.MatchManager matchManager = PGM.get().getMatchManager(); List updates = newMatch.getTeams().stream() .map(Team::getParticipations) .flatMap(Collection::stream) .map(Participation::getUser) + .filter(Objects::nonNull) .map( user -> new RankUpdate( @@ -92,11 +95,9 @@ public void handleMatchUpdate(@Nonnull BoltMatch oldMatch, @Nonnull BoltMatch ne .filter(RankUpdate::isValid) .collect(Collectors.toList()); - if (updates.isEmpty()) return; - - Match match = updates.get(0).player.getMatch(); - Map stats = new HashMap<>(); - Bukkit.getServer().getPluginManager().callEvent(new MatchStatsEvent(match, true, true, stats)); + // FIXME: stats passed in are null. they should not be required in the event, but this is a + // community PGM thing. + Bukkit.getServer().getPluginManager().callEvent(new MatchStatsEvent(match, true, true, null)); if (AppData.Web.getMatch() != null) { match.sendMessage(Messages.matchLink(newMatch)); @@ -129,6 +130,7 @@ public void notifyUpdate(@Nonnull User old, @Nonnull User user, @Nonnull MatchPl player.sendMessage(PLACEMENT_MATCHES); for (int i = 0; i < results.size(); ) + //noinspection UnstableApiUsage player.sendMessage(Component.join(MATCH_SEPARATOR, results.subList(i, i += 15))); } } diff --git a/src/main/java/rip/bolt/ingame/managers/StatsManager.java b/src/main/java/rip/bolt/ingame/managers/StatsManager.java new file mode 100644 index 0000000..40e954b --- /dev/null +++ b/src/main/java/rip/bolt/ingame/managers/StatsManager.java @@ -0,0 +1,65 @@ +package rip.bolt.ingame.managers; + +import dev.pgm.events.EventsPlugin; +import java.util.Collection; +import java.util.UUID; +import org.bukkit.event.Listener; +import rip.bolt.ingame.api.definitions.BoltMatch; +import rip.bolt.ingame.api.definitions.Participation; +import rip.bolt.ingame.api.definitions.Stats; +import rip.bolt.ingame.api.definitions.Team; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.score.ScoreMatchModule; +import tc.oc.pgm.stats.PlayerStats; +import tc.oc.pgm.stats.StatsMatchModule; + +public class StatsManager implements Listener { + + private final MatchManager manager; + + public StatsManager(MatchManager manager) { + this.manager = manager; + } + + public void handleMatchUpdate(BoltMatch boltMatch, Match match) { + StatsMatchModule statsModule = match.getModule(StatsMatchModule.class); + ScoreMatchModule scoreModule = match.getModule(ScoreMatchModule.class); + + if (statsModule == null) return; + + boltMatch.getTeams().stream() + .map(Team::getParticipations) + .flatMap(Collection::stream) + .forEach(participation -> populatePlayerStats(participation, statsModule, scoreModule)); + + if (scoreModule == null) return; + boltMatch + .getTeams() + .forEach( + team -> + EventsPlugin.get() + .getTeamManager() + .fromTournamentTeam(team) + .ifPresent(t -> team.setScore(scoreModule.getScore(t)))); + } + + public void populatePlayerStats( + Participation participation, StatsMatchModule statsModule, ScoreMatchModule scoreModule) { + UUID uuid = participation.getUser().getUuid(); + if (statsModule.hasNoStats(uuid)) return; + + PlayerStats stats = statsModule.getPlayerStat(uuid); + participation.setStats( + new Stats( + stats.getKills(), + stats.getDeaths(), + stats.getMaxKillstreak(), + stats.getDamageDone(), + stats.getBowDamage(), + stats.getDamageTaken(), + stats.getBowDamageTaken(), + stats.getShotsHit(), + stats.getShotsTaken(), + scoreModule != null ? scoreModule.getContributions().get(uuid) : 0)); + } +} diff --git a/src/main/java/rip/bolt/ingame/ranked/TabManager.java b/src/main/java/rip/bolt/ingame/managers/TabManager.java similarity index 96% rename from src/main/java/rip/bolt/ingame/ranked/TabManager.java rename to src/main/java/rip/bolt/ingame/managers/TabManager.java index 6a09d62..1528479 100644 --- a/src/main/java/rip/bolt/ingame/ranked/TabManager.java +++ b/src/main/java/rip/bolt/ingame/managers/TabManager.java @@ -1,4 +1,4 @@ -package rip.bolt.ingame.ranked; +package rip.bolt.ingame.managers; import org.bukkit.plugin.Plugin; import rip.bolt.ingame.config.AppData; diff --git a/src/main/java/rip/bolt/ingame/pugs/BoltWebSocket.java b/src/main/java/rip/bolt/ingame/pugs/BoltWebSocket.java new file mode 100644 index 0000000..42db046 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/pugs/BoltWebSocket.java @@ -0,0 +1,147 @@ +package rip.bolt.ingame.pugs; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.Component.translatable; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.net.URI; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.craftbukkit.libs.joptsimple.internal.Strings; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.handshake.ServerHandshake; +import rip.bolt.ingame.Ingame; +import rip.bolt.ingame.api.definitions.pug.PugMessage; +import rip.bolt.ingame.api.definitions.pug.PugResponse; +import rip.bolt.ingame.config.AppData; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.util.named.NameStyle; +import tc.oc.pgm.util.text.PlayerComponent; + +public class BoltWebSocket extends WebSocketClient { + + public static final Component CONSOLE_NAME = + translatable("misc.console", NamedTextColor.DARK_AQUA) + .decoration(TextDecoration.ITALIC, true); + + private final ObjectMapper objectMapper; + private final PugManager manager; + + public BoltWebSocket(URI serverUri, PugManager manager) { + super(serverUri); + + this.manager = manager; + this.objectMapper = manager.getObjectMapper(); + } + + @Override + public void send(String text) { + super.send(text); + } + + @Override + public void onOpen(ServerHandshake serverHandshake) { + Ingame.newSharedChain("pug-sync").sync(manager::reset).execute(); + } + + @Override + public void onMessage(String s) { + try { + PugResponse pugResponse = objectMapper.readValue(s, PugResponse.class); + + Ingame.newSharedChain("pug-sync") + .sync(() -> handleMessageSync(manager, pugResponse)) + .execute(); + + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } + + private void handleMessageSync(PugManager manager, PugResponse pugResponse) { + try { + manager.syncPugLobby(pugResponse.getLobby()); + } catch (IOException e) { + e.printStackTrace(); + } + + PugMessage chat = pugResponse.getChat(); + + if (chat == null) return; + + Match pgmMatch = Ingame.get().getMatchManager().getPGMMatch(); + + MatchPlayer sender = + chat.getPlayer() != null ? pgmMatch.getPlayer(chat.getPlayer().getUuid()) : null; + Component senderName = + sender != null + ? sender.getName(NameStyle.VERBOSE) + : chat.getPlayer() != null && chat.getPlayer().getUsername() != null + ? PlayerComponent.player( + (UUID) null, chat.getPlayer().getUsername(), NameStyle.VERBOSE) + : CONSOLE_NAME; + + Component body = text(Strings.join(chat.getMessage(), ", ")); + + switch (chat.getType()) { + case PLAYER_INGAME: + return; // No-op, message was already sent by pgm + case PLAYER_WEB: + pgmMatch.sendMessage( + text() + .append(text("<", NamedTextColor.WHITE)) + .append(senderName) + .append(text(">: ", NamedTextColor.WHITE)) + .append(body) + .build()); + break; + case SYSTEM_KO: + // If it is not specific to a player, it will fall-thru to the bottom case + if (chat.getPlayer() != null) { + if (sender != null) sender.sendWarning(body); + break; + } + case SYSTEM: + Component message = + text() + .append(text("[", NamedTextColor.WHITE)) + .append(text("PUG", NamedTextColor.GOLD)) + .append(text("] ", NamedTextColor.WHITE)) + .append(senderName) + .append(text(" » ", NamedTextColor.GRAY)) + .append(body) + .build(); + + if (AppData.publiclyLogPugs()) { + pgmMatch.sendMessage(message); + } else { + for (MatchPlayer player : pgmMatch.getPlayers()) { + if (player.getBukkit().isOp()) player.sendMessage(message); + } + } + + break; + } + } + + @Override + public void onClose(int i, String s, boolean b) { + System.out.println("[Ingame] Closed socket " + i + " - " + b + " " + s + ""); + + // Try to reconnect if failed. + if (i == CloseFrame.NEVER_CONNECTED || i == CloseFrame.ABNORMAL_CLOSE) { + Ingame.newSharedChain("pug-sync").sync(manager::reconnect).execute(); + } + } + + @Override + public void onError(Exception e) { + System.out.println(e.getMessage()); + } +} diff --git a/src/main/java/rip/bolt/ingame/pugs/ManagedTeam.java b/src/main/java/rip/bolt/ingame/pugs/ManagedTeam.java new file mode 100644 index 0000000..e588d54 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/pugs/ManagedTeam.java @@ -0,0 +1,63 @@ +package rip.bolt.ingame.pugs; + +import dev.pgm.events.team.TournamentTeam; +import java.util.List; +import rip.bolt.ingame.api.definitions.Team; +import rip.bolt.ingame.api.definitions.pug.PugPlayer; +import rip.bolt.ingame.api.definitions.pug.PugTeam; + +public class ManagedTeam implements TournamentTeam { + private final String pugTeamId; + + private PugTeam pugTeam; + private Team boltTeam; + private tc.oc.pgm.teams.Team pgmTeam; + + public ManagedTeam(String teamId) { + this.pugTeamId = teamId; + } + + public String getId() { + return pugTeamId; + } + + public PugTeam getPugTeam() { + return pugTeam; + } + + public void setPugTeam(PugTeam pugTeam) { + this.pugTeam = pugTeam; + } + + public Team getBoltTeam() { + return boltTeam; + } + + public void setBoltTeam(Team boltTeam) { + this.boltTeam = boltTeam; + } + + public tc.oc.pgm.teams.Team getPgmTeam() { + return pgmTeam; + } + + public void setPgmTeam(tc.oc.pgm.teams.Team pgmTeam) { + this.pgmTeam = pgmTeam; + } + + public void clean() { + this.pugTeam = null; + this.boltTeam = null; + this.pgmTeam = null; + } + + @Override + public String getName() { + return pugTeam.getName(); + } + + @Override + public List getPlayers() { + return pugTeam.getPlayers(); + } +} diff --git a/src/main/java/rip/bolt/ingame/pugs/PugListener.java b/src/main/java/rip/bolt/ingame/pugs/PugListener.java new file mode 100644 index 0000000..6992f1c --- /dev/null +++ b/src/main/java/rip/bolt/ingame/pugs/PugListener.java @@ -0,0 +1,155 @@ +package rip.bolt.ingame.pugs; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +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.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import rip.bolt.ingame.Ingame; +import rip.bolt.ingame.api.definitions.BoltMatch; +import rip.bolt.ingame.api.definitions.pug.PugCommand; +import rip.bolt.ingame.api.definitions.pug.PugTeam; +import rip.bolt.ingame.events.BoltMatchStatusChangeEvent; +import tc.oc.pgm.api.event.PlayerVanishEvent; +import tc.oc.pgm.api.match.event.MatchLoadEvent; +import tc.oc.pgm.api.party.Competitor; +import tc.oc.pgm.api.party.event.PartyRenameEvent; +import tc.oc.pgm.events.PlayerParticipationStartEvent; +import tc.oc.pgm.events.PlayerParticipationStopEvent; +import tc.oc.pgm.teams.events.TeamResizeEvent; + +public class PugListener implements Listener { + + private final PugManager pugManager; + private final Set transitioningPlayers; + + public PugListener(PugManager pugManager) { + this.pugManager = pugManager; + + transitioningPlayers = new HashSet<>(); + } + + @EventHandler(priority = EventPriority.LOW) + public void onPlayerJoinLow(PlayerJoinEvent event) { + transitioningPlayers.add(event.getPlayer().getUniqueId()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerJoin(PlayerJoinEvent event) { + transitioningPlayers.remove(event.getPlayer().getUniqueId()); + pugManager.syncPlayerStatus(event.getPlayer(), true); + } + + @EventHandler(priority = EventPriority.LOW) + public void onPlayerQuitLow(PlayerQuitEvent event) { + transitioningPlayers.add(event.getPlayer().getUniqueId()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent event) { + transitioningPlayers.remove(event.getPlayer().getUniqueId()); + pugManager.syncPlayerStatus(event.getPlayer(), false); + } + + @EventHandler + public void onPlayerVanish(PlayerVanishEvent event) { + if (transitioningPlayers.contains(event.getPlayer().getId())) return; + pugManager.syncPlayerStatus(event.getPlayer().getBukkit(), !event.isVanished()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onMatchCycleEvent(MatchLoadEvent event) { + pugManager.syncMatchTeams(); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerChat(AsyncPlayerChatEvent event) { + // A bit hacky, but we don't have another way to know if it's global chat. + if (event.getFormat() == null || !event.getFormat().equals("<%s>: %s")) return; + Player p = event.getPlayer(); + if (p == null) return; + pugManager.write(PugCommand.sendMessage(p, event.getMessage())); + } + + @EventHandler + public void onBoltMatchStateChange(BoltMatchStatusChangeEvent event) { + BoltMatch match = event.getBoltMatch(); + System.out.println("[Ingame] <- Match ID " + match.getId()); + System.out.println("[Ingame] <- Match Status: " + match.getStatus()); + + pugManager.write(PugCommand.setMatchStatus(match)); + } + + @EventHandler + public void onTeamChangeSize(TeamResizeEvent event) { + int newMax = event.getTeam().getMaxPlayers(); + if (pugManager.getLobby().getTeams().stream().allMatch(pt -> pt.getMaxPlayers() == newMax)) + return; + pugManager.write( + PugCommand.setTeamSize(null, newMax * pugManager.getLobby().getTeams().size())); + } + + @EventHandler + public void onTeamRename(PartyRenameEvent event) { + if (!(event.getParty() instanceof Competitor)) return; + PugTeam pugTeam = pugManager.findPugTeam(event.getParty()); + if (pugTeam == null || pugTeam.getName().equals(event.getParty().getNameLegacy())) return; + + String newName = event.getParty().getNameLegacy(); + if (newName.length() > 32) newName = newName.substring(0, 32); + pugManager.write(PugCommand.setTeamName(null, pugTeam, newName)); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onParticipate(PlayerParticipationStartEvent event) { + if (!event.isCancelled()) return; + + // Events should expose this constant. It'll still be dirty, but will survive updates. + if (!isMessage(event.getCancelReason(), "You may not join in a tournament setting!")) return; + event.cancel(Component.empty()); + + // Events probably cancelled the join + PugTeam team = pugManager.findPugTeam(event.getCompetitor()); + if (team != null) pugManager.write(PugCommand.joinTeam(event.getPlayer().getBukkit(), team)); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onLeaveParticipate(PlayerParticipationStopEvent event) { + if (!event.isCancelled()) return; + + // Events should expose this constant. It'll still be dirty, but will survive updates. + if (!isMessage(event.getCancelReason(), "You may not leave in a tournament setting!")) return; + event.cancel(Component.empty()); + pugManager.write(PugCommand.joinObs(event.getPlayer().getBukkit())); + } + + private boolean isMessage(Component component, String msg) { + if (!(component instanceof TextComponent)) return false; + TextComponent textComponent = (TextComponent) component; + return textComponent.content().equals(msg); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onPlayerPGMCommand(PlayerCommandPreprocessEvent event) { + String cmd = event.getMessage().substring(1).toLowerCase(Locale.ROOT); + + for (String command : Ingame.get().getPugCommands().getCommandList()) { + if (cmd.startsWith(command)) { + event.setMessage("/pug " + event.getMessage().substring(1)); + return; + } + } + } + + // TODO handle cycles as new matches. + +} diff --git a/src/main/java/rip/bolt/ingame/pugs/PugManager.java b/src/main/java/rip/bolt/ingame/pugs/PugManager.java new file mode 100644 index 0000000..2f76831 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/pugs/PugManager.java @@ -0,0 +1,283 @@ +package rip.bolt.ingame.pugs; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import dev.pgm.events.EventsPlugin; +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.java_websocket.framing.CloseFrame; +import org.jetbrains.annotations.Nullable; +import rip.bolt.ingame.Ingame; +import rip.bolt.ingame.api.definitions.BoltMatch; +import rip.bolt.ingame.api.definitions.MatchStatus; +import rip.bolt.ingame.api.definitions.pug.PugCommand; +import rip.bolt.ingame.api.definitions.pug.PugLobby; +import rip.bolt.ingame.api.definitions.pug.PugMatch; +import rip.bolt.ingame.api.definitions.pug.PugPlayer; +import rip.bolt.ingame.api.definitions.pug.PugState; +import rip.bolt.ingame.api.definitions.pug.PugTeam; +import rip.bolt.ingame.config.AppData; +import rip.bolt.ingame.managers.GameManager; +import rip.bolt.ingame.managers.MatchManager; +import rip.bolt.ingame.utils.CancelReason; +import tc.oc.pgm.api.integration.Integration; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.party.Party; +import tc.oc.pgm.start.StartCountdown; +import tc.oc.pgm.start.StartMatchModule; + +public class PugManager extends GameManager { + + private final ObjectMapper objectMapper; + private final String wsUrl; + private final PugListener listener; + + private final PugTeamManager teamManager; + + private BoltWebSocket boltWebSocket; + private PugLobby pugLobby; + + private boolean newConnection = true; + + private PugManager(MatchManager matchManager) { + super(matchManager); + + this.objectMapper = Ingame.get().getApiManager().objectMapper; + this.wsUrl = AppData.Socket.getUrl(); + + this.listener = new PugListener(this); + this.teamManager = new PugTeamManager(matchManager, this); + } + + public static PugManager of(MatchManager matchManager) { + if (matchManager.getGameManager() instanceof PugManager) { + PugManager existing = (PugManager) matchManager.getGameManager(); + if (existing.pugLobby.getId().equals(matchManager.getMatch().getLobbyId())) return existing; + } + return new PugManager(matchManager); + } + + @Override + public void enable(MatchManager manager) { + super.enable(manager); + connect(manager.getMatch()); + + Bukkit.getPluginManager().registerEvents(listener, Ingame.get()); + Bukkit.getPluginManager().registerEvents(teamManager, Ingame.get()); + + EventsPlugin.get().getTeamManager().clear(); + } + + @Override + public void setup(BoltMatch match) { + if (this.pugLobby != null) teamManager.setupTeams(match); + } + + @Override + public void disable() { + super.disable(); + HandlerList.unregisterAll(listener); + HandlerList.unregisterAll(teamManager); + + this.disconnect(); + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + public BoltWebSocket getBoltWebSocket() { + return boltWebSocket; + } + + public PugLobby getLobby() { + return pugLobby; + } + + public String getLobbyId() { + return pugLobby != null ? pugLobby.getId() : null; + } + + public void connect(BoltMatch boltMatch) { + if (this.getLobbyId() != null && !Objects.equals(boltMatch.getLobbyId(), getLobbyId())) + disconnect(); + + // Check if match is a pug + if (boltMatch.getLobbyId() == null) return; + + boltWebSocket = new BoltWebSocket(URI.create(wsUrl + "/pugs/" + boltMatch.getLobbyId()), this); + boltWebSocket.addHeader("Authorization", "Bearer " + AppData.API.getKey()); + boltWebSocket.connect(); + + System.out.println("[Ingame] Connected to " + boltMatch.getLobbyId()); + } + + public void reset() { + this.newConnection = true; + } + + public void write(PugCommand command) { + try { + this.boltWebSocket.send(objectMapper.writeValueAsString(command)); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } + + public void disconnect() { + if (boltWebSocket == null) return; + + System.out.println("[Ingame] Disconnected from " + getLobbyId()); + + boltWebSocket.close(); + } + + public void syncPugLobby(JsonNode newLobby) throws IOException { + if (newLobby == null) return; + + if (this.pugLobby == null) { + this.pugLobby = objectMapper.treeToValue(newLobby, PugLobby.class); + setup(matchManager.getMatch()); + } else { + ObjectReader objectReader = objectMapper.readerForUpdating(this.pugLobby); + this.pugLobby = objectReader.readValue(newLobby); + } + + MatchStatus status = matchManager.getMatch().getStatus(); + PugMatch pugMatch = this.pugLobby.getMatch(); + + if (newConnection && pugLobby.getState() != PugState.FINISHED) { + newConnection = false; + syncOnline(); + + // We have just reconnected to the WS, let the pug know about the current match status. + // If it's old it'll get ignored. + write(PugCommand.setMatchStatus(this.matchManager.getMatch())); + } + + // Avoid updating status if there is no match + if (pugMatch == null) return; + + if (!Objects.equals(matchManager.getMatch().getId(), pugMatch.getId())) { + if (!matchManager.getMatch().getStatus().isFinished()) + matchManager.cancel(matchManager.getPGMMatch(), CancelReason.MANUAL_CANCEL); + + // Setup the new match and cycle to it + this.matchManager.setupMatch( + new BoltMatch( + matchManager.getMatch().getLobbyId(), matchManager.getMatch().getSeries(), pugMatch)); + + return; + } + + // Detect match cancellation + if (pugMatch.getStatus().equals(MatchStatus.CANCELLED)) { + if (!matchManager.getMatch().getStatus().isFinished()) + matchManager.cancel(matchManager.getPGMMatch(), CancelReason.MANUAL_CANCEL); + } + + if (this.pugLobby.getState() == PugState.FINISHED) { + this.boltWebSocket.close(CloseFrame.NORMAL, "Pug is in finished status"); + return; + } + + // Stop processing 'reactive' components + if (status.isFinished()) return; + + // Start match countdown if required + syncMatchStart(status, pugMatch); + + if (pugLobby.getTeams() != null) teamManager.syncMatchTeams(); + } + + public PugTeam findPugTeam(@Nullable Party team) { + ManagedTeam mt = teamManager.getTeam(team); + return mt == null ? null : mt.getPugTeam(); + } + + public void syncMatchTeams() { + teamManager.syncMatchTeams(); + } + + private void syncMatchStart(MatchStatus status, PugMatch pugMatch) { + if (!status.equals(MatchStatus.LOADED)) return; + + Instant newStart = pugMatch.getStartedAt(); + + // Check if started at are same (no change needed) + if (Objects.equals(matchManager.getMatch().getStartedAt(), newStart)) return; + matchManager.getMatch().setStartedAt(newStart); + + Match match = matchManager.getPGMMatch(); + + // Cancel start by sending a null start time + if (newStart == null) { + match.getCountdown().cancelAll(StartCountdown.class); + return; + } + + // Always at least 5s start. Round up to 4s up, or 1s down, to keep a multiple of 5s. + long startingInMillis = Math.max(Duration.between(Instant.now(), newStart).toMillis(), 5_000); + Duration startIn = Duration.ofSeconds(5 * ((startingInMillis + 4_000) / 5_000)); + match.needModule(StartMatchModule.class).forceStartCountdown(startIn, Duration.ZERO); + } + + private void syncOnline() { + List lobbyPlayers = this.pugLobby.getPlayers(); + + Set onlinePlayers = + Bukkit.getServer().getOnlinePlayers().stream() + .map(p -> syncPlayerStatus(p, true)) + .filter(Objects::nonNull) + .map(PugPlayer::getUuid) + .collect(Collectors.toSet()); + + lobbyPlayers.stream() + .filter(pugPlayer -> !onlinePlayers.contains(pugPlayer.getUuid())) + .forEach(lobbyPlayer -> syncPlayerStatus(lobbyPlayer, false)); + } + + private void syncPlayerStatus(PugPlayer pugPlayer, boolean online) { + write(PugCommand.setPlayerStatus(pugPlayer, online)); + } + + public PugPlayer syncPlayerStatus(Player player, boolean online) { + // Do not send online status for vanished players + if (online && Integration.isVanished(player)) return null; + + PugPlayer pugPlayer = new PugPlayer(player.getUniqueId(), player.getName()); + syncPlayerStatus(pugPlayer, online); + return pugPlayer; + } + + public void reconnect() { + // Stop trying if lobby has changed + if (pugLobby.getId() != null + && !Objects.equals(matchManager.getMatch().getLobbyId(), pugLobby.getId())) return; + + if (boltWebSocket.isOpen()) return; + + new Thread( + () -> { + try { + Thread.sleep(5000L); + } catch (InterruptedException ignore) { + } + + boltWebSocket.reconnect(); + }) + .start(); + } +} diff --git a/src/main/java/rip/bolt/ingame/pugs/PugTeamManager.java b/src/main/java/rip/bolt/ingame/pugs/PugTeamManager.java new file mode 100644 index 0000000..42fb635 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/pugs/PugTeamManager.java @@ -0,0 +1,171 @@ +package rip.bolt.ingame.pugs; + +import dev.pgm.events.EventsPlugin; +import dev.pgm.events.team.TournamentTeamManager; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.jetbrains.annotations.Nullable; +import rip.bolt.ingame.api.definitions.BoltMatch; +import rip.bolt.ingame.api.definitions.Participation; +import rip.bolt.ingame.api.definitions.Team; +import rip.bolt.ingame.api.definitions.User; +import rip.bolt.ingame.api.definitions.pug.PugLobby; +import rip.bolt.ingame.api.definitions.pug.PugPlayer; +import rip.bolt.ingame.api.definitions.pug.PugTeam; +import rip.bolt.ingame.events.BoltMatchResponseEvent; +import rip.bolt.ingame.managers.MatchManager; +import tc.oc.pgm.api.party.Party; + +public class PugTeamManager implements Listener { + + private final MatchManager matchManager; + private final PugManager pugManager; + private final TournamentTeamManager teamManager; + + private final Map pugTeams; + + public PugTeamManager(MatchManager matchManager, PugManager pugManager) { + this.matchManager = matchManager; + this.pugManager = pugManager; + this.teamManager = EventsPlugin.get().getTeamManager(); + + this.pugTeams = new HashMap<>(); + } + + private PugLobby getLobby() { + return pugManager.getLobby(); + } + + private List getTeams() { + return pugManager.getLobby().getTeams(); + } + + public ManagedTeam getTeam(String pugTeamId) { + return pugTeams.get(pugTeamId); + } + + public ManagedTeam getTeam(Integer boltTeamId) { + for (ManagedTeam managedTeam : pugTeams.values()) { + if (managedTeam.getBoltTeam() == null) continue; + + if (managedTeam.getBoltTeam().getId().equals(boltTeamId)) { + return managedTeam; + } + } + + return null; + } + + public ManagedTeam getTeam(@Nullable Party team) { + for (ManagedTeam mt : pugTeams.values()) { + if (team == null || mt.getPgmTeam() == team) return mt; + } + return null; + } + + /** + * Process cycling teams from an old match into a new one This method may add or remove teams, + * unregister or register teams from events. + */ + public void setupTeams(BoltMatch match) { + Map teams = + getTeams().stream().collect(Collectors.toMap(PugTeam::getId, Function.identity())); + + List toRemoveIds = new ArrayList<>(pugTeams.keySet()); + toRemoveIds.retainAll(teams.keySet()); + + // Pain. EventsPlugin doesn't allow removing just one team, gotta remove them all and re-add. + if (!toRemoveIds.isEmpty()) { + teamManager.clear(); + for (String toRemoveId : toRemoveIds) { + ManagedTeam mt = pugTeams.remove(toRemoveId); + mt.clean(); + } + } + + Iterator boltTeamsIt = match.getTeams().iterator(); + teams.forEach( + (id, team) -> { + ManagedTeam mt = pugTeams.computeIfAbsent(id, ManagedTeam::new); + mt.clean(); + mt.setPugTeam(team); + mt.setBoltTeam(boltTeamsIt.next()); + teamManager.addTeam(mt); + }); + } + + public void syncMatchTeams() { + // We have not cycled yet, just wait to sync + if (!Objects.equals(matchManager.getPGMMatchId(), getLobby().getMatch().getId())) return; + // Do not update teams after match end, before stats get reported + if (getLobby().getMatch().getStatus().isFinished()) return; + + // Push all players on to correct team (if not already on) + getTeams().forEach(this::syncMatchTeam); + + // Events plugin to sync in game players with stored teams + this.teamManager.syncTeams(); + } + + private void syncMatchTeam(PugTeam lobbyTeam) { + ManagedTeam mt = getTeam(lobbyTeam.getId()); + mt.setPugTeam(lobbyTeam); + teamManager.fromTournamentTeam(mt).ifPresent(mt::setPgmTeam); + + // Sync team names + String newTeamName = lobbyTeam.getName(); + String oldTeamName = mt.getBoltTeam().getName(); + if (!Objects.equals(oldTeamName, newTeamName)) { + mt.getBoltTeam().setName(newTeamName); + if (mt.getPgmTeam() != null) mt.getPgmTeam().setName(newTeamName); + } + + // Change size if required + if (mt.getPgmTeam() != null) { + int curr = mt.getPgmTeam().getMaxPlayers(); + int wanted = lobbyTeam.getMaxPlayers(); + + if (curr != wanted) mt.getPgmTeam().setMaxSize(wanted, wanted); + } + + // Loop players and get current participation or create new + List participations = + mt.getPlayers().stream() + .map(player -> getOrCreate(mt.getBoltTeam(), player)) + .collect(Collectors.toList()); + + // Clear team participation list and populate with new + mt.getBoltTeam().getParticipations().clear(); + mt.getBoltTeam().getParticipations().addAll(participations); + } + + private Participation getOrCreate(Team team, PugPlayer player) { + return team.getParticipations().stream() + .filter(participation -> participation.getUser().getUuid().equals(player.getUuid())) + .findFirst() + .orElseGet(() -> new Participation(new User(player.getUUID(), player.getUsername()))); + } + + @EventHandler(priority = EventPriority.NORMAL) + public void onBoltMatchResponse(BoltMatchResponseEvent event) { + event + .getResponseMatch() + .getTeams() + .forEach( + team -> { + ManagedTeam mt = getTeam(team.getId()); + if (mt != null) mt.setBoltTeam(team); + }); + + syncMatchTeams(); + } +} diff --git a/src/main/java/rip/bolt/ingame/ranked/ForfeitManager.java b/src/main/java/rip/bolt/ingame/ranked/ForfeitManager.java deleted file mode 100644 index 0bc7e91..0000000 --- a/src/main/java/rip/bolt/ingame/ranked/ForfeitManager.java +++ /dev/null @@ -1,164 +0,0 @@ -package rip.bolt.ingame.ranked; - -import static net.kyori.adventure.text.Component.text; - -import dev.pgm.events.Tournament; -import dev.pgm.events.team.TournamentPlayer; -import dev.pgm.events.team.TournamentTeamManager; -import java.time.Duration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; -import net.kyori.adventure.text.format.NamedTextColor; -import rip.bolt.ingame.config.AppData; -import rip.bolt.ingame.utils.Messages; -import tc.oc.pgm.api.match.Match; -import tc.oc.pgm.api.match.MatchScope; -import tc.oc.pgm.api.party.Competitor; -import tc.oc.pgm.api.party.Party; -import tc.oc.pgm.api.party.VictoryCondition; -import tc.oc.pgm.api.player.MatchPlayer; -import tc.oc.pgm.result.CompetitorVictoryCondition; -import tc.oc.pgm.result.TieVictoryCondition; -import tc.oc.pgm.teams.Team; - -public class ForfeitManager { - - private static final Duration FORFEIT_DURATION = AppData.forfeitAfter(); - - private final PlayerWatcher playerWatcher; - - private final Map leaves = new HashMap<>(); - private final Map forfeit = new HashMap<>(); - - public ForfeitManager(PlayerWatcher playerWatcher) { - this.playerWatcher = playerWatcher; - } - - public ForfeitPoll getForfeitPoll(Competitor team) { - return forfeit.computeIfAbsent(team, ForfeitPoll::new); - } - - public boolean mayForfeit(Competitor team) { - if (!AppData.forfeitEnabled()) return false; - if (team.getMatch().getDuration().compareTo(FORFEIT_DURATION) >= 0) return true; - - return getRegisteredPlayers(team) - .map(playerWatcher::getParticipation) - .filter(Objects::nonNull) - .anyMatch(PlayerWatcher.MatchParticipation::hasAbandoned); - } - - public void clearPolls() { - leaves.clear(); - forfeit.clear(); - } - - private Stream getRegisteredPlayers(Competitor team) { - TournamentTeamManager teamManager = Tournament.get().getTeamManager(); - return teamManager - .tournamentTeam(team) - .map(t -> t.getPlayers().stream()) - .orElse(Stream.empty()) - .map(TournamentPlayer::getUUID); - } - - public void updateCountdown(Party team) { - if (!AppData.forfeitEnabled() || !(team instanceof Competitor)) return; - - leaves.computeIfAbsent((Competitor) team, LeaveAnnouncer::new).update(); - } - - public class LeaveAnnouncer { - private final Competitor team; - private boolean hasCompleted; - - private ScheduledFuture scheduledFuture; - - public LeaveAnnouncer(Competitor team) { - this.team = team; - } - - private void broadcast() { - if (this.hasCompleted) return; - this.hasCompleted = true; - team.sendMessage(Messages.forfeit()); - } - - private void update() { - if (this.hasCompleted) return; - - if (scheduledFuture != null) { - scheduledFuture.cancel(false); - scheduledFuture = null; - } - - getRegisteredPlayers(team) - .map(playerWatcher::getParticipation) - .filter(PlayerWatcher.MatchParticipation::canStartCountdown) - .map(PlayerWatcher.MatchParticipation::absentDuration) - .max(Duration::compareTo) - .map(PlayerWatcher.ABSENT_MAX::minus) - .filter(duration -> !duration.isNegative()) - .ifPresent( - duration -> - scheduledFuture = - team.getMatch() - .getExecutor(MatchScope.RUNNING) - .schedule(this::broadcast, duration.toMillis(), TimeUnit.MILLISECONDS)); - } - } - - public static class ForfeitPoll { - - private final Competitor team; - private final Set voted = new HashSet<>(); - - public ForfeitPoll(Competitor team) { - this.team = team; - } - - public Set getVoted() { - return voted; - } - - public void addVote(MatchPlayer player) { - voted.add(player.getId()); - check(); - } - - public void check() { - if (hasPassed()) endMatch(); - } - - private boolean hasPassed() { - return voted.stream().filter(uuid -> team.getPlayer(uuid) != null).count() - >= Math.min( - team instanceof Team ? ((Team) team).getMaxPlayers() - 1 : 0, - team.getPlayers().size()); - } - - public void endMatch() { - Match match = team.getMatch(); - match.sendMessage( - team.getName().append(text(" has voted to forfeit the match.", NamedTextColor.WHITE))); - - // Create victory condition for other team or tie - VictoryCondition victoryCondition = - match.getCompetitors().stream() - .filter(competitor -> !competitor.equals(team)) - .findFirst() - .map(CompetitorVictoryCondition::new) - .orElseGet(TieVictoryCondition::new); - - match.addVictoryCondition(victoryCondition); - match.finish(null); - } - } -} diff --git a/src/main/java/rip/bolt/ingame/ranked/RankedManager.java b/src/main/java/rip/bolt/ingame/ranked/RankedManager.java index 009b799..c877830 100644 --- a/src/main/java/rip/bolt/ingame/ranked/RankedManager.java +++ b/src/main/java/rip/bolt/ingame/ranked/RankedManager.java @@ -1,302 +1,91 @@ package rip.bolt.ingame.ranked; -import com.google.common.collect.Iterables; -import dev.pgm.events.Tournament; -import dev.pgm.events.format.RoundReferenceHolder; -import dev.pgm.events.format.TournamentFormat; -import dev.pgm.events.format.TournamentFormatImpl; -import dev.pgm.events.format.TournamentRoundOptions; -import dev.pgm.events.format.rounds.single.SingleRound; -import dev.pgm.events.format.rounds.single.SingleRoundOptions; -import dev.pgm.events.format.winner.BestOfCalculation; +import dev.pgm.events.EventsPlugin; import dev.pgm.events.team.TournamentPlayer; import dev.pgm.events.team.TournamentTeam; -import java.time.Duration; -import java.time.Instant; import java.util.Collection; -import java.util.Objects; import java.util.stream.Collectors; -import net.md_5.bungee.api.ChatColor; import org.bukkit.Bukkit; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.plugin.Plugin; +import org.bukkit.event.HandlerList; import rip.bolt.ingame.Ingame; import rip.bolt.ingame.api.definitions.BoltMatch; -import rip.bolt.ingame.api.definitions.Team; import rip.bolt.ingame.config.AppData; -import rip.bolt.ingame.events.BoltMatchStatusChangeEvent; -import rip.bolt.ingame.utils.CancelReason; -import rip.bolt.ingame.utils.Messages; -import rip.bolt.ingame.utils.PGMMapUtils; -import tc.oc.pgm.api.PGM; +import rip.bolt.ingame.events.BoltMatchResponseEvent; +import rip.bolt.ingame.managers.GameManager; +import rip.bolt.ingame.managers.MatchManager; +import rip.bolt.ingame.ranked.forfeit.PlayerWatcher; import tc.oc.pgm.api.match.Match; -import tc.oc.pgm.api.match.MatchPhase; -import tc.oc.pgm.api.match.event.MatchFinishEvent; -import tc.oc.pgm.api.match.event.MatchLoadEvent; -import tc.oc.pgm.api.match.event.MatchStartEvent; import tc.oc.pgm.api.party.Competitor; -import tc.oc.pgm.restart.RestartManager; -import tc.oc.pgm.result.TieVictoryCondition; -import tc.oc.pgm.util.Audience; -public class RankedManager implements Listener { +public class RankedManager extends GameManager { private final PlayerWatcher playerWatcher; - private final RequeueManager requeueManager; - private final RankManager rankManager; - private final StatsManager statsManager; private final SpectatorManager spectatorManager; - private final KnockbackManager knockbackManager; - private final TabManager tabManager; - private final MatchSearch poll; - - private TournamentFormat format; - private BoltMatch match; - - private Duration cycleTime = Duration.ofSeconds(0); - private CancelReason cancelReason = null; - - public RankedManager(Plugin plugin) { - playerWatcher = new PlayerWatcher(this); - requeueManager = new RequeueManager(); - rankManager = new RankManager(this); - statsManager = new StatsManager(this); - spectatorManager = new SpectatorManager(playerWatcher); - knockbackManager = new KnockbackManager(); - tabManager = new TabManager(plugin); + private final RequeueManager requeueManager; - MatchPreloader.create(); + public RankedManager(MatchManager matchManager) { + super(matchManager); - poll = new MatchSearch(this::setupMatch); - poll.startIn(Duration.ofSeconds(5)); + this.playerWatcher = new PlayerWatcher(matchManager); + this.spectatorManager = new SpectatorManager(playerWatcher); + this.requeueManager = new RequeueManager(); } - public void setupMatch(BoltMatch match) { - if (!this.isServerReady()) return; - if (!this.isMatchValid(match)) return; - - this.match = match; - this.cancelReason = null; - poll.stop(); + @Override + public void enable(MatchManager manager) { + super.enable(manager); - Tournament.get().getTeamManager().clear(); - for (TournamentTeam team : match.getTeams()) Tournament.get().getTeamManager().addTeam(team); + Bukkit.getPluginManager().registerEvents(this.playerWatcher, Ingame.get()); + Bukkit.getPluginManager().registerEvents(this.spectatorManager, Ingame.get()); + Bukkit.getPluginManager().registerEvents(this.requeueManager, Ingame.get()); + } + @Override + public void setup(BoltMatch match) { + EventsPlugin.get().getTeamManager().clear(); + for (TournamentTeam team : match.getTeams()) EventsPlugin.get().getTeamManager().addTeam(team); playerWatcher.addPlayers( match.getTeams().stream() .flatMap(team -> team.getPlayers().stream()) .map(TournamentPlayer::getUUID) .collect(Collectors.toList())); - - format = - new TournamentFormatImpl( - Tournament.get().getTeamManager(), - new TournamentRoundOptions( - false, - false, - false, - Duration.ofMinutes(30), - Duration.ofSeconds(30), - Duration.ofSeconds(40), - new BestOfCalculation<>(1)), - new RoundReferenceHolder()); - SingleRound ranked = - new SingleRound( - format, - new SingleRoundOptions( - "ranked", - cycleTime, - AppData.matchStartDuration(), - match.getMap().getName(), - 1, - true, - true)); - cycleTime = Duration.ofSeconds(5); - format.addRound(ranked); - - Ingame.get() - .getServer() - .getPluginManager() - .callEvent(new BoltMatchStatusChangeEvent(match, null, MatchStatus.CREATED)); - - Bukkit.broadcastMessage(ChatColor.YELLOW + "A new match is starting on this server!"); - Tournament.get() - .getTournamentManager() - .createTournament(PGM.get().getMatchManager().getMatches().next(), format); - } - - private void updateMatch(BoltMatch newMatch) { - BoltMatch oldMatch = this.match; - if (oldMatch == null - || newMatch == null - || !Objects.equals(oldMatch.getId(), newMatch.getId()) - || newMatch.getStatus() != MatchStatus.ENDED) { - return; - } - - this.match = newMatch; - rankManager.handleMatchUpdate(oldMatch, newMatch); } - private boolean isMatchValid(BoltMatch match) { - return match != null - && match.getId() != null - && !match.getId().isEmpty() - && match.getMap() != null - && (match.getStatus().equals(MatchStatus.CREATED) - || match.getStatus().equals(MatchStatus.LOADED)) - && (this.match == null || !Objects.equals(this.match.getId(), match.getId())); - } - - private boolean isServerReady() { - return !RestartManager.isQueued() && RestartManager.getCountdown() == null; - } - - public BoltMatch getMatch() { - return match; + @Override + public void disable() { + super.disable(); + HandlerList.unregisterAll(this.playerWatcher); + HandlerList.unregisterAll(this.spectatorManager); + HandlerList.unregisterAll(this.requeueManager); } public PlayerWatcher getPlayerWatcher() { return playerWatcher; } - public RankManager getRankManager() { - return rankManager; - } - - public MatchSearch getPoll() { - return poll; + public SpectatorManager getSpectatorManager() { + return spectatorManager; } public RequeueManager getRequeueManager() { return requeueManager; } - public CancelReason getCancelReason() { - return cancelReason; - } - - public void manualPoll(boolean repeat) { - if (repeat) { - poll.startIn(0L); - return; - } - - poll.trigger(true); - } - - public void manualReset() { - this.match = null; - } - - public void cancel(Match match, CancelReason reason) { - this.cancelReason = reason; - this.postMatchStatus(match, MatchStatus.CANCELLED); - - // Cancel countdowns if match has not started - if (match.getPhase().equals(MatchPhase.STARTING)) { - match.getCountdown().cancelAll(); - } - - // Check if match is in progress - if (match.getPhase().equals(MatchPhase.RUNNING)) { - // Add tie victory condition if in progress - match.addVictoryCondition(new TieVictoryCondition()); - match.finish(); - } else { - // Prompt players to requeue and start polling - Audience.get(match.getCompetitors()).sendMessage(Messages.requeue()); - this.getPoll().startIn(Duration.ofSeconds(15)); - } - } - - @EventHandler - public void onMatchLoad(MatchLoadEvent event) { - postMatchStatus(event.getMatch(), MatchStatus.LOADED); - } - @EventHandler(priority = EventPriority.MONITOR) - public void onMatchStart(MatchStartEvent event) { - postMatchStatus(event.getMatch(), MatchStatus.STARTED); - } - - @EventHandler - public void onMatchFinish(MatchFinishEvent event) { - postMatchStatus(event.getMatch(), MatchStatus.ENDED); - - poll.startIn(Duration.ofSeconds(30)); - + public void onBoltMatchResponse(BoltMatchResponseEvent event) { if (!AppData.allowRequeue()) return; - // delay requeue message until after match stats are sent - Bukkit.getScheduler() - .scheduleSyncDelayedTask( - Ingame.get(), - () -> - event.getMatch().getCompetitors().stream() - .map(Competitor::getPlayers) - .flatMap(Collection::stream) - .forEach(requeueManager::sendRequeueMessage), - 20); - } - - public void postMatchStatus(Match match, MatchStatus status) { - Instant now = Instant.now(); - Ingame.newSharedChain("match") - .syncFirst(() -> transition(match, this.match, status, now)) - .abortIfNull() - .async(Ingame.get().getApiManager()::postMatch) - .syncLast(this::updateMatch) - .execute(); - } - - public BoltMatch transition( - Match match, BoltMatch boltMatch, MatchStatus newStatus, Instant transitionAt) { - if (boltMatch == null) return null; - - MatchStatus oldStatus = boltMatch.getStatus(); - if (!oldStatus.canTransitionTo(newStatus)) return null; - switch (newStatus) { - case LOADED: - break; - case STARTED: - boltMatch.setMap(PGMMapUtils.getBoltPGMMap(boltMatch, match)); - boltMatch.setStartedAt(transitionAt); - break; - case ENDED: - statsManager.handleMatchUpdate(boltMatch, match); - boltMatch.setEndedAt(transitionAt); - Collection winners = match.getWinners(); - if (winners.size() == 1) { - format - .teamManager() - .tournamentTeam(Iterables.getOnlyElement(winners)) - .filter(t -> t instanceof Team) - .map(t -> (Team) t) - .ifPresent(winner -> boltMatch.setWinner(new Team(winner.getId()))); - } - break; - case CANCELLED: - boltMatch.setEndedAt(transitionAt); - break; + if (event.hasMatchFinished()) { + sendRequeueMessage(event.getPgmMatch()); } - - boltMatch.setStatus(newStatus); - Ingame.get() - .getServer() - .getPluginManager() - .callEvent(new BoltMatchStatusChangeEvent(boltMatch, oldStatus, newStatus)); - - return boltMatch; - } - - public SpectatorManager getSpectatorManager() { - return spectatorManager; } - public KnockbackManager getKnockbackManager() { - return knockbackManager; + private void sendRequeueMessage(Match match) { + match.getCompetitors().stream() + .map(Competitor::getPlayers) + .flatMap(Collection::stream) + .forEach(requeueManager::sendRequeueMessage); } } diff --git a/src/main/java/rip/bolt/ingame/ranked/RequeueManager.java b/src/main/java/rip/bolt/ingame/ranked/RequeueManager.java index 91b4a62..5e98862 100644 --- a/src/main/java/rip/bolt/ingame/ranked/RequeueManager.java +++ b/src/main/java/rip/bolt/ingame/ranked/RequeueManager.java @@ -24,7 +24,7 @@ public class RequeueManager implements Listener { private static final ItemStack RED_DYE = createRequeueItem(1); private static final ItemStack GREEN_DYE = createRequeueItem(2); - private Map lastRequeues = new OnlinePlayerMapAdapter(Ingame.get()); + private final Map lastRequeues = new OnlinePlayerMapAdapter<>(Ingame.get()); private static ItemStack createRequeueItem(int data) { ItemStack item = new ItemStack(Material.INK_SACK, 1, (short) data); diff --git a/src/main/java/rip/bolt/ingame/ranked/SpectatorManager.java b/src/main/java/rip/bolt/ingame/ranked/SpectatorManager.java index 95178ba..e972faa 100644 --- a/src/main/java/rip/bolt/ingame/ranked/SpectatorManager.java +++ b/src/main/java/rip/bolt/ingame/ranked/SpectatorManager.java @@ -11,7 +11,9 @@ import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.permissions.PermissionAttachment; import rip.bolt.ingame.Ingame; +import rip.bolt.ingame.api.definitions.MatchStatus; import rip.bolt.ingame.events.BoltMatchStatusChangeEvent; +import rip.bolt.ingame.ranked.forfeit.PlayerWatcher; import tc.oc.pgm.api.PGM; import tc.oc.pgm.api.integration.Integration; import tc.oc.pgm.api.player.MatchPlayer; @@ -48,9 +50,9 @@ public void onPlayerQuit(PlayerQuitEvent event) { } private void updatePlayer(Player player) { - if (Ingame.get().getRankedManager().getMatch() == null) return; + if (Ingame.get().getMatchManager().getMatch() == null) return; - boolean hidden = Ingame.get().getRankedManager().getMatch().getSeries().getHideObservers(); + boolean hidden = Ingame.get().getMatchManager().getMatch().getSeries().getHideObservers(); boolean playing = watcher.isPlaying(player.getUniqueId()); // Allow people who can vanish to remain in current state diff --git a/src/main/java/rip/bolt/ingame/ranked/StatsManager.java b/src/main/java/rip/bolt/ingame/ranked/StatsManager.java deleted file mode 100644 index 238fde8..0000000 --- a/src/main/java/rip/bolt/ingame/ranked/StatsManager.java +++ /dev/null @@ -1,46 +0,0 @@ -package rip.bolt.ingame.ranked; - -import java.util.Collection; -import org.bukkit.event.Listener; -import rip.bolt.ingame.api.definitions.BoltMatch; -import rip.bolt.ingame.api.definitions.Participation; -import rip.bolt.ingame.api.definitions.Stats; -import rip.bolt.ingame.api.definitions.Team; -import tc.oc.pgm.api.match.Match; -import tc.oc.pgm.stats.PlayerStats; -import tc.oc.pgm.stats.StatsMatchModule; - -public class StatsManager implements Listener { - - private final RankedManager manager; - - public StatsManager(RankedManager manager) { - this.manager = manager; - } - - public void handleMatchUpdate(BoltMatch boltMatch, Match match) { - StatsMatchModule statsModule = match.needModule(StatsMatchModule.class); - - boltMatch.getTeams().stream() - .map(Team::getParticipations) - .flatMap(Collection::stream) - .forEach( - participation -> - populatePlayerStats( - participation, statsModule.getPlayerStat(participation.getUser().getUUID()))); - } - - public void populatePlayerStats(Participation participation, PlayerStats stats) { - participation.setStats( - new Stats( - stats.getKills(), - stats.getDeaths(), - stats.getMaxKillstreak(), - stats.getDamageDone(), - stats.getBowDamage(), - stats.getDamageTaken(), - stats.getBowDamageTaken(), - stats.getShotsHit(), - stats.getShotsTaken())); - } -} diff --git a/src/main/java/rip/bolt/ingame/ranked/CancelManager.java b/src/main/java/rip/bolt/ingame/ranked/forfeit/CancelManager.java similarity index 97% rename from src/main/java/rip/bolt/ingame/ranked/CancelManager.java rename to src/main/java/rip/bolt/ingame/ranked/forfeit/CancelManager.java index 81d69c2..3be8c63 100644 --- a/src/main/java/rip/bolt/ingame/ranked/CancelManager.java +++ b/src/main/java/rip/bolt/ingame/ranked/forfeit/CancelManager.java @@ -1,4 +1,4 @@ -package rip.bolt.ingame.ranked; +package rip.bolt.ingame.ranked.forfeit; import static net.kyori.adventure.text.Component.text; @@ -34,7 +34,7 @@ public CancelManager(PlayerWatcher playerWatcher) { protected void cancelMatch(Match match, List players) { playerWatcher.playersAbandoned(players); - playerWatcher.getRankedManager().cancel(match, CancelReason.AUTOMATED_CANCEL); + playerWatcher.getMatchManager().cancel(match, CancelReason.AUTOMATED_CANCEL); match.sendMessage(Messages.participationBan()); } diff --git a/src/main/java/rip/bolt/ingame/ranked/forfeit/ForfeitManager.java b/src/main/java/rip/bolt/ingame/ranked/forfeit/ForfeitManager.java new file mode 100644 index 0000000..9c5597b --- /dev/null +++ b/src/main/java/rip/bolt/ingame/ranked/forfeit/ForfeitManager.java @@ -0,0 +1,66 @@ +package rip.bolt.ingame.ranked.forfeit; + +import dev.pgm.events.EventsPlugin; +import dev.pgm.events.team.TournamentPlayer; +import dev.pgm.events.team.TournamentTeamManager; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Stream; +import rip.bolt.ingame.config.AppData; +import tc.oc.pgm.api.party.Competitor; +import tc.oc.pgm.api.party.Party; + +public class ForfeitManager { + + private static final Duration FORFEIT_DURATION = AppData.forfeitAfter(); + + private final PlayerWatcher playerWatcher; + + private final Map leaves = new HashMap<>(); + private final Map forfeit = new HashMap<>(); + + public ForfeitManager(PlayerWatcher playerWatcher) { + this.playerWatcher = playerWatcher; + } + + public ForfeitPoll getForfeitPoll(Competitor team) { + return forfeit.computeIfAbsent(team, ForfeitPoll::new); + } + + public boolean mayForfeit(Competitor team) { + if (!AppData.forfeitEnabled()) return false; + if (team.getMatch().getDuration().compareTo(FORFEIT_DURATION) >= 0) return true; + + return getRegisteredPlayers(team) + .map(playerWatcher::getParticipation) + .filter(Objects::nonNull) + .anyMatch(PlayerWatcher.MatchParticipation::hasAbandoned); + } + + public void clearPolls() { + leaves.clear(); + forfeit.clear(); + } + + Stream getRegisteredPlayers(Competitor team) { + TournamentTeamManager teamManager = EventsPlugin.get().getTeamManager(); + return teamManager + .tournamentTeam(team) + .map(t -> t.getPlayers().stream()) + .orElse(Stream.empty()) + .map(TournamentPlayer::getUUID); + } + + public void updateCountdown(Party team) { + if (!AppData.forfeitEnabled() || !(team instanceof Competitor)) return; + + leaves.computeIfAbsent((Competitor) team, this::createAnnouncer).update(); + } + + private LeaveAnnouncer createAnnouncer(Competitor team) { + return new LeaveAnnouncer(playerWatcher, team); + } +} diff --git a/src/main/java/rip/bolt/ingame/ranked/forfeit/ForfeitPoll.java b/src/main/java/rip/bolt/ingame/ranked/forfeit/ForfeitPoll.java new file mode 100644 index 0000000..eae8bde --- /dev/null +++ b/src/main/java/rip/bolt/ingame/ranked/forfeit/ForfeitPoll.java @@ -0,0 +1,61 @@ +package rip.bolt.ingame.ranked.forfeit; + +import static net.kyori.adventure.text.Component.text; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import net.kyori.adventure.text.format.NamedTextColor; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.party.Competitor; +import tc.oc.pgm.api.party.VictoryCondition; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.result.CompetitorVictoryCondition; +import tc.oc.pgm.result.TieVictoryCondition; +import tc.oc.pgm.teams.Team; + +public class ForfeitPoll { + + private final Competitor team; + private final Set voted = new HashSet<>(); + + public ForfeitPoll(Competitor team) { + this.team = team; + } + + public Set getVoted() { + return voted; + } + + public void addVote(MatchPlayer player) { + voted.add(player.getId()); + check(); + } + + public void check() { + if (hasPassed()) endMatch(); + } + + private boolean hasPassed() { + return voted.stream().filter(uuid -> team.getPlayer(uuid) != null).count() + >= Math.min( + team instanceof Team ? ((Team) team).getMaxPlayers() - 1 : 0, team.getPlayers().size()); + } + + public void endMatch() { + Match match = team.getMatch(); + match.sendMessage( + team.getName().append(text(" has voted to forfeit the match.", NamedTextColor.WHITE))); + + // Create victory condition for other team or tie + VictoryCondition victoryCondition = + match.getCompetitors().stream() + .filter(competitor -> !competitor.equals(team)) + .findFirst() + .map(CompetitorVictoryCondition::new) + .orElseGet(TieVictoryCondition::new); + + match.addVictoryCondition(victoryCondition); + match.finish(null); + } +} diff --git a/src/main/java/rip/bolt/ingame/ranked/forfeit/LeaveAnnouncer.java b/src/main/java/rip/bolt/ingame/ranked/forfeit/LeaveAnnouncer.java new file mode 100644 index 0000000..4b430b6 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/ranked/forfeit/LeaveAnnouncer.java @@ -0,0 +1,56 @@ +package rip.bolt.ingame.ranked.forfeit; + +import java.time.Duration; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import rip.bolt.ingame.utils.Messages; +import tc.oc.pgm.api.match.MatchScope; +import tc.oc.pgm.api.party.Competitor; + +public class LeaveAnnouncer { + + private final PlayerWatcher watcher; + private final ForfeitManager forfeitManager; + + private final Competitor team; + private boolean hasCompleted; + + private ScheduledFuture scheduledFuture; + + public LeaveAnnouncer(PlayerWatcher watcher, Competitor team) { + this.watcher = watcher; + this.team = team; + + this.forfeitManager = watcher.getForfeitManager(); + } + + private void broadcast() { + if (this.hasCompleted) return; + this.hasCompleted = true; + team.sendMessage(Messages.forfeit()); + } + + void update() { + if (this.hasCompleted) return; + + if (scheduledFuture != null) { + scheduledFuture.cancel(false); + scheduledFuture = null; + } + + forfeitManager + .getRegisteredPlayers(team) + .map(watcher::getParticipation) + .filter(PlayerWatcher.MatchParticipation::canStartCountdown) + .map(PlayerWatcher.MatchParticipation::absentDuration) + .max(Duration::compareTo) + .map(PlayerWatcher.ABSENT_MAX::minus) + .filter(duration -> !duration.isNegative()) + .ifPresent( + duration -> + scheduledFuture = + team.getMatch() + .getExecutor(MatchScope.RUNNING) + .schedule(this::broadcast, duration.toMillis(), TimeUnit.MILLISECONDS)); + } +} diff --git a/src/main/java/rip/bolt/ingame/ranked/PlayerWatcher.java b/src/main/java/rip/bolt/ingame/ranked/forfeit/PlayerWatcher.java similarity index 93% rename from src/main/java/rip/bolt/ingame/ranked/PlayerWatcher.java rename to src/main/java/rip/bolt/ingame/ranked/forfeit/PlayerWatcher.java index c10c856..efba12f 100644 --- a/src/main/java/rip/bolt/ingame/ranked/PlayerWatcher.java +++ b/src/main/java/rip/bolt/ingame/ranked/forfeit/PlayerWatcher.java @@ -1,4 +1,4 @@ -package rip.bolt.ingame.ranked; +package rip.bolt.ingame.ranked.forfeit; import java.time.Duration; import java.util.HashMap; @@ -14,6 +14,7 @@ import rip.bolt.ingame.Ingame; import rip.bolt.ingame.api.definitions.Punishment; import rip.bolt.ingame.config.AppData; +import rip.bolt.ingame.managers.MatchManager; import rip.bolt.ingame.utils.CancelReason; import rip.bolt.ingame.utils.Messages; import tc.oc.pgm.api.match.Match; @@ -28,20 +29,20 @@ public class PlayerWatcher implements Listener { public static final Duration ABSENT_MAX = Duration.ofSeconds(AppData.absentSecondsLimit()); - private final RankedManager rankedManager; + private final MatchManager matchManager; private final ForfeitManager forfeitManager; private final CancelManager cancelManager; private final Map players = new HashMap<>(); - public PlayerWatcher(RankedManager rankedManager) { - this.rankedManager = rankedManager; + public PlayerWatcher(MatchManager matchManager) { + this.matchManager = matchManager; this.forfeitManager = new ForfeitManager(this); this.cancelManager = new CancelManager(this); } - public RankedManager getRankedManager() { - return rankedManager; + public MatchManager getMatchManager() { + return matchManager; } public ForfeitManager getForfeitManager() { @@ -108,7 +109,7 @@ public void onLeave(PlayerPartyChangeEvent event) { @EventHandler(priority = EventPriority.LOW) public void onMatchEnd(MatchFinishEvent event) { // If match was cancelled, don't bother with the rest - if (rankedManager.getCancelReason() != null) return; + if (matchManager.getCancelReason() != null) return; // Duration less than max absent period no bans to check if (event.getMatch().getDuration().compareTo(ABSENT_MAX) > 0) return; @@ -134,7 +135,7 @@ public void onMatchStart(MatchStartEvent event) { // If a player never joined mark as abandoned if (playersAbandoned(getNonJoinedPlayers())) { - rankedManager.cancel(event.getMatch(), CancelReason.AUTOMATED_CANCEL); + matchManager.cancel(event.getMatch(), CancelReason.AUTOMATED_CANCEL); event.getMatch().sendMessage(Messages.matchStartCancelled()); return; } @@ -169,7 +170,7 @@ private List getParticipationsBelowDuration(Duration minimumDuration) { public boolean playersAbandoned(List players) { if (players.size() <= 5) { - Integer seriesId = Ingame.get().getRankedManager().getMatch().getSeries().getId(); + Integer seriesId = matchManager.getMatch().getSeries().getId(); players.forEach(player -> playerAbandoned(player, seriesId)); } diff --git a/src/main/java/rip/bolt/ingame/ranked/MatchPreloader.java b/src/main/java/rip/bolt/ingame/setup/MatchPreloader.java similarity index 98% rename from src/main/java/rip/bolt/ingame/ranked/MatchPreloader.java rename to src/main/java/rip/bolt/ingame/setup/MatchPreloader.java index 22af22e..3ee48f6 100644 --- a/src/main/java/rip/bolt/ingame/ranked/MatchPreloader.java +++ b/src/main/java/rip/bolt/ingame/setup/MatchPreloader.java @@ -1,4 +1,4 @@ -package rip.bolt.ingame.ranked; +package rip.bolt.ingame.setup; import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.ReentrantLock; diff --git a/src/main/java/rip/bolt/ingame/ranked/MatchSearch.java b/src/main/java/rip/bolt/ingame/setup/MatchSearch.java similarity index 81% rename from src/main/java/rip/bolt/ingame/ranked/MatchSearch.java rename to src/main/java/rip/bolt/ingame/setup/MatchSearch.java index a9c6f54..28d30b5 100644 --- a/src/main/java/rip/bolt/ingame/ranked/MatchSearch.java +++ b/src/main/java/rip/bolt/ingame/setup/MatchSearch.java @@ -1,4 +1,4 @@ -package rip.bolt.ingame.ranked; +package rip.bolt.ingame.setup; import java.time.Duration; import java.util.function.Consumer; @@ -42,7 +42,15 @@ public void startIn(Duration delay) { startIn(delay.getSeconds() * 20); } - public void startIn(long delay) { + public synchronized void startIn(long delay) { + + System.out.println("[Ingame] Request poll for " + delay); + + // Don't poll if already polling + if (this.isSyncTaskRunning()) return; + + System.out.println("[Ingame] Starting poll for " + delay); + stop(); syncTaskId = diff --git a/src/main/java/rip/bolt/ingame/utils/AudienceProvider.java b/src/main/java/rip/bolt/ingame/utils/AudienceProvider.java new file mode 100644 index 0000000..b2e7d9d --- /dev/null +++ b/src/main/java/rip/bolt/ingame/utils/AudienceProvider.java @@ -0,0 +1,21 @@ +package rip.bolt.ingame.utils; + +import java.lang.annotation.Annotation; +import java.util.List; +import org.bukkit.command.CommandSender; +import tc.oc.pgm.lib.app.ashcon.intake.argument.CommandArgs; +import tc.oc.pgm.lib.app.ashcon.intake.bukkit.parametric.provider.BukkitProvider; +import tc.oc.pgm.util.Audience; + +public final class AudienceProvider implements BukkitProvider { + + @Override + public boolean isProvided() { + return true; + } + + @Override + public Audience get(CommandSender sender, CommandArgs args, List list) { + return Audience.get(sender); + } +} diff --git a/src/main/java/rip/bolt/ingame/utils/CancelReason.java b/src/main/java/rip/bolt/ingame/utils/CancelReason.java index 0199e82..4b22adc 100644 --- a/src/main/java/rip/bolt/ingame/utils/CancelReason.java +++ b/src/main/java/rip/bolt/ingame/utils/CancelReason.java @@ -1,6 +1,7 @@ package rip.bolt.ingame.utils; public enum CancelReason { - MANUAL_CANCEL, - AUTOMATED_CANCEL, + MANUAL_CANCEL, // An admin manually used /ingame cancel + AUTOMATED_CANCEL, // Players left, the match automatically cancels + SYNC_STATE // Server said it's over, just finish it } diff --git a/src/main/java/rip/bolt/ingame/utils/CommandsUtil.java b/src/main/java/rip/bolt/ingame/utils/CommandsUtil.java new file mode 100644 index 0000000..d68b628 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/utils/CommandsUtil.java @@ -0,0 +1,14 @@ +package rip.bolt.ingame.utils; + +import org.bukkit.command.CommandSender; +import rip.bolt.ingame.managers.GameManager; +import rip.bolt.ingame.pugs.PugManager; +import tc.oc.pgm.lib.app.ashcon.intake.util.auth.AuthorizationException; + +public class CommandsUtil { + public static void checkPermissionsRanked( + CommandSender sender, String node, GameManager gameManager) throws AuthorizationException { + if (!(gameManager instanceof PugManager) && !sender.hasPermission(node)) + throw new AuthorizationException(); + } +} diff --git a/src/main/java/rip/bolt/ingame/utils/MapInfoParser.java b/src/main/java/rip/bolt/ingame/utils/MapInfoParser.java new file mode 100644 index 0000000..89192c6 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/utils/MapInfoParser.java @@ -0,0 +1,72 @@ +package rip.bolt.ingame.utils; + +import static tc.oc.pgm.util.text.TextException.exception; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.List; +import org.bukkit.command.CommandSender; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.map.MapInfo; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.lib.app.ashcon.intake.argument.ArgumentException; +import tc.oc.pgm.lib.app.ashcon.intake.argument.CommandArgs; +import tc.oc.pgm.lib.app.ashcon.intake.argument.MissingArgumentException; +import tc.oc.pgm.lib.app.ashcon.intake.bukkit.parametric.provider.BukkitProvider; +import tc.oc.pgm.lib.app.ashcon.intake.parametric.annotation.Default; + +public final class MapInfoParser implements BukkitProvider { + + @Override + public String getName() { + return "map"; + } + + @Override + public MapInfo get(CommandSender sender, CommandArgs args, List annotations) + throws ArgumentException { + final PGM pgm = PGM.get(); + + MapInfo map = null; + if (args.hasNext()) { + map = pgm.getMapLibrary().getMap(getRemainingText(args)); + } else if (isNextMap(annotations)) { + map = pgm.getMapOrder().getNextMap(); + } else { + final Match match = pgm.getMatchManager().getMatch(sender); + if (match != null) { + map = match.getMap(); + } + } + + if (map == null && !isNextMap(annotations)) { + throw exception("map.notFound"); + } + + return map; + } + + private String getRemainingText(CommandArgs args) throws MissingArgumentException { + StringBuilder mapName = new StringBuilder(); + boolean first = true; + + while (args.hasNext()) { + if (!first) { + mapName.append(" "); + } + + mapName.append(args.next()); + first = false; + } + + return mapName.toString(); + } + + private boolean isNextMap(List annotations) { + return annotations.stream() + .filter(o -> o instanceof Default) + .findFirst() + .filter(value -> Arrays.asList(((Default) value).value()).contains("next")) + .isPresent(); + } +} diff --git a/src/main/java/rip/bolt/ingame/utils/PartyProvider.java b/src/main/java/rip/bolt/ingame/utils/PartyProvider.java new file mode 100644 index 0000000..f51e254 --- /dev/null +++ b/src/main/java/rip/bolt/ingame/utils/PartyProvider.java @@ -0,0 +1,47 @@ +package rip.bolt.ingame.utils; + +import java.lang.annotation.Annotation; +import java.util.List; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.party.Party; +import tc.oc.pgm.lib.app.ashcon.intake.argument.CommandArgs; +import tc.oc.pgm.lib.app.ashcon.intake.argument.MissingArgumentException; +import tc.oc.pgm.lib.app.ashcon.intake.bukkit.parametric.provider.BukkitProvider; +import tc.oc.pgm.lib.app.ashcon.intake.parametric.ProvisionException; +import tc.oc.pgm.teams.Team; +import tc.oc.pgm.teams.TeamMatchModule; +import tc.oc.pgm.util.text.TextException; + +public final class PartyProvider implements BukkitProvider { + public PartyProvider() {} + + public String getName() { + return "team"; + } + + public Party get(CommandSender sender, CommandArgs args, List list) + throws MissingArgumentException, ProvisionException { + String text = args.next(); + Match match = PGM.get().getMatchManager().getMatch(sender); + if (match == null) { + throw TextException.exception("command.onlyPlayers", new Component[0]); + } else if (text.startsWith("obs")) { + return match.getDefaultParty(); + } else { + TeamMatchModule teams = match.getModule(TeamMatchModule.class); + if (teams == null) { + throw TextException.exception("command.noTeams", new Component[0]); + } else { + Team team = teams.bestFuzzyMatch(text); + if (team == null) { + throw TextException.invalidFormat(text, Team.class, null); + } else { + return team; + } + } + } + } +} diff --git a/src/main/java/rip/bolt/ingame/utils/RankedTeamTabEntry.java b/src/main/java/rip/bolt/ingame/utils/RankedTeamTabEntry.java index e0e023e..6a3ebad 100644 --- a/src/main/java/rip/bolt/ingame/utils/RankedTeamTabEntry.java +++ b/src/main/java/rip/bolt/ingame/utils/RankedTeamTabEntry.java @@ -1,10 +1,11 @@ package rip.bolt.ingame.utils; -import dev.pgm.events.Tournament; +import dev.pgm.events.EventsPlugin; import dev.pgm.events.team.TournamentTeam; import java.util.Optional; import javax.annotation.Nullable; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import rip.bolt.ingame.api.definitions.Team; @@ -14,7 +15,7 @@ public class RankedTeamTabEntry extends TeamTabEntry { @Nullable private Component mmrComponent; - private tc.oc.pgm.teams.Team team; + private final tc.oc.pgm.teams.Team team; public RankedTeamTabEntry(tc.oc.pgm.teams.Team team) { super(team); @@ -34,12 +35,18 @@ public Component getContent(TabView view) { private Component getMmrComponent() { Optional tournamentTeam = - Tournament.get().getTeamManager().tournamentTeam(team); + EventsPlugin.get().getTeamManager().tournamentTeam(team); return tournamentTeam .filter(t -> t instanceof Team) .map(t -> (Team) t) - .map(t -> Component.text(" " + t.getMmr(), NamedTextColor.GRAY, TextDecoration.ITALIC)) + .map(Team::getMmr) + .filter(mmr -> !mmr.isEmpty() && !mmr.equals("0")) + .map(this::getMmrComponent) .orElse(null); } + + private TextComponent getMmrComponent(String mmr) { + return Component.text(" " + mmr, NamedTextColor.GRAY, TextDecoration.ITALIC); + } } diff --git a/src/main/java/rip/bolt/ingame/utils/TeamsProvider.java b/src/main/java/rip/bolt/ingame/utils/TeamsProvider.java new file mode 100644 index 0000000..13c01fd --- /dev/null +++ b/src/main/java/rip/bolt/ingame/utils/TeamsProvider.java @@ -0,0 +1,34 @@ +package rip.bolt.ingame.utils; + +import static tc.oc.pgm.util.text.TextException.exception; + +import java.lang.annotation.Annotation; +import java.util.List; +import org.bukkit.command.CommandSender; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.lib.app.ashcon.intake.argument.CommandArgs; +import tc.oc.pgm.lib.app.ashcon.intake.bukkit.parametric.provider.BukkitProvider; +import tc.oc.pgm.teams.TeamMatchModule; + +public final class TeamsProvider implements BukkitProvider { + + @Override + public boolean isProvided() { + return true; + } + + @Override + public TeamMatchModule get( + CommandSender sender, CommandArgs commandArgs, List list) { + final Match match = PGM.get().getMatchManager().getMatch(sender); + if (match != null) { + final TeamMatchModule teams = match.getModule(TeamMatchModule.class); + if (teams != null) { + return teams; + } + } + + throw exception("command.noTeams"); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3ed83f7..355ba10 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -30,6 +30,9 @@ match-start-duration: "300s" # if custom tab list should be used custom-tab-enabled: true +# if PUGs should be able to publicly show logs +publicly-log-pugs: false + # website page links (remove section to remove references) web: match: https://localhost:3000/matches/{matchId} @@ -40,3 +43,6 @@ api: url: https://localhost:3000/v1/ key: authorisation-key +# web socket connection details +socket: + url: "ws://localhost:3000"