diff --git a/core/src/main/java/dev/pgm/community/CommunityPermissions.java b/core/src/main/java/dev/pgm/community/CommunityPermissions.java index 30b77bf..13bc1f7 100644 --- a/core/src/main/java/dev/pgm/community/CommunityPermissions.java +++ b/core/src/main/java/dev/pgm/community/CommunityPermissions.java @@ -29,6 +29,8 @@ public interface CommunityPermissions { // Sign Logger String SIGN_LOG_BROADCASTS = ROOT + ".view-sign-logs"; // Access to view when signs are placed + String BLOCK_GLITCH_BROADCASTS = + ROOT + ".view-block-glitch"; // Access to view blockglitch alerts and replays // Reports String REPORTS = ROOT + ".reports"; // Access to view report broadcast & report history diff --git a/core/src/main/java/dev/pgm/community/commands/graph/CommunityCommandGraph.java b/core/src/main/java/dev/pgm/community/commands/graph/CommunityCommandGraph.java index 8c17c93..14d589f 100644 --- a/core/src/main/java/dev/pgm/community/commands/graph/CommunityCommandGraph.java +++ b/core/src/main/java/dev/pgm/community/commands/graph/CommunityCommandGraph.java @@ -22,6 +22,7 @@ import dev.pgm.community.history.MatchHistoryCommand; import dev.pgm.community.mobs.MobCommand; import dev.pgm.community.moderation.commands.BanCommand; +import dev.pgm.community.moderation.commands.BlockGlitchCommand; import dev.pgm.community.moderation.commands.KickCommand; import dev.pgm.community.moderation.commands.MuteCommand; import dev.pgm.community.moderation.commands.PunishmentCommand; @@ -137,6 +138,7 @@ protected void registerCommands() { register(new PunishmentCommand()); register(new ToolCommand()); register(new WarnCommand()); + register(new BlockGlitchCommand()); // Mutations register(new MutationCommands()); diff --git a/core/src/main/java/dev/pgm/community/moderation/ModerationConfig.java b/core/src/main/java/dev/pgm/community/moderation/ModerationConfig.java index df38fcb..d544edb 100644 --- a/core/src/main/java/dev/pgm/community/moderation/ModerationConfig.java +++ b/core/src/main/java/dev/pgm/community/moderation/ModerationConfig.java @@ -43,6 +43,7 @@ public class ModerationConfig extends FeatureConfigImpl { private static final String LOOKUP_SIGN_KEY = TOOLS_KEY + ".lookup-sign"; private static final String SIGN_LOGGER_KEY = KEY + ".sign-logger"; + private static final String BLOCK_GLITCH_LOGGER_KEY = KEY + ".block-glitch-logger"; // General options private boolean persist; @@ -92,6 +93,8 @@ public class ModerationConfig extends FeatureConfigImpl { // Sign Logger private boolean signLoggerEnabled; + // BlockGlitch Logger + private boolean blockGlitchLoggerEnabled; /** * Config options related to {@link ModerationFeature} @@ -260,6 +263,10 @@ public boolean isSignLoggerEnabled() { return signLoggerEnabled; } + public boolean isBlockGlitchLoggerEnabled() { + return blockGlitchLoggerEnabled; + } + @Override public void reload(Configuration config) { super.reload(config); @@ -311,5 +318,7 @@ public void reload(Configuration config) { // Sign Logger this.signLoggerEnabled = config.getBoolean(getEnabledKey(SIGN_LOGGER_KEY)); + // BlockGlitch logger + this.blockGlitchLoggerEnabled = config.getBoolean(getEnabledKey(BLOCK_GLITCH_LOGGER_KEY)); } } diff --git a/core/src/main/java/dev/pgm/community/moderation/commands/BlockGlitchCommand.java b/core/src/main/java/dev/pgm/community/moderation/commands/BlockGlitchCommand.java new file mode 100644 index 0000000..c4a229d --- /dev/null +++ b/core/src/main/java/dev/pgm/community/moderation/commands/BlockGlitchCommand.java @@ -0,0 +1,58 @@ +package dev.pgm.community.moderation.commands; + +import static net.kyori.adventure.text.Component.text; +import static tc.oc.pgm.util.text.TextException.exception; + +import dev.pgm.community.Community; +import dev.pgm.community.CommunityPermissions; +import dev.pgm.community.moderation.feature.loggers.BlockGlitchLogger; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.lib.org.incendo.cloud.annotations.Argument; +import tc.oc.pgm.lib.org.incendo.cloud.annotations.Command; +import tc.oc.pgm.lib.org.incendo.cloud.annotations.Permission; + +public class BlockGlitchCommand { + private static final int TP_DISTANCE = 15; + private static final int MAX_NO_OBS_DISTANCE = 40; + + private final BlockGlitchLogger blockGlitch; + + public BlockGlitchCommand() { + this.blockGlitch = Community.get().getFeatures().getModeration().getBlockGlitchLogger(); + } + + @Command("blockglitch list") + @Permission(CommunityPermissions.BLOCK_GLITCH_BROADCASTS) + public void listIncidents(MatchPlayer player) { + var incidents = blockGlitch.getIncidents(); + if (incidents.isEmpty()) throw exception("No recorded blockglitch incidents"); + + for (var incident : incidents) { + player.sendMessage(incident.getDescription()); + } + } + + @Command("blockglitch replay ") + @Permission(CommunityPermissions.BLOCK_GLITCH_BROADCASTS) + public void replayIncident(MatchPlayer player, @Argument("id") int id) { + var incident = blockGlitch.getIncident(id); + if (incident == null) throw exception("Sorry, the block glitch happened too long ago to view"); + Location curr = player.getLocation(); + int distance = + (int) Math.min(curr.distance(incident.getStart()), curr.distance(incident.getEnd())); + + if (player.isObserving()) { + if (distance > TP_DISTANCE) player.getBukkit().teleport(incident.getStart()); + } else if (distance > MAX_NO_OBS_DISTANCE) { + throw exception("Join observers or get closer to watch this replay"); + } + + player.sendMessage(text("Replaying ", NamedTextColor.GRAY) + .append(incident.getPlayerName()) + .append(text(" blockglitching ")) + .append(incident.getWhen().color(NamedTextColor.YELLOW))); + incident.play(player.getBukkit()); + } +} diff --git a/core/src/main/java/dev/pgm/community/moderation/feature/ModerationFeature.java b/core/src/main/java/dev/pgm/community/moderation/feature/ModerationFeature.java index cf64df7..2472044 100644 --- a/core/src/main/java/dev/pgm/community/moderation/feature/ModerationFeature.java +++ b/core/src/main/java/dev/pgm/community/moderation/feature/ModerationFeature.java @@ -1,6 +1,7 @@ package dev.pgm.community.moderation.feature; import dev.pgm.community.feature.Feature; +import dev.pgm.community.moderation.feature.loggers.BlockGlitchLogger; import dev.pgm.community.moderation.punishments.NetworkPunishment; import dev.pgm.community.moderation.punishments.Punishment; import dev.pgm.community.moderation.punishments.PunishmentType; @@ -145,4 +146,6 @@ Punishment punish( String getStaffFormat(); ModerationTools getTools(); + + BlockGlitchLogger getBlockGlitchLogger(); } diff --git a/core/src/main/java/dev/pgm/community/moderation/feature/ModerationFeatureBase.java b/core/src/main/java/dev/pgm/community/moderation/feature/ModerationFeatureBase.java index 37dd383..7cb8d72 100644 --- a/core/src/main/java/dev/pgm/community/moderation/feature/ModerationFeatureBase.java +++ b/core/src/main/java/dev/pgm/community/moderation/feature/ModerationFeatureBase.java @@ -11,6 +11,8 @@ import dev.pgm.community.events.PlayerPunishmentEvent; import dev.pgm.community.feature.FeatureBase; import dev.pgm.community.moderation.ModerationConfig; +import dev.pgm.community.moderation.feature.loggers.BlockGlitchLogger; +import dev.pgm.community.moderation.feature.loggers.SignLogger; import dev.pgm.community.moderation.punishments.NetworkPunishment; import dev.pgm.community.moderation.punishments.Punishment; import dev.pgm.community.moderation.punishments.PunishmentFormats; @@ -28,8 +30,10 @@ import dev.pgm.community.utils.Sounds; import java.time.Duration; import java.time.Instant; +import java.util.Arrays; import java.util.List; import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -37,12 +41,9 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.event.ClickEvent; -import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import org.bukkit.Bukkit; -import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -64,9 +65,12 @@ public abstract class ModerationFeatureBase extends FeatureBase implements Moder private final Cache> banEvasionCache; private final Cache observerBanCache; private final Cache pardonedPlayers; - private Cache matchBan; + private final Cache matchBan; private PGMPunishmentIntegration integration; + private SignLogger signLogger; + private BlockGlitchLogger blockGlitchLogger; + private boolean color = false; public ModerationFeatureBase( @@ -85,20 +89,23 @@ public ModerationFeatureBase( .build(); this.observerBanCache = CacheBuilder.newBuilder().build(); this.pardonedPlayers = CacheBuilder.newBuilder().build(); - - if (config.getMatchBanDuration() != null) { - this.matchBan = CacheBuilder.newBuilder() - .expireAfterWrite(config.getMatchBanDuration().getSeconds(), TimeUnit.SECONDS) - .build(); - } + this.matchBan = config.getMatchBanDuration() == null + ? null + : CacheBuilder.newBuilder() + .expireAfterWrite(config.getMatchBanDuration().getSeconds(), TimeUnit.SECONDS) + .build(); if (config.isEnabled()) { enable(); + if (config.isSignLoggerEnabled()) this.signLogger = new SignLogger(); // Set PGM punishment integration if (PGMUtils.isPGMEnabled()) { this.integration = new PGMPunishmentIntegration(this); this.integration.enable(); + + // BG uses pgm dependencies, only enable if pgm is loaded + if (config.isBlockGlitchLoggerEnabled()) this.blockGlitchLogger = new BlockGlitchLogger(); } Community.get() @@ -154,6 +161,11 @@ public ModerationTools getTools() { return integration != null ? integration.getTools() : null; } + @Override + public BlockGlitchLogger getBlockGlitchLogger() { + return blockGlitchLogger; + } + @Override public Optional getLastPunishment(UUID issuer) { return recents.stream() @@ -178,7 +190,7 @@ public void sendUpdate(NetworkPunishment punishment) { @Override public void recieveUpdate(NetworkPunishment punishment) { recieveRefresh(punishment.getPunishment().getTargetId()); - broadcastPunishment(punishment.getPunishment(), true, punishment.getServer(), null); + broadcastPunishment(punishment.getPunishment(), true, punishment.getServer()); // Extra step due to gson limitation (maybe look into type tokens) Punishment typedPunishment = Punishment.of(punishment.getPunishment()); Community.get() @@ -244,7 +256,7 @@ public void onPunishmentEvent(PlayerPunishmentEvent event) { .thenAcceptAsync(ips -> banEvasionCache.put(punishment.getTargetId(), ips)); break; case MUTE: // Cache mute for easy lookup for sign/chat events - addMute(punishment.getTargetId(), MutePunishment.class.cast(punishment)); + addMute(punishment.getTargetId(), (MutePunishment) punishment); break; case KICK: if (matchBan != null) { // Store match ban @@ -255,7 +267,7 @@ public void onPunishmentEvent(PlayerPunishmentEvent event) { break; } - broadcastPunishment(punishment, event.isSilent(), event.getSender().getAudience()); + broadcastPunishment(punishment, event.isSilent()); } @EventHandler(priority = EventPriority.LOWEST) @@ -295,18 +307,11 @@ public void onAsyncPlayerChatEvent(AsyncPlayerChatEvent event) { public void onPlaceSign(SignChangeEvent event) { // Prevent muted players from using signs Optional mute = getCachedMute(event.getPlayer().getUniqueId()); + if (mute.isEmpty()) return; - if (mute.isPresent()) { - for (int i = 0; i < 4; i++) { - event.setLine(i, " "); - } - Audience.get(event.getPlayer()).sendWarning(mute.get().getSignMuteMessage()); - - return; - } - - // Log sign text to file & chat when enabled - logSign(event.getPlayer(), event.getLines(), event.getBlock().getLocation()); + if (Arrays.stream(event.getLines()).allMatch(String::isBlank)) return; + for (int i = 0; i < 4; i++) event.setLine(i, " "); + Audience.get(event.getPlayer()).sendWarning(mute.get().getSignMuteMessage()); } // BANS @@ -342,17 +347,14 @@ public Optional getCachedMute(UUID playerId) { // ETC. @Nullable private UUID getSenderId(CommandSender sender) { - if (!(sender instanceof Player)) return null; - - Player player = (Player) sender; - return player.getUniqueId(); + return sender instanceof Player p ? p.getUniqueId() : null; } private Optional isBanEvasion(String address) { - Optional>> cached = banEvasionCache.asMap().entrySet().stream() + return banEvasionCache.asMap().entrySet().stream() .filter(s -> s.getValue().contains(address)) + .map(Entry::getKey) .findAny(); - return Optional.ofNullable(cached.isPresent() ? cached.get().getKey() : null); } private boolean hasRecentPardon(UUID playerId) { @@ -360,6 +362,8 @@ private boolean hasRecentPardon(UUID playerId) { } private void banHover() { + if (observerBanCache.asMap().isEmpty()) return; + color = !color; NamedTextColor alertColor = color ? NamedTextColor.YELLOW : NamedTextColor.DARK_RED; Component warning = text(" \u26a0 ", alertColor); @@ -370,12 +374,10 @@ private void banHover() { .build(); this.observerBanCache.asMap().keySet().stream() - .filter(id -> Bukkit.getPlayer(id) != null) .map(Bukkit::getPlayer) + .filter(Objects::nonNull) .map(Audience::get) - .forEach(viewer -> { - viewer.sendActionBar(banned); - }); + .forEach(viewer -> viewer.sendActionBar(banned)); } private Audience getStaffAudience() { @@ -392,91 +394,20 @@ private Audience getGlobalAudience() { return Audience.get(normal); } - private void broadcastPunishment( - Punishment punishment, boolean silent, @Nullable Audience audience) { - broadcastPunishment(punishment, silent, null, audience); + private void broadcastPunishment(Punishment punishment, boolean silent) { + broadcastPunishment(punishment, silent, null); } - private void broadcastPunishment( - Punishment punishment, boolean silent, @Nullable String server, @Nullable Audience sender) { - + private void broadcastPunishment(Punishment punishment, boolean silent, @Nullable String server) { boolean global = !silent && getModerationConfig().isPunishmentPublic(punishment); if (global) { PunishmentFormats.formatBroadcast(punishment, server, getGlobalFormat(), users) - .thenAcceptAsync(broadcast -> { - getGlobalAudience().sendMessage(broadcast); - }); + .thenAcceptAsync(broadcast -> getGlobalAudience().sendMessage(broadcast)); } PunishmentFormats.formatBroadcast(punishment, server, getStaffFormat(), users) - .thenAcceptAsync(broadcast -> { - BroadcastUtils.sendAdminChatMessage( - broadcast, CommunityPermissions.PUNISHMENT_BROADCASTS); - }); - } - - private boolean hasText(String string) { - for (int i = 0; i < string.length(); i++) { - if (Character.isLetterOrDigit(string.charAt(i))) return true; - } - return false; - } - - private boolean isAllText(String string) { - for (int i = 0; i < string.length(); i++) { - char ch = string.charAt(i); - if (!(Character.isLetterOrDigit(ch) || ch == ' ')) return false; - } - return true; - } - - private void compactDuplicates(StringBuilder builder, String base) { - if (base.isEmpty()) return; - char last = base.charAt(0); - builder.append(last); - for (int i = 1; i < base.length(); i++) { - char next = base.charAt(i); - if (last == next) continue; - builder.append(next); - last = next; - } - } - - private void logSign(Player player, String[] lines, Location location) { - if (!getModerationConfig().isSignLoggerEnabled()) return; - - StringBuilder compactBuilder = new StringBuilder(), fullBuilder = new StringBuilder(); - for (String line : lines) { - if ((line = line.trim()).isEmpty()) continue; - fullBuilder.append(line).append('\n'); - - if (hasText(line)) compactBuilder.append(line); - else compactDuplicates(compactBuilder, line); - compactBuilder.append(' '); - } - if (!fullBuilder.isEmpty()) { - compactBuilder.deleteCharAt(compactBuilder.length() - 1); - fullBuilder.deleteCharAt(fullBuilder.length() - 1); - } - - String oneLineSign = compactBuilder.toString(); - if (oneLineSign.length() < 4 && isAllText(oneLineSign)) - return; // Don't track signs with barely any text - - String locString = - String.format("%d %d %d", location.getBlockX(), location.getBlockY(), location.getBlockZ()); - - Component alert = text() - .append(player(player, NameStyle.FANCY)) - .append(text(" placed a sign: \"", NamedTextColor.GRAY)) - .append(text(oneLineSign, NamedTextColor.YELLOW) - .hoverEvent(HoverEvent.showText(text(fullBuilder.toString(), NamedTextColor.YELLOW)))) - .append(text("\"", NamedTextColor.GRAY)) - .clickEvent(ClickEvent.runCommand("/tploc " + locString)) - .hoverEvent(HoverEvent.showText(text("Click to teleport to sign", NamedTextColor.GRAY))) - .build(); - - BroadcastUtils.sendAdminChatMessage(alert, CommunityPermissions.SIGN_LOG_BROADCASTS); + .thenAcceptAsync(broadcast -> BroadcastUtils.sendAdminChatMessage( + broadcast, CommunityPermissions.PUNISHMENT_BROADCASTS)); } } diff --git a/core/src/main/java/dev/pgm/community/moderation/feature/loggers/BlockGlitchLogger.java b/core/src/main/java/dev/pgm/community/moderation/feature/loggers/BlockGlitchLogger.java new file mode 100644 index 0000000..0df7cdd --- /dev/null +++ b/core/src/main/java/dev/pgm/community/moderation/feature/loggers/BlockGlitchLogger.java @@ -0,0 +1,302 @@ +package dev.pgm.community.moderation.feature.loggers; + +import static net.kyori.adventure.text.Component.join; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.Component.translatable; +import static net.kyori.adventure.text.JoinConfiguration.newlines; +import static net.kyori.adventure.text.event.ClickEvent.runCommand; +import static net.kyori.adventure.text.event.HoverEvent.showText; +import static net.kyori.adventure.text.format.NamedTextColor.GOLD; +import static tc.oc.pgm.util.nms.Packets.ENTITIES; +import static tc.oc.pgm.util.text.NumberComponent.number; +import static tc.oc.pgm.util.text.TemporalComponent.duration; + +import dev.pgm.community.Community; +import dev.pgm.community.CommunityPermissions; +import dev.pgm.community.utils.BroadcastUtils; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.LeatherArmorMeta; +import org.bukkit.util.BlockVector; +import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.match.event.MatchAfterLoadEvent; +import tc.oc.pgm.api.party.Party; +import tc.oc.pgm.api.player.MatchPlayerState; +import tc.oc.pgm.events.ParticipantBlockTransformEvent; +import tc.oc.pgm.util.TimeUtils; +import tc.oc.pgm.util.block.BlockVectors; +import tc.oc.pgm.util.material.BlockMaterialData; +import tc.oc.pgm.util.material.MaterialData; +import tc.oc.pgm.util.material.Materials; +import tc.oc.pgm.util.nms.packets.FakeEntity; + +public class BlockGlitchLogger implements Listener { + + private final Map activeIncidents = new HashMap<>(); + private final List pastIncidents = new ArrayList<>(); + + public BlockGlitchLogger() { + PGM.get() + .getExecutor() + .scheduleAtFixedRate( + () -> activeIncidents.values().removeIf(incident -> { + if (!incident.isOver()) return false; + if (pastIncidents.size() > 30) pastIncidents.removeFirst(); + pastIncidents.add(incident); + BroadcastUtils.sendAdminChatMessage( + incident.getDescription(), CommunityPermissions.BLOCK_GLITCH_BROADCASTS); + return true; + }), + 0, + 50L, + TimeUnit.MILLISECONDS); + Community.get().registerListener(this); + } + + public List getIncidents() { + return pastIncidents; + } + + public Incident getIncident(int id) { + for (Incident i : pastIncidents) { + if (i.id == id) return i; + } + return null; + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onBlockTransform(ParticipantBlockTransformEvent event) { + if (!(event.getCause() instanceof BlockPlaceEvent bpe)) return; + MatchPlayerState playerState = event.getPlayerState(); + if (!playerState.canInteract()) return; + Player pl = bpe.getPlayer(); + + boolean isBlockglitch = event.isCancelled() + && (playerState.getLocation().getY() - 0.75) > event.getBlock().getY() + && pl != null + && !pl.isOnGround() + && !pl.isFlying(); + + Incident active = activeIncidents.get(event.getPlayerState().getId()); + if (active != null) { + active.placed(BlockVectors.position(event.getBlock()), isBlockglitch); + } else if (isBlockglitch) { + activeIncidents.put( + pl.getUniqueId(), + new Incident( + pl, + playerState.getName(), + playerState.getParty(), + BlockVectors.position(event.getBlock()))); + } + } + + @EventHandler + public void onMatchLoad(MatchAfterLoadEvent event) { + activeIncidents.clear(); + pastIncidents.clear(); + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + activeIncidents.remove(event.getPlayer().getUniqueId()); + } + + public static class Incident { + private static final long CANCEL_AFTER_PLACE = TimeUtils.toTicks(2, TimeUnit.SECONDS); + private static int MAX_ID = 0; + + private final int id = ++MAX_ID; + private final Instant startedAt = Instant.now(); + private final List queue = new ArrayList<>(100); + private final Player player; + private final Component name; + private final Party party; + + private int currTick = 0; + private int lastPlace = 0; + private int glitchBlocks = 1; + private double maxHeight = 0; + + public Incident(Player player, Component name, Party party, BlockVector initial) { + this.player = player; + this.name = name; + this.party = party; + + this.queue.add(new MoveAction(player.getLocation())); + this.queue.add(new PlaceAction(initial)); + } + + public void play(Player viewer) { + new BlockGlitchReplay(viewer, this); + } + + public void placed(BlockVector vector, boolean isGlitching) { + if (isGlitching) { + lastPlace = currTick; + glitchBlocks++; + } + queue.add(new PlaceAction(vector)); + } + + public boolean isOver() { + if (!player.isOnline()) return true; + Location loc = player.getLocation(); + if (!(queue.getLast() instanceof MoveAction move) || !move.to.equals(loc)) { + maxHeight = Math.max(maxHeight, loc.getY()); + queue.add(new MoveAction(loc)); + } + return loc.getY() < -64 + || (currTick++ - lastPlace >= CANCEL_AFTER_PLACE && player.isOnGround()); + } + + public Location getStart() { + return ((MoveAction) queue.getFirst()).to; + } + + public Location getEnd() { + return ((MoveAction) queue.getLast()).to; + } + + public Component getPlayerName() { + return name; + } + + public Component getWhen() { + return translatable( + "misc.timeAgo", duration(Duration.between(startedAt, Instant.now()), GOLD)); + } + + public Component getDescription() { + return text() + .append(getPlayerName()) + .append(text(" blockglitched for ", NamedTextColor.GRAY) + .append(duration(TimeUtils.fromTicks(lastPlace + 10)).color(NamedTextColor.YELLOW)) + .hoverEvent(showText(getStatistics()))) + .append(text(" [View]", NamedTextColor.GREEN, TextDecoration.BOLD) + .clickEvent(runCommand("/blockglitch replay " + id))) + .build(); + } + + public Component getStatistics() { + double from = getStart().getY(); + return join( + newlines(), + text("Started ").append(getWhen()), + text("Placed ").append(text(glitchBlocks, GOLD)).append(text(" glitch blocks")), + text("Climbed from ") + .append(number(from, GOLD)) + .append(text(" to ")) + .append(number(maxHeight, GOLD)) + .append(text(" (")) + .append(number(maxHeight - from, GOLD)) + .append(text(" blocks)"))) + .color(NamedTextColor.YELLOW); + } + + record MoveAction(Location to) implements Action { + @Override + public void play(BlockGlitchReplay replay) { + replay.fakeEntity.teleport(to).send(replay.viewer); + } + } + + record PlaceAction(BlockVector place) implements Action { + private static final BlockMaterialData BLOCK = + MaterialData.block(Materials.parse("STAINED_GLASS", "WHITE_STAINED_GLASS")); + + @Override + public void play(BlockGlitchReplay replay) { + BLOCK.sendBlockChange(replay.viewer, place.toLocation(replay.viewer.getWorld())); + } + + @Override + public void clear(Player viewer) { + Location loc = place.toLocation(viewer.getWorld()); + MaterialData.block(loc.getBlock()).sendBlockChange(viewer, loc); + } + } + } + + private interface Action { + void play(BlockGlitchReplay replay); + + default void clear(Player viewer) {} + } + + static class BlockGlitchReplay implements Runnable { + private final Player viewer; + private final List queue; + private final Future task; + private final FakeEntity fakeEntity; + private int idx = 0; + + private BlockGlitchReplay(Player viewer, BlockGlitchLogger.Incident incident) { + this.viewer = viewer; + this.queue = incident.queue; + + this.fakeEntity = ENTITIES.fakePlayer(incident.player, incident.party.getColor()); + this.fakeEntity.spawn(incident.getStart()).send(viewer); + + var armorColor = incident.party.getFullColor(); + this.fakeEntity + .wear( + dyeLeather(Material.LEATHER_HELMET, armorColor), + dyeLeather(Material.LEATHER_CHESTPLATE, armorColor), + dyeLeather(Material.LEATHER_LEGGINGS, armorColor), + dyeLeather(Material.LEATHER_BOOTS, armorColor)) + .send(viewer); + + // Instant play first tp and first place + queue.get(idx++).play(this); + queue.get(idx++).play(this); + + this.task = + PGM.get().getExecutor().scheduleWithFixedDelay(this, 1000, 50, TimeUnit.MILLISECONDS); + } + + private static ItemStack dyeLeather(Material material, Color color) { + ItemStack item = new ItemStack(material); + LeatherArmorMeta meta = (LeatherArmorMeta) item.getItemMeta(); + meta.setColor(color); + item.setItemMeta(meta); + return item; + } + + @Override + public void run() { + if (!viewer.isOnline()) { + task.cancel(true); + return; + } + + if (idx < queue.size()) queue.get(idx).play(this); + int cleanup = idx++ - 20; + if (cleanup >= 0 && cleanup < queue.size()) queue.get(cleanup).clear(viewer); + else if (cleanup >= queue.size()) { + fakeEntity.destroy().send(viewer); + task.cancel(true); + } + } + } +} diff --git a/core/src/main/java/dev/pgm/community/moderation/feature/loggers/SignLogger.java b/core/src/main/java/dev/pgm/community/moderation/feature/loggers/SignLogger.java new file mode 100644 index 0000000..e4692de --- /dev/null +++ b/core/src/main/java/dev/pgm/community/moderation/feature/loggers/SignLogger.java @@ -0,0 +1,96 @@ +package dev.pgm.community.moderation.feature.loggers; + +import static net.kyori.adventure.text.Component.text; +import static tc.oc.pgm.util.player.PlayerComponent.player; + +import dev.pgm.community.Community; +import dev.pgm.community.CommunityPermissions; +import dev.pgm.community.utils.BroadcastUtils; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.SignChangeEvent; +import tc.oc.pgm.util.named.NameStyle; + +public class SignLogger implements Listener { + + public SignLogger() { + Community.get().registerListener(this); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlaceSign(SignChangeEvent event) { + Player player = event.getPlayer(); + String[] lines = event.getLines(); + Block block = event.getBlock(); + + StringBuilder fullSign = new StringBuilder(); + String oneLineSign = mergeSignLines(lines, fullSign); + // Don't track signs with barely any text + if (oneLineSign.length() < 4 && isAllText(oneLineSign)) return; + + String locString = String.format("%d %d %d", block.getX(), block.getY(), block.getZ()); + + Component alert = text() + .append(player(player, NameStyle.FANCY)) + .append(text(" placed a sign: \"", NamedTextColor.GRAY)) + .append(text(oneLineSign, NamedTextColor.YELLOW) + .hoverEvent(HoverEvent.showText(text(fullSign.toString(), NamedTextColor.YELLOW)))) + .append(text("\"", NamedTextColor.GRAY)) + .clickEvent(ClickEvent.runCommand("/tploc " + locString)) + .hoverEvent(HoverEvent.showText(text("Click to teleport to sign", NamedTextColor.GRAY))) + .build(); + + BroadcastUtils.sendAdminChatMessage(alert, CommunityPermissions.SIGN_LOG_BROADCASTS); + } + + private String mergeSignLines(String[] lines, StringBuilder fullBuilder) { + StringBuilder compactBuilder = new StringBuilder(); + for (String line : lines) { + if ((line = line.trim()).isEmpty()) continue; + fullBuilder.append(line).append('\n'); + + if (hasText(line)) compactBuilder.append(line); + else compactDuplicates(compactBuilder, line); + compactBuilder.append(' '); + } + if (!fullBuilder.isEmpty()) { + compactBuilder.deleteCharAt(compactBuilder.length() - 1); + fullBuilder.deleteCharAt(fullBuilder.length() - 1); + } + return compactBuilder.toString(); + } + + private boolean hasText(String string) { + for (int i = 0; i < string.length(); i++) { + if (Character.isLetterOrDigit(string.charAt(i))) return true; + } + return false; + } + + private boolean isAllText(String string) { + for (int i = 0; i < string.length(); i++) { + char ch = string.charAt(i); + if (!(Character.isLetterOrDigit(ch) || ch == ' ')) return false; + } + return true; + } + + private void compactDuplicates(StringBuilder builder, String base) { + if (base.isEmpty()) return; + char last = base.charAt(0); + builder.append(last); + for (int i = 1; i < base.length(); i++) { + char next = base.charAt(i); + if (last == next) continue; + builder.append(next); + last = next; + } + } +} diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 246ef21..13dfbf2 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -81,7 +81,11 @@ moderation: # Sign Logging sign-logger: enabled: true - + + # BlockGlitch Logging + block-glitch-logger: + enabled: true + #Logins login-timeout: 30