package tc.oc.pgm.mapratings; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import javax.inject.Inject; import me.anxuiz.settings.bukkit.PlayerSettings; import org.bukkit.ChatColor; import org.bukkit.DyeColor; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.material.MaterialData; import org.bukkit.material.Wool; import org.bukkit.permissions.Permission; import tc.oc.api.docs.MapRating; import tc.oc.api.docs.UserId; import tc.oc.api.maps.MapRatingsRequest; import tc.oc.api.maps.MapService; import tc.oc.commons.bukkit.event.ObserverKitApplyEvent; import tc.oc.commons.core.commands.CommandFutureCallback; import tc.oc.commons.core.formatting.StringUtils; import tc.oc.commons.core.util.Comparables; import tc.oc.pgm.PGMTranslations; import tc.oc.pgm.events.ListenerScope; import tc.oc.pgm.events.PlayerLeaveMatchEvent; import tc.oc.pgm.blitz.BlitzMatchModule; import tc.oc.pgm.match.Match; import tc.oc.pgm.match.MatchExecutor; import tc.oc.pgm.match.MatchModule; import tc.oc.pgm.match.MatchPlayer; import tc.oc.pgm.match.MatchPlayerExecutor; import tc.oc.pgm.match.MatchScope; import tc.oc.pgm.match.MultiPlayerParty; import tc.oc.pgm.settings.Settings; @ListenerScope(MatchScope.LOADED) public class MapRatingsMatchModule extends MatchModule implements Listener { public static final String RATE_PERM_NAME = "map.rating.rate"; public static final Permission RATE_PERM = new Permission(RATE_PERM_NAME); public static final Permission VIEW_LIVE_PERM = new Permission("map.rating.view.live"); // Player is allowed to rate if the match has ended and they participated at least this much private static final double MIN_PARTICIPATION_PERCENT = 0.75f; // Player is allowed to rate if they participated for this long, regardless of match state private static final Duration MIN_PARTICIPATION_TIME = Duration.ofMinutes(10); // Time between the end of the match and automatically showing the rating dialog // This is synced with the match end title private static final Duration DIALOG_DELAY = Duration.ofSeconds(4); // Inventory slot for the button that opens the rating dialog private static final int OPEN_BUTTON_SLOT = 5; // Magic invisible string used to identify items serving as buttons private static final String BUTTON_PREFIX = ChatColor.COLOR_CHAR + "z"; private static final DyeColor[] BUTTON_COLORS = new DyeColor[] { DyeColor.RED, DyeColor.PURPLE, DyeColor.BLUE, DyeColor.CYAN, DyeColor.LIME }; private static final String[] BUTTON_LABELS = new String[] { "rating.choice.terrible", "rating.choice.bad", "rating.choice.ok", "rating.choice.good", "rating.choice.amazing" }; private static final ChatColor[] BUTTON_LABEL_COLORS = new ChatColor[] { ChatColor.RED, ChatColor.LIGHT_PURPLE, ChatColor.BLUE, ChatColor.AQUA, ChatColor.GREEN }; @Inject private MapRatingsConfiguration config; @Inject private MapService mapService; @Inject private MatchExecutor matchExecutor; @Inject private BlitzMatchModule blitz; private final Map<MatchPlayer, Integer> playerRatings = new HashMap<>(); private final int minimumScore, maximumScore; @Inject MapRatingsMatchModule(Match match) { super(match); this.minimumScore = 1; this.maximumScore = 5; } @Override public boolean shouldLoad() { return config.enabled(); } private static String formatScore(@Nullable Integer score) { return score == null ? "" : BUTTON_LABEL_COLORS[score - 1].toString() + ChatColor.BOLD + score; } public int getMinimumScore() { return minimumScore; } public int getMaximumScore() { return maximumScore; } public boolean isScoreValid(int score) { return score >= minimumScore && score <= maximumScore; } /** * Return a friendly description of why the given player is not allowed to rate maps, * or null if they are allowed. */ public @Nullable String cantRateReason(MatchPlayer player) { if(!player.getBukkit().hasPermission(RATE_PERM)) { return PGMTranslations.t("noPermission", player); } if(Comparables.lessThan(player.getCumulativeParticipationTime(), MIN_PARTICIPATION_TIME) && !(blitz.eliminated(player)) && !(this.getMatch().isFinished() && player.getCumulativeParticipationPercent() > MIN_PARTICIPATION_PERCENT)) { return PGMTranslations.t("rating.lowParticipation", player); } return null; } public @Nullable String cantShowDialogReason(MatchPlayer player) { if(player.isParticipating()) { return PGMTranslations.get().t( ChatColor.RED.toString(), "rating.whilePlaying", player.getBukkit(), ChatColor.GOLD + "/rate " + ChatColor.ITALIC + minimumScore + "..." + maximumScore ); } return this.cantRateReason(player); } public boolean canRate(MatchPlayer player) { return this.cantRateReason(player) == null; } public boolean canShowDialog(MatchPlayer player) { return this.cantShowDialogReason(player) == null; } public boolean checkCanRate(MatchPlayer player) { String reason = this.cantRateReason(player); if(reason == null) return true; player.sendWarning(reason, false); return false; } public boolean checkCanShowDialog(MatchPlayer player) { String reason = this.cantShowDialogReason(player); if(reason == null) return true; player.sendWarning(reason, false); return false; } private ItemStack getOpenButton(MatchPlayer player) { ItemStack stack = new ItemStack(Material.HOPPER); ItemMeta meta = stack.getItemMeta(); meta.addItemFlags(ItemFlag.values()); meta.setDisplayName(BUTTON_PREFIX + ChatColor.BLUE.toString() + ChatColor.BOLD + PGMTranslations.t("rating.rateThisMap", player)); stack.setItemMeta(meta); return stack; } private ItemStack getScoreButton(MatchPlayer player, int i) { Integer score = this.playerRatings.get(player); MaterialData material; if(score != null && score == i + 1) { material = new MaterialData(Material.CARPET, BUTTON_COLORS[i].getWoolData()); } else { material = new Wool(BUTTON_COLORS[i]); } ItemStack stack = material.toItemStack(i + 1); ItemMeta meta = stack.getItemMeta(); meta.addItemFlags(ItemFlag.values()); meta.setDisplayName(BUTTON_PREFIX + BUTTON_LABEL_COLORS[i] + ChatColor.BOLD + PGMTranslations.t(BUTTON_LABELS[i], player)); stack.setItemMeta(meta); return stack; } private void updateScoreButtons(MatchPlayer player, Inventory inv) { for(int i = minimumScore - 1; i < maximumScore; i++) { inv.setItem(2 + i, this.getScoreButton(player, i)); } } public void showDialog(final MatchPlayer player) { if(!checkCanShowDialog(player)) return; this.loadPlayerRating(player, () -> { String title = ChatColor.DARK_BLUE.toString() + ChatColor.BOLD + PGMTranslations.t("rating.rateThisMap", player); Inventory inv = getMatch().getServer().createInventory( player.getBukkit(), 9, StringUtils.truncate(title, 32) ); updateScoreButtons(player, inv); player.getBukkit().openInventory(inv); }); } public void rate(final MatchPlayer player, final int score) { if(!this.checkCanRate(player)) return; final Integer oldScore = this.playerRatings.put(player, score); InventoryView inv = player.getBukkit().getOpenInventory(); if(inv.getTopInventory().getType() == InventoryType.HOPPER) { this.updateScoreButtons(player, inv.getTopInventory()); } if(oldScore != null && score == oldScore) { player.sendWarning(PGMTranslations.t("rating.sameRating", player, score)); return; } player.facet(MatchPlayerExecutor.class).callback(mapService.rate(new MapRating( player.getPlayerId(), getMatch().getMap().getDocument(), score, null )), CommandFutureCallback.onSuccess(player.getBukkit(), result -> { if(result.first.isOnline()) { result.first.getBukkit().closeInventory(); } notifyRating(result.first, score, oldScore); })); } private void notifyRating(MatchPlayer rater, int score, @Nullable Integer oldScore) { rater.sendMessage(PGMTranslations.get().t( ChatColor.WHITE.toString(), oldScore == null ? "command.rate.successful" : "command.rate.update", rater.getBukkit(), formatScore(score), this.getMatch().getMapInfo().getColoredName(), this.getMatch().getMapInfo().getColoredVersion(), formatScore(oldScore) )); if(oldScore == null) { rater.sendMessage(PGMTranslations.get().t( ChatColor.BLUE.toString(), "rating.changeLater", rater.getBukkit(), ChatColor.GOLD + "/rate" )); } for(MatchPlayer viewer : this.getMatch().getPlayers()) { if(viewer != rater && viewer.getBukkit().hasPermission(VIEW_LIVE_PERM)) { String message; if(rater.getParty() instanceof MultiPlayerParty) { viewer.sendMessage(PGMTranslations.get().t( ChatColor.WHITE.toString(), oldScore == null ? "rating.create.notify" : "rating.update.notify", viewer.getBukkit(), rater.getParty().getColoredName(), formatScore(score), formatScore(oldScore) )); } else { viewer.sendMessage(PGMTranslations.get().t( ChatColor.WHITE.toString(), oldScore == null ? "rating.create.notify.ffa" : "rating.update.notify.ffa", viewer.getBukkit(), formatScore(score), formatScore(oldScore) )); } } } } @EventHandler public void onOpenButtonClick(PlayerInteractEvent event) { if(event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) return; MatchPlayer player = this.getMatch().getPlayer(event.getPlayer()); if(player == null) return; ItemStack stack = event.getPlayer().getItemInHand(); if(stack == null) return; if(stack.getType() != Material.HOPPER) return; String name = stack.getItemMeta().getDisplayName(); if(name == null || !name.startsWith(BUTTON_PREFIX)) return; this.showDialog(player); } @EventHandler public void onButtonClick(final InventoryClickEvent event) { ItemStack stack = event.getCurrentItem(); final MatchPlayer player = this.getMatch().getPlayer(event.getWhoClicked()); if(stack == null || player == null) return; if(stack.getType() != Material.WOOL && stack.getType() != Material.CARPET) return; ItemMeta meta = stack.getItemMeta(); if(!meta.hasDisplayName()) return; String name = meta.getDisplayName(); if(!name.startsWith(BUTTON_PREFIX)) return; event.setCancelled(true); final int score = stack.getAmount(); if(!isScoreValid(score)) return; this.getMatch().getScheduler(MatchScope.LOADED).createTask(() -> { Integer oldScore = playerRatings.get(player); if(oldScore == null || oldScore != score) { player.playSound(Sound.UI_BUTTON_CLICK, 1, 2); rate(player, score); } else { player.getBukkit().closeInventory(); } }); } protected void loadPlayerRating(final MatchPlayer player, final @Nullable Runnable callback) { if(this.playerRatings.containsKey(player)) { if(callback != null) callback.run(); return; } matchExecutor.callback( mapService.getRatings(new MapRatingsRequest( getMatch().getMap().getDocument(), Collections.singletonList(player.getPlayerId()) )), (match, result) -> { if(player.isOnline()) { Integer score = result.player_ratings().get(player.getPlayerId()); playerRatings.put(player, score); if(callback != null) callback.run(); } } ); } @Override public void disable() { super.disable(); this.getMatch().getScheduler(MatchScope.LOADED).createDelayedTask(DIALOG_DELAY, () -> { loadAllPlayerRatings(this::showDialogToAll); }); } private void showDialogToAll() { for(MatchPlayer player : this.getMatch().getPlayers()) { if(this.canRate(player) && this.playerRatings.get(player) == null && PlayerSettings.getManager(player.getBukkit()).getValue(Settings.RATINGS, Boolean.class)) { this.showDialog(player); } } } private void loadAllPlayerRatings(final Runnable callback) { List<UserId> userIds = new ArrayList<>(); for(MatchPlayer player : this.getMatch().getPlayers()) { if(this.canRate(player)) { userIds.add(player.getPlayerId()); } } matchExecutor.callback( mapService.getRatings(new MapRatingsRequest( getMatch().getMap().getDocument(), userIds )), (match, ratings) -> { for(MatchPlayer player : getMatch().getPlayers()) { playerRatings.put(player, ratings.player_ratings().get(player.getPlayerId())); } if(callback != null) callback.run(); } ); } @EventHandler public void onPlayerLeave(PlayerLeaveMatchEvent event) { this.playerRatings.remove(event.getPlayer()); } @EventHandler public void giveKit(ObserverKitApplyEvent event) { match.player(event.getPlayer()) .filter(player -> player.isObserving() && canShowDialog(player)) .ifPresent(player -> player.getInventory().setItem(OPEN_BUTTON_SLOT, getOpenButton(player))); } }