diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/CordLeak.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/CordLeak.java index f6925d3..42932ff 100644 --- a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/CordLeak.java +++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/CordLeak.java @@ -14,13 +14,12 @@ import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +import java.time.Instant; +import java.util.*; public class CordLeak { private static final Path saveFile = Paths.get(Main.instance().getDataFolder().getAbsolutePath(), "cordleak.json"); - private static final Map locations = new HashMap<>(); + private static final Map leaks = new HashMap<>(); static { load(); @@ -36,18 +35,66 @@ public class CordLeak { } } + public record LeakInfo(Location location, Instant leakedAt) {} + + private record LeakInfoDto(LocationDto location, long leakedAt) { + static LeakInfoDto from(LeakInfo info) { + return new LeakInfoDto(LocationDto.from(info.location), info.leakedAt.toEpochMilli()); + } + + LeakInfo toLeakInfo() { + return new LeakInfo(this.location.toLocation(), Instant.ofEpochMilli(this.leakedAt)); + } + } + public static void update() { - IteratorUtil.onlinePlayers(player -> locations.put(player.getUniqueId(), player.getLocation())); + IteratorUtil.onlinePlayers(player -> { + UUID uuid = player.getUniqueId(); + Location loc = player.getLocation(); + + leaks.compute(uuid, (key, existing) -> { + // Falls bereits geleakt, nicht aktualisieren + if (existing != null && !existing.leakedAt.equals(Instant.EPOCH)) { + return existing; + } + return new LeakInfo(loc, existing != null ? existing.leakedAt : Instant.EPOCH); + }); + }); Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), CordLeak::save); } + public static void markAsLeaked(UUID uuid) { + LeakInfo existing = leaks.get(uuid); + if (existing == null) return; + leaks.put(uuid, new LeakInfo(existing.location, Instant.now())); + Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), CordLeak::save); + } + + public static LeakInfo getLastKnownLocation(UUID uuid) { + return leaks.get(uuid); + } + + public static Map getAllLeaked() { + Map result = new HashMap<>(); + Instant cutoff = Instant.now().minusSeconds(3 * 24 * 60 * 60); // 3 Tage in Sekunden + + for (Map.Entry entry : leaks.entrySet()) { + LeakInfo info = entry.getValue(); + if (info.leakedAt.isAfter(cutoff)) { + result.put(entry.getKey(), info); + } + } + + return result; + } + private static void load() { if (!Files.exists(saveFile)) return; try (Reader reader = Files.newBufferedReader(saveFile)) { - Type type = new TypeToken>() {}.getType(); - Map raw = new Gson().fromJson(reader, type); - for (Map.Entry entry : raw.entrySet()) { - locations.put(UUID.fromString(entry.getKey()), entry.getValue().toLocation()); + Type type = new TypeToken>() {}.getType(); + Map raw = new Gson().fromJson(reader, type); + for (Map.Entry entry : raw.entrySet()) { + leaks.put(UUID.fromString(entry.getKey()), entry.getValue().toLeakInfo()); } } catch (IOException e) { Main.logger().warning("Failed to load CordLeak data: " + e.getMessage()); @@ -57,9 +104,9 @@ public class CordLeak { private static void save() { try { Files.createDirectories(saveFile.getParent()); - Map raw = new HashMap<>(); - for (Map.Entry entry : locations.entrySet()) { - raw.put(entry.getKey().toString(), LocationDto.from(entry.getValue())); + Map raw = new HashMap<>(); + for (Map.Entry entry : leaks.entrySet()) { + raw.put(entry.getKey().toString(), LeakInfoDto.from(entry.getValue())); } try (Writer writer = Files.newBufferedWriter(saveFile)) { new Gson().toJson(raw, writer); @@ -68,8 +115,4 @@ public class CordLeak { Main.logger().warning("Failed to save CordLeak data: " + e.getMessage()); } } - - public static Location getLastKnownLocation(UUID uuid) { - return locations.get(uuid); - } } diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/InvClear.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/InvClear.java index 135d3de..26340ea 100644 --- a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/InvClear.java +++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/InvClear.java @@ -23,6 +23,7 @@ public class InvClear { } public static void add(UUID player) { + Main.logger().info(String.format("Cannot clear inv because Player is offline. Remembering %s", player)); players.add(player); Bukkit.getScheduler().runTaskAsynchronously(Main.instance(), InvClear::save); } diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/OnStrikeActionListener.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/OnStrikeActionListener.java index 0fd5a88..97b757b 100644 --- a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/OnStrikeActionListener.java +++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/OnStrikeActionListener.java @@ -1,6 +1,8 @@ package eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.strike; import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.event.EventHandler; import org.bukkit.event.player.PlayerJoinEvent; @@ -8,5 +10,16 @@ public class OnStrikeActionListener extends ApplianceListener { @EventHandler public void onJoin(PlayerJoinEvent event) { this.getAppliance().checkJoin(event.getPlayer()); + + var leaks = CordLeak.getAllLeaked(); + var text = Component.text(); + if(!leaks.isEmpty()) { + text.append(Component.text("Geleakte Koordinaten:", NamedTextColor.AQUA)); + leaks.forEach((uuid, leakInfo) -> { + this.getAppliance().appendCordLeak(text, uuid); + }); + + event.getPlayer().sendMessage(text); + } } } diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/Strike.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/Strike.java index ece4f0b..b33a017 100644 --- a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/Strike.java +++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/strike/Strike.java @@ -19,6 +19,8 @@ import org.bukkit.entity.Player; import org.bukkit.event.Listener; import org.jetbrains.annotations.NotNull; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @@ -32,9 +34,6 @@ public class Strike extends Appliance { ); } - public void leakCoordinates() { - } - record StrikeInfo(List users, int totalWeight) { } @Override @@ -47,8 +46,31 @@ public class Strike extends Appliance { this.processUpdate(data); return HttpServer.nothing; }); + + apiBuilder.get("cords", req -> { + Map raw = CordLeak.getAllLeaked(); + Map dto = new HashMap<>(); + for (Map.Entry entry : raw.entrySet()) { + dto.put(entry.getKey().toString(), LeakInfoDto.from(entry.getKey(), entry.getValue())); + } + return dto; + }); } + record LocationDto(String world, double x, double y, double z, float yaw, float pitch) { + static LocationDto from(Location loc) { + return new LocationDto(loc.getWorld().getName(), loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch()); + } + } + + record LeakInfoDto(String name, LocationDto location, long leakedAt) { + static LeakInfoDto from(UUID uuid, CordLeak.LeakInfo info) { + String name = Bukkit.getOfflinePlayer(uuid).getName(); + return new LeakInfoDto(name, LocationDto.from(info.location()), info.leakedAt().toEpochMilli()); + } + } + + private void processUpdate(StrikeInfo data) { try { VaroTeam select = null; @@ -59,46 +81,33 @@ public class Strike extends Appliance { Objects.requireNonNull(select); VaroTeam team = select; - switch(data.totalWeight) { - case 0 -> team.members.forEach(member -> { - Main.logger().info(String.format("Unbanning player %s because there are now 0 Strikes!", member.player.getName())); + if(data.totalWeight < 3) { + team.members.forEach(member -> { + Main.logger().info(String.format("Unbanning player %s because there are less than 3 Strikes!", member.player.getName())); Bukkit.getBanList(BanListType.PROFILE).pardon(member.player.getPlayerProfile()); }); + } + switch(data.totalWeight) { case 1 -> { Main.logger().info(String.format("Cord leak for Team %s", team.name)); - ComponentBuilder text = Component.text() - .append( - Component.text( - String.format("Das Team %s hat einen Strike erhalten. Daher werden folgende Koordinaten des Teams veröffentlicht:", team.name), - NamedTextColor.RED - ) - ); + + ComponentBuilder text = Component.text(); + IteratorUtil.onlinePlayers(player -> player.sendMessage(Component.text( + String.format("Das Team %s hat einen Strike erhalten. Daher werden folgende Koordinaten des Teams veröffentlicht:", team.name), + NamedTextColor.RED + ))); data.users.forEach(uuid -> { - OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); - Location cords = CordLeak.getLastKnownLocation(uuid); - if(cords == null) return; - - text.appendNewline(); - text.append(Component.text(Objects.requireNonNull(player.getName()), NamedTextColor.DARK_AQUA)); - text.append(Component.text(": ", NamedTextColor.GRAY)); - text.append(Component.text( - String.format( - "(%s) %d, %d, %d", - cords.getWorld().getName(), - cords.getBlockX(), - cords.getBlockY(), - cords.getBlockZ() - ), - NamedTextColor.AQUA) - ); + CordLeak.markAsLeaked(uuid); + this.appendCordLeak(text, uuid); }); - IteratorUtil.onlinePlayers(player -> player.sendMessage(text)); + IteratorUtil.onlinePlayers(p -> p.sendMessage(text)); } case 2 -> team.members.forEach(member -> { + Main.logger().info(String.format("Clearing Inventory of %s", member.player.getName())); Optional player = Optional.ofNullable(Bukkit.getPlayer(member.player.getUniqueId())); player.ifPresentOrElse( p -> p.getInventory().clear(), @@ -110,9 +119,9 @@ public class Strike extends Appliance { team.members.forEach(member -> { Main.logger().info(String.format("Banning player %s because of third Strike!", member.player.getName())); Bukkit.getBanList(BanListType.PROFILE) - .addBan(member.player.getPlayerProfile(), "projektausschluiss", (Date) null, null); + .addBan(member.player.getPlayerProfile(), "projektausschluss", (Date) null, null); }); - team.getOnlinePlayers().forEach(Player::kick); + Bukkit.getScheduler().runTask(Main.instance(), () -> team.getOnlinePlayers().forEach(Player::kick)); } } } catch(Exception e) { @@ -121,6 +130,31 @@ public class Strike extends Appliance { } } + public void appendCordLeak(ComponentBuilder text, UUID playerToLeak) { + OfflinePlayer player = Bukkit.getOfflinePlayer(playerToLeak); + CordLeak.LeakInfo cords = CordLeak.getLastKnownLocation(playerToLeak); + if(cords == null) return; + + String timestamp = DateTimeFormatter.ofPattern("dd.MM HH:mm") + .withZone(ZoneId.of("Europe/Berlin")) + .format(cords.leakedAt()); + + text.appendNewline(); + text.append(Component.text(Objects.requireNonNull(player.getName()), NamedTextColor.DARK_AQUA)); + text.append(Component.text(": ", NamedTextColor.GRAY)); + text.append(Component.text( + String.format( + "(%s) %d, %d, %d - Uhrzeit: %s", + cords.location().getWorld().getName(), + cords.location().getBlockX(), + cords.location().getBlockY(), + cords.location().getBlockZ(), + timestamp + ), + NamedTextColor.AQUA) + ); + } + public void checkJoin(@NotNull Player player) { if(InvClear.contains(player.getUniqueId())) { player.getInventory().clear(); diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/teams/ConnectivityChangeListener.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/teams/ConnectivityChangeListener.java index e733637..22bc300 100644 --- a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/teams/ConnectivityChangeListener.java +++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/teams/ConnectivityChangeListener.java @@ -5,6 +5,7 @@ import eu.mhsl.craftattack.spawn.core.Main; import eu.mhsl.craftattack.spawn.core.api.client.ReqResp; import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo; +import eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.playTimer.PlayTimer; import org.bukkit.event.EventHandler; import org.bukkit.event.player.AsyncPlayerPreLoginEvent; import org.bukkit.event.player.PlayerJoinEvent; @@ -46,19 +47,24 @@ class ConnectivityChangeListener extends ApplianceListener { VaroTeam team = Main.instance().getAppliance(Teams.class).getTeamFromPlayer(event.getPlayer().getUniqueId()); Objects.requireNonNull(team, "Team not found for player " + event.getPlayer().getUniqueId()); - Main.logger().info(String.format("Team %s got a Strike, because they %s left early!", team.name, event.getPlayer().getName())); - VaroReportRepository.StrikeCreationInfo report = new VaroReportRepository.StrikeCreationInfo( - null, - event.getPlayer().getUniqueId(), - "early left", - "player left the server too early", - null, - null, - 1 - ); - ReqResp response = Main.instance().getRepositoryLoader().getRepository(VaroReportRepository.class).createStrike(report); - Main.logger().info(String.format("Autostrike response for Team %s: %s", team.name, response)); + if(Main.instance().getAppliance(PlayTimer.class).isEnabled()) { + Main.logger().info(String.format("Team %s got a Strike, because they %s left early!", team.name, event.getPlayer().getName())); + + VaroReportRepository.StrikeCreationInfo report = new VaroReportRepository.StrikeCreationInfo( + null, + event.getPlayer().getUniqueId(), + "early left", + "player left the server too early", + null, + null, + 1 + ); + ReqResp response = Main.instance().getRepositoryLoader().getRepository(VaroReportRepository.class).createStrike(report); + Main.logger().info(String.format("Autostrike response for Team %s: %s", team.name, response)); + } else { + Main.logger().info("Allow ealry leave because PlayTimer is not active!"); + } this.getAppliance().enforceTeamLeave(event.getPlayer()); }