ProjectAres/PGM/src/main/java/tc/oc/pgm/api/EngagementMatchModule.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);
}
}