356 lines
14 KiB
Java
356 lines
14 KiB
Java
package tc.oc.pgm.api;
|
|
|
|
import java.time.Duration;
|
|
import java.time.Instant;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import javax.annotation.Nullable;
|
|
import javax.inject.Inject;
|
|
|
|
import com.google.common.base.Function;
|
|
import com.google.common.collect.Collections2;
|
|
import com.google.common.collect.HashBasedTable;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.google.common.collect.Table;
|
|
import com.google.common.util.concurrent.ListenableFuture;
|
|
import net.md_5.bungee.api.ChatColor;
|
|
import net.md_5.bungee.api.chat.BaseComponent;
|
|
import net.md_5.bungee.api.chat.TranslatableComponent;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.event.EventHandler;
|
|
import org.bukkit.event.Listener;
|
|
import tc.oc.api.bukkit.users.BukkitUserStore;
|
|
import tc.oc.api.docs.PlayerId;
|
|
import tc.oc.api.docs.virtual.EngagementDoc;
|
|
import tc.oc.api.docs.virtual.EngagementDocBase;
|
|
import tc.oc.api.docs.virtual.MatchDoc;
|
|
import tc.oc.api.engagement.EngagementService;
|
|
import tc.oc.api.message.types.Reply;
|
|
import tc.oc.api.model.IdFactory;
|
|
import tc.oc.commons.bukkit.chat.Links;
|
|
import tc.oc.commons.core.chat.Component;
|
|
import tc.oc.commons.core.formatting.PeriodFormats;
|
|
import tc.oc.commons.core.util.Comparables;
|
|
import tc.oc.commons.core.util.Numbers;
|
|
import tc.oc.commons.core.util.PunchClock;
|
|
import tc.oc.commons.core.util.RankedSet;
|
|
import tc.oc.minecraft.scheduler.SyncExecutor;
|
|
import tc.oc.pgm.Config;
|
|
import tc.oc.pgm.broadcast.Broadcast;
|
|
import tc.oc.pgm.events.ListenerScope;
|
|
import tc.oc.pgm.events.MatchEndEvent;
|
|
import tc.oc.pgm.events.MatchPostCommitEvent;
|
|
import tc.oc.pgm.events.PlayerJoinPartyEvent;
|
|
import tc.oc.pgm.join.JoinConfiguration;
|
|
import tc.oc.pgm.match.Competitor;
|
|
import tc.oc.pgm.match.Match;
|
|
import tc.oc.pgm.match.MatchModule;
|
|
import tc.oc.pgm.match.MatchPlayer;
|
|
import tc.oc.pgm.match.MatchScope;
|
|
import tc.oc.pgm.teams.Team;
|
|
import tc.oc.pgm.victory.VictoryMatchModule;
|
|
|
|
/**
|
|
* Responsible for creating/updating {@link EngagementDoc}s.
|
|
*/
|
|
@ListenerScope(MatchScope.LOADED)
|
|
public class EngagementMatchModule extends MatchModule implements Listener {
|
|
|
|
abstract class Engagement extends EngagementDocBase {
|
|
final PlayerId playerId;
|
|
final Competitor competitor;
|
|
final Instant commitTime;
|
|
|
|
Engagement(PlayerId playerId, Competitor competitor) {
|
|
super(idFactory.newId(), EngagementMatchModule.this.matchDocument);
|
|
this.playerId = playerId;
|
|
this.competitor = competitor;
|
|
this.commitTime = getMatch().getClock().now().instant;
|
|
}
|
|
|
|
public PlayerId getPlayerId() {
|
|
return playerId;
|
|
}
|
|
|
|
@Override
|
|
public String user_id() {
|
|
return playerId._id();
|
|
}
|
|
|
|
@Override
|
|
public Instant match_joined_at() {
|
|
return commitTime;
|
|
}
|
|
|
|
@Override
|
|
public @Nullable Duration match_participation() {
|
|
if(!getMatch().isFinished()) return null;
|
|
return getMatch().getParticipationClock().getCumulativePresence(playerId);
|
|
}
|
|
|
|
@Override
|
|
public boolean committed() {
|
|
return joinConfiguration.commitPlayers();
|
|
}
|
|
|
|
@Override
|
|
public @Nullable Integer rank() {
|
|
if(!getMatch().isFinished()) return null;
|
|
int rank = match.needMatchModule(VictoryMatchModule.class).rankedCompetitors().getPosition(competitor);
|
|
return rank == -1 ? null : rank;
|
|
}
|
|
|
|
@Override
|
|
public @Nullable Integer tied_count() {
|
|
if(!getMatch().isFinished()) return null;
|
|
final RankedSet<Competitor> ranks = match.needMatchModule(VictoryMatchModule.class).rankedCompetitors();
|
|
int rank = ranks.getPosition(competitor);
|
|
return rank == -1 ? null : ranks.getRank(rank).size();
|
|
}
|
|
|
|
boolean isWin() {
|
|
return getMatch().isFinished() && rank() == 0 && tied_count() < competitor_count();
|
|
}
|
|
|
|
boolean isLoss() {
|
|
return getMatch().isFinished() && rank() > 0;
|
|
}
|
|
|
|
boolean isTie() {
|
|
return getMatch().isFinished() && rank() == 0 && tied_count().equals(competitor_count());
|
|
}
|
|
|
|
boolean isForfeit() {
|
|
return forfeit_reason() != null;
|
|
}
|
|
}
|
|
|
|
class TeamEngagement extends Engagement {
|
|
final Team team;
|
|
|
|
TeamEngagement(PlayerId playerId, Team team) {
|
|
super(playerId, team);
|
|
this.team = team;
|
|
}
|
|
|
|
@Override
|
|
public String team_pgm_id() {
|
|
return team.slug();
|
|
}
|
|
|
|
@Override
|
|
public Integer team_size() {
|
|
return team.getPastPlayers().size();
|
|
}
|
|
|
|
@Override
|
|
public Duration team_participation() {
|
|
return team.getParticipationClock().getCumulativePresence(playerId);
|
|
}
|
|
|
|
@Override
|
|
public @Nullable ForfeitReason forfeit_reason() {
|
|
return getForfeitReason(playerId, team.getParticipationClock());
|
|
}
|
|
}
|
|
|
|
class FreeForAllEngagement extends Engagement {
|
|
FreeForAllEngagement(PlayerId playerId) {
|
|
super(playerId, getMatch().getLastCompetitor(playerId));
|
|
}
|
|
|
|
@Override
|
|
public @Nullable ForfeitReason forfeit_reason() {
|
|
return getForfeitReason(playerId, getMatch().getParticipationClock());
|
|
}
|
|
|
|
@Override public @Nullable String team_pgm_id() { return null; }
|
|
@Override public @Nullable Integer team_size() { return null; }
|
|
@Override public @Nullable Duration team_participation() { return null; }
|
|
}
|
|
|
|
private final EngagementService engagementService;
|
|
private final BukkitUserStore userStore;
|
|
private final IdFactory idFactory;
|
|
private final SyncExecutor syncExecutor;
|
|
private final JoinConfiguration joinConfiguration;
|
|
private final MatchDoc matchDocument;
|
|
|
|
private final Table<PlayerId, Competitor, Engagement> engagements = HashBasedTable.create();
|
|
|
|
@Inject EngagementMatchModule(Match match, EngagementService engagementService, BukkitUserStore userStore, IdFactory idFactory, SyncExecutor syncExecutor, JoinConfiguration joinConfiguration, MatchDoc matchDocument) {
|
|
super(match);
|
|
this.engagementService = engagementService;
|
|
this.userStore = userStore;
|
|
this.idFactory = idFactory;
|
|
this.syncExecutor = syncExecutor;
|
|
this.joinConfiguration = joinConfiguration;
|
|
this.matchDocument = matchDocument;
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldLoad() {
|
|
return super.shouldLoad() && Config.Stats.Engagements.enabled();
|
|
}
|
|
|
|
static @Nullable Engagement.ForfeitReason getForfeitReason(PlayerId playerId, PunchClock<PlayerId> clock) {
|
|
if(Duration.ZERO.equals(Config.Stats.Engagements.maxContinuousAbsence()) ||
|
|
Duration.ZERO.equals(Config.Stats.Engagements.maxCumulativeAbsence()) ||
|
|
Config.Stats.Engagements.minParticipationPercent() == 1) {
|
|
|
|
// If any limit is at the extreme then no absence is allowed, which requires a special message
|
|
if(Comparables.greaterThan(clock.getCumulativeAbsence(playerId), Duration.ZERO)) {
|
|
return Engagement.ForfeitReason.ABSENCE;
|
|
}
|
|
|
|
} else {
|
|
// Since we only show one of these at most, they are ordered roughly by importance/relevance.
|
|
// The percentage that you missed is more important than the absolute amount, and if your cumulative
|
|
// absence was too high, then max continuous absence doesn't really matter.
|
|
if(clock.getCumulativePresencePercent(playerId) < Config.Stats.Engagements.minParticipationPercent()) {
|
|
return Engagement.ForfeitReason.PARTICIPATION_PERCENT;
|
|
}
|
|
|
|
if(Comparables.greaterThan(clock.getCumulativeAbsence(playerId), Config.Stats.Engagements.maxCumulativeAbsence())) {
|
|
return Engagement.ForfeitReason.CUMULATIVE_ABSENCE;
|
|
}
|
|
|
|
if(Comparables.greaterThan(clock.getMaxContinuousAbsence(playerId), Config.Stats.Engagements.maxContinuousAbsence())) {
|
|
return Engagement.ForfeitReason.CONTINUOUS_ABSENCE;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get (or create) the engagement for the given player and competitor
|
|
*/
|
|
Engagement getEngagement(PlayerId playerId, Competitor competitor) {
|
|
Engagement engagement = engagements.get(playerId, competitor);
|
|
if(engagement == null) {
|
|
if(competitor instanceof Team) {
|
|
engagement = new TeamEngagement(playerId, (Team) competitor);
|
|
} else {
|
|
engagement = new FreeForAllEngagement(playerId);
|
|
}
|
|
engagements.put(playerId, competitor, engagement);
|
|
}
|
|
return engagement;
|
|
}
|
|
|
|
void createAllEngagements() {
|
|
for(PlayerId playerId : getMatch().getPastParticipants()) {
|
|
for(Competitor competitor : getMatch().getPastCompetitors(playerId)) {
|
|
getEngagement(playerId, competitor);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get an engagement for the given player, using their most recently joined competitor
|
|
*/
|
|
@Nullable Engagement getLastEngagement(MatchPlayer player) {
|
|
Competitor competitor = getMatch().getLastCompetitor(player.getPlayerId());
|
|
if(competitor == null) return null;
|
|
|
|
return getEngagement(player.getPlayerId(), competitor);
|
|
}
|
|
|
|
ListenableFuture<Reply> saveEngagements(final Collection<Engagement> engagements, final boolean feedback) {
|
|
logger.fine("Saving " + engagements.size() + " engagements");
|
|
|
|
if(feedback && getMatch().isFinished()) {
|
|
for(Engagement engagement : engagements) {
|
|
MatchPlayer player = getMatch().getPlayer(engagement.getPlayerId());
|
|
if(player != null) sendPostMatchFeedback(player);
|
|
}
|
|
}
|
|
|
|
return engagementService.updateMulti(engagements);
|
|
}
|
|
|
|
static BaseComponent formatForfeitReason(Engagement.ForfeitReason forfeitReason) {
|
|
switch(forfeitReason) {
|
|
case ABSENCE:
|
|
return new TranslatableComponent("engagement.forfeitReason.missedPart");
|
|
|
|
case PARTICIPATION_PERCENT:
|
|
return new TranslatableComponent("engagement.forfeitReason.participationPercent",
|
|
String.valueOf(Numbers.percentage(Config.Stats.Engagements.minParticipationPercent())));
|
|
|
|
case CUMULATIVE_ABSENCE:
|
|
return new TranslatableComponent("engagement.forfeitReason.cumulativeAbsence",
|
|
PeriodFormats.briefNaturalPrecise(Config.Stats.Engagements.maxCumulativeAbsence()));
|
|
|
|
case CONTINUOUS_ABSENCE:
|
|
return new TranslatableComponent("engagement.forfeitReason.continuousAbsence",
|
|
PeriodFormats.briefNaturalPrecise(Config.Stats.Engagements.maxContinuousAbsence()));
|
|
}
|
|
throw new IllegalStateException();
|
|
}
|
|
|
|
static BaseComponent formatResult(Engagement engagement) {
|
|
if(engagement.isForfeit()) {
|
|
return new Component(new TranslatableComponent("engagement.result.forfeit"), ChatColor.DARK_RED);
|
|
} else if(engagement.isWin()) {
|
|
return new Component(new TranslatableComponent("engagement.result.win"), ChatColor.GREEN);
|
|
} else if(engagement.isLoss()) {
|
|
return new Component(new TranslatableComponent("engagement.result.loss"), ChatColor.RED);
|
|
} else {
|
|
return new Component(new TranslatableComponent("engagement.result.tie"), ChatColor.BLUE);
|
|
}
|
|
}
|
|
|
|
public void sendPreMatchFeedback(MatchPlayer player) {
|
|
player.sendMessage(new TranslatableComponent("command.match.matchInfo.ranking.message"));
|
|
}
|
|
|
|
void sendPostMatchFeedback(MatchPlayer player) {
|
|
Engagement engagement = getLastEngagement(player);
|
|
if(engagement == null) return;
|
|
|
|
player.sendMessage(new TranslatableComponent("engagement.matchRecorded", formatResult(engagement)));
|
|
if(engagement.isForfeit()) {
|
|
player.sendMessage(new Component(formatForfeitReason(engagement.forfeit_reason()), ChatColor.YELLOW));
|
|
}
|
|
|
|
player.sendMessage(Broadcast.Type.TIP.format(new TranslatableComponent("engagement.statsLink", Links.profileLink(player.getPlayerId()))));
|
|
}
|
|
|
|
@EventHandler
|
|
public void onMatchCommit(MatchPostCommitEvent event) {
|
|
// Save all engagements once players have committed
|
|
createAllEngagements();
|
|
|
|
final Collection<Player> players = ImmutableSet.copyOf(Collections2.transform(
|
|
getMatch().getParticipatingPlayers(),
|
|
new Function<MatchPlayer, Player>() {
|
|
@Override public Player apply(MatchPlayer player) {
|
|
return player.getBukkit();
|
|
}
|
|
}
|
|
));
|
|
|
|
syncExecutor.callback(saveEngagements(engagements.values(), false), (result) -> {
|
|
// Refresh all user docs after creating the engagements, to update their match quotas
|
|
userStore.refresh(players);
|
|
});
|
|
}
|
|
|
|
@EventHandler
|
|
public void onJoin(PlayerJoinPartyEvent event) {
|
|
// Save engagement when a competitor is joined mid-match
|
|
if(getMatch().isCommitted() && event.getNewParty() instanceof Competitor) {
|
|
Engagement engagement = getEngagement(event.getPlayer().getPlayerId(), (Competitor) event.getNewParty());
|
|
saveEngagements(Collections.singleton(engagement), false);
|
|
}
|
|
}
|
|
|
|
@EventHandler
|
|
public void onEnd(MatchEndEvent event) {
|
|
// Save all engagements on match end
|
|
saveEngagements(engagements.values(), true);
|
|
}
|
|
}
|