From 0d1e6070ced825667cd1e6c84c845267362f6a92 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Elias=20M=C3=BCller?= <elias@elias-mueller.com>
Date: Sat, 21 Jun 2025 17:18:47 +0200
Subject: [PATCH] updated playtimer and teamtasks

---
 common/build.gradle                           |  1 +
 .../spawn/core/util/text/Countdown.java       | 10 ++--
 .../appliances/internal/teamTasks/Task.java   |  6 +++
 .../internal/teamTasks/TeamTasks.java         | 15 +++---
 .../internal/teamTasks/TeamTasksCommand.java  |  2 +-
 .../teamTasks/tasks/BukkitTeamTask.java       | 16 ++++++
 .../teamTasks/tasks/CountdownTeamTask.java    | 24 +++++++++
 .../teams/ConnectivityChangeListener.java     | 19 ++++---
 .../appliances/metaGameplay/teams/Teams.java  | 49 +++++++++++++++----
 9 files changed, 111 insertions(+), 31 deletions(-)
 create mode 100644 varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/Task.java
 create mode 100644 varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/tasks/BukkitTeamTask.java
 create mode 100644 varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/tasks/CountdownTeamTask.java

diff --git a/common/build.gradle b/common/build.gradle
index 578064a..f9f7a33 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -4,4 +4,5 @@ dependencies {
     compileOnly 'io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT'
     compileOnly 'org.geysermc.floodgate:api:2.2.2-SNAPSHOT'
     implementation 'org.apache.httpcomponents:httpclient:4.5.14'
+    implementation 'com.sparkjava:spark-core:2.9.4'
 }
diff --git a/core/src/main/java/eu/mhsl/craftattack/spawn/core/util/text/Countdown.java b/core/src/main/java/eu/mhsl/craftattack/spawn/core/util/text/Countdown.java
index bb5c7b0..7ba6540 100644
--- a/core/src/main/java/eu/mhsl/craftattack/spawn/core/util/text/Countdown.java
+++ b/core/src/main/java/eu/mhsl/craftattack/spawn/core/util/text/Countdown.java
@@ -34,12 +34,12 @@ public class Countdown {
         this.onDone = onDone;
 
         this.defaultAnnouncements = count -> {
-            if(this.current > 60 && this.current % 60 == 0) {
-                return new AnnouncementData(this.current / 60, "Minuten");
+            if(count > 60 && count % 60 == 0) {
+                return new AnnouncementData(count / 60, "Minuten");
             }
 
-            if(this.current <= 60 && (this.current <= 10 || this.current % 10 == 0)) {
-                return new AnnouncementData(this.current, "Sekunden");
+            if(count <= 60 && (count <= 10 || count % 10 == 0)) {
+                return new AnnouncementData(count, "Sekunden");
             }
 
             return null;
@@ -87,7 +87,7 @@ public class Countdown {
 
         if(this.isDone()) {
             this.onDone.run();
-            this.cancel();
+            this.cancelIfRunning();
         }
     }
 
diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/Task.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/Task.java
new file mode 100644
index 0000000..2c6bfa8
--- /dev/null
+++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/Task.java
@@ -0,0 +1,6 @@
+package eu.mhsl.craftattack.spawn.varo.appliances.internal.teamTasks;
+
+public interface Task {
+    void stopTask();
+    boolean isRunning();
+}
diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/TeamTasks.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/TeamTasks.java
index 37f66f5..6364b05 100644
--- a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/TeamTasks.java
+++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/TeamTasks.java
@@ -4,7 +4,6 @@ import eu.mhsl.craftattack.spawn.core.Main;
 import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
 import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
 import eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.teams.VaroTeam;
-import org.bukkit.scheduler.BukkitTask;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.HashMap;
@@ -20,25 +19,25 @@ public class TeamTasks extends Appliance {
         TIME_KICK
     }
 
-    private final Map<VaroTeam, Map<Type, BukkitTask>> tasks = new HashMap<>();
+    private final Map<VaroTeam, Map<Type, Task>> tasks = new HashMap<>();
 
-    private Map<Type, BukkitTask> getTeamTasks(VaroTeam team) {
+    private Map<Type, Task> getTeamTasks(VaroTeam team) {
         return this.tasks.computeIfAbsent(team, varoTeam -> new HashMap<>());
     }
 
-    public Map<Type, BukkitTask> getRunningTeamTasks(VaroTeam team) {
+    public Map<Type, Task> getRunningTeamTasks(VaroTeam team) {
         return this.getTeamTasks(team).entrySet().stream()
-            .filter(entry -> !entry.getValue().isCancelled())
+            .filter(entry -> entry.getValue().isRunning())
             .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
     }
 
     public void cancelTeamTasks(VaroTeam team) {
         Main.logger().info(String.format("All TeamTasks for Team %s were cancelled: %s", team.name, this.getRunningTeamTasks(team)));
-        this.getTeamTasks(team).forEach((type, bukkitTask) -> bukkitTask.cancel());
+        this.getTeamTasks(team).forEach((type, bukkitTask) -> bukkitTask.stopTask());
     }
 
-    public void addTask(VaroTeam team, Type type, BukkitTask runnable) {
-        if(this.getTeamTasks(team).containsKey(type) && !this.getTeamTasks(team).get(type).isCancelled()) {
+    public void addTask(VaroTeam team, Type type, Task runnable) {
+        if(this.getTeamTasks(team).containsKey(type) && this.getTeamTasks(team).get(type).isRunning()) {
             throw new IllegalStateException(String.format("Task %s for Team %s was already running!", type.name(), team.name));
         }
 
diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/TeamTasksCommand.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/TeamTasksCommand.java
index fc39f21..fd16e83 100644
--- a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/TeamTasksCommand.java
+++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/TeamTasksCommand.java
@@ -28,7 +28,7 @@ public class TeamTasksCommand extends ApplianceCommand<TeamTasks> {
             sender.sendMessage(
                 tasks.entrySet()
                     .stream()
-                    .map(entry -> String.format("%s: %d", entry.getKey().name(), entry.getValue().getTaskId()))
+                    .map(entry -> String.format("%s: %s", entry.getKey().name(), entry.getValue().getClass().getSimpleName()))
                     .collect(Collectors.joining("\n"))
             );
         }
diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/tasks/BukkitTeamTask.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/tasks/BukkitTeamTask.java
new file mode 100644
index 0000000..0d366ff
--- /dev/null
+++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/tasks/BukkitTeamTask.java
@@ -0,0 +1,16 @@
+package eu.mhsl.craftattack.spawn.varo.appliances.internal.teamTasks.tasks;
+
+import eu.mhsl.craftattack.spawn.varo.appliances.internal.teamTasks.Task;
+import org.bukkit.scheduler.BukkitTask;
+
+public abstract class BukkitTeamTask implements Task, BukkitTask {
+    @Override
+    public void stopTask() {
+        this.cancel();
+    }
+
+    @Override
+    public boolean isRunning() {
+        return !this.isCancelled();
+    }
+}
diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/tasks/CountdownTeamTask.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/tasks/CountdownTeamTask.java
new file mode 100644
index 0000000..16ba024
--- /dev/null
+++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/internal/teamTasks/tasks/CountdownTeamTask.java
@@ -0,0 +1,24 @@
+package eu.mhsl.craftattack.spawn.varo.appliances.internal.teamTasks.tasks;
+
+import eu.mhsl.craftattack.spawn.core.util.text.Countdown;
+import eu.mhsl.craftattack.spawn.varo.appliances.internal.teamTasks.Task;
+import net.kyori.adventure.text.Component;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public class CountdownTeamTask extends Countdown implements Task {
+    public CountdownTeamTask(int countdownFrom, Function<AnnouncementData, Component> announcementBuilder, Consumer<Component> announcementConsumer, Runnable onDone) {
+        super(countdownFrom, announcementBuilder, announcementConsumer, onDone);
+    }
+
+    @Override
+    public void stopTask() {
+        super.cancelIfRunning();
+    }
+
+    @Override
+    public boolean isRunning() {
+        return !super.isDone();
+    }
+}
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 a359cf1..d9c7aea 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
@@ -8,17 +8,21 @@ import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
 import org.bukkit.event.player.PlayerJoinEvent;
 import org.bukkit.event.player.PlayerQuitEvent;
 
+import java.util.Objects;
+
 class ConnectivityChangeListener extends ApplianceListener<Teams> {
     @EventHandler
     public void onLogin(AsyncPlayerPreLoginEvent event) {
         boolean result = this.getAppliance().canLogin(event.getUniqueId());
-        event.kickMessage(new DisconnectInfo(
-            "Kein Teilnehmer",
-            "Du bist nicht als Teilnehmer registriert oder bist ausgeschieden!",
-            "Sollte dies ein Fehler sein, kontaktiere bitte einen Admin.",
-            event.getUniqueId()
-        ).getComponent());
-        if(!result) event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER);
+        if(!result) {
+            event.kickMessage(new DisconnectInfo(
+                "Kein Teilnehmer",
+                "Du bist nicht als Teilnehmer registriert oder bist ausgeschieden!",
+                "Sollte dies ein Fehler sein, kontaktiere bitte einen Admin.",
+                event.getUniqueId()
+            ).getComponent());
+            event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER);
+        }
     }
 
     @EventHandler
@@ -37,6 +41,7 @@ class ConnectivityChangeListener extends ApplianceListener<Teams> {
         }
 
         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()));
         // TODO: strike team
         this.getAppliance().enforceTeamLeave(event.getPlayer());
diff --git a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/teams/Teams.java b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/teams/Teams.java
index 4735c4e..56ab8e5 100644
--- a/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/teams/Teams.java
+++ b/varo/src/main/java/eu/mhsl/craftattack/spawn/varo/appliances/metaGameplay/teams/Teams.java
@@ -3,10 +3,12 @@ package eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.teams;
 import eu.mhsl.craftattack.spawn.core.Main;
 import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
 import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
+import eu.mhsl.craftattack.spawn.core.util.text.Countdown;
 import eu.mhsl.craftattack.spawn.core.util.text.DisconnectInfo;
 import eu.mhsl.craftattack.spawn.common.appliances.metaGameplay.displayName.DisplayName;
 import eu.mhsl.craftattack.spawn.varo.api.repositories.TeamRepository;
 import eu.mhsl.craftattack.spawn.varo.appliances.internal.teamTasks.TeamTasks;
+import eu.mhsl.craftattack.spawn.varo.appliances.internal.teamTasks.tasks.CountdownTeamTask;
 import eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.joinProtection.JoinProtection;
 import eu.mhsl.craftattack.spawn.varo.appliances.metaGameplay.playTimer.PlayTimer;
 import net.kyori.adventure.text.Component;
@@ -17,7 +19,6 @@ import org.bukkit.Bukkit;
 import org.bukkit.OfflinePlayer;
 import org.bukkit.entity.Player;
 import org.bukkit.event.Listener;
-import org.bukkit.scheduler.BukkitTask;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -56,7 +57,7 @@ public class Teams extends Appliance implements DisplayName.Prefixed {
                     updatedTeam.color()
                 );
                 this.teams.add(newTeam);
-                Main.logger().info("Added missing team to Teams registry: " + newTeam);
+                Main.logger().info("Added missing team to Teams registry: " + newTeam.name);
             }
         }
     }
@@ -70,10 +71,20 @@ public class Teams extends Appliance implements DisplayName.Prefixed {
 
     public boolean canLogin(UUID playerId) {
         OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
-        if(offlinePlayer.isOp()) return true;
+        if(offlinePlayer.isOp()) {
+            Main.logger().info(String.format("Allowing player %s to login, because he ist OP!", playerId));
+            return true;
+        }
 
-        return this.teams.stream()
-            .anyMatch(varoTeam -> varoTeam.hasMember(playerId) && !Objects.requireNonNull(varoTeam.getMemberById(playerId)).isDead);
+        Optional<VaroTeam> team = this.teams.stream()
+            .filter(varoTeam -> varoTeam.hasMember(playerId) && !Objects.requireNonNull(varoTeam.getMemberById(playerId)).isDead)
+            .findAny();
+
+        team.ifPresentOrElse(
+            found -> Main.logger().info(String.format("Player %s is in Team %s!", playerId, found.name)),
+            () -> Main.logger().info(String.format("No valid Team found for %s (or he is in a Team but dead)!", playerId))
+        );
+        return team.isPresent();
     }
 
     public @Nullable VaroTeam getTeamFromPlayer(UUID playerId) {
@@ -143,12 +154,30 @@ public class Teams extends Appliance implements DisplayName.Prefixed {
                 .ifPresentOrElse(
                     member -> team.kickTeam(teamNotCompleteInfo),
                     () -> {
-                        BukkitTask kickTask = Bukkit.getScheduler().runTaskLater(
-                            Main.instance(),
-                            team::timeOverKick,
-                            Ticks.TICKS_PER_SECOND * 60 * PlayTimer.PLAYTIME_MINUTES
+                        Main.logger().info(String.format("Starting Time countdown for Team %s with %d", team.name, PlayTimer.PLAYTIME_MINUTES));
+                        CountdownTeamTask countdown = new CountdownTeamTask(
+                            60 * PlayTimer.PLAYTIME_MINUTES,
+                            announcementData -> Component.text(
+                                String.format("Es verbleiben noch %d %s Spielzeit!", announcementData.count(), announcementData.unit()),
+                                NamedTextColor.RED
+                            ),
+                            component -> team.getOnlinePlayers().forEach(player -> player.sendMessage(component)),
+                            team::timeOverKick
                         );
-                        this.queryAppliance(TeamTasks.class).addTask(team, TeamTasks.Type.TIME_KICK, kickTask);
+                        countdown.setDefaultAnnouncements(count -> {
+                            if(count > 300) return null;
+                            if(count > 60 && count % 60 == 0) {
+                                return new Countdown.AnnouncementData(count / 60, "Minuten");
+                            }
+
+                            if(count <= 30 && (count <= 10 || count % 10 == 0)) {
+                                return new Countdown.AnnouncementData(count, "Sekunden");
+                            }
+
+                            return null;
+                        });
+                        countdown.start();
+                        this.queryAppliance(TeamTasks.class).addTask(team, TeamTasks.Type.TIME_KICK, countdown);
                     }
                 ),
             Ticks.TICKS_PER_SECOND * (JoinProtection.resistanceDuration / 2)