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 extends Annotation> 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 extends Annotation> 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 extends Annotation> 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 extends Annotation> 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 extends Annotation> 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"