From 648535fd222d928f0b43feee5b8438a5a92bd8fc Mon Sep 17 00:00:00 2001
From: Minecon724 <git@m724.eu>
Date: Fri, 3 Jan 2025 13:14:21 +0100
Subject: [PATCH] Make abstract Before it's too late

---
 pom.xml                                       |   4 +-
 src/main/java/eu/m724/tweaks/DebugLogger.java |  38 ++++---
 .../java/eu/m724/tweaks/TweaksModule.java     |  87 +++++++++++++++
 .../java/eu/m724/tweaks/TweaksPlugin.java     | 100 ++++++++----------
 .../eu/m724/tweaks/alert/AlertCommand.java    |  10 +-
 .../eu/m724/tweaks/alert/AlertManager.java    |  90 ----------------
 .../eu/m724/tweaks/alert/AlertModule.java     |  74 +++++++++++++
 .../java/eu/m724/tweaks/auth/AuthManager.java |  25 -----
 .../java/eu/m724/tweaks/auth/AuthModule.java  |  19 ++++
 .../eu/m724/tweaks/chat/ChatCommands.java     |  10 +-
 .../eu/m724/tweaks/chat/ChatListener.java     |  19 ++--
 .../{ChatManager.java => ChatModule.java}     |  29 ++---
 .../m724/tweaks/compass/CompassManager.java   |  21 ----
 ...ompassListener.java => CompassModule.java} |  15 ++-
 ...nockListener.java => DoorKnockModule.java} |  10 +-
 ...rOpenListener.java => DoorOpenModule.java} |  10 +-
 .../{FullListener.java => FullModule.java}    |  10 +-
 .../m724/tweaks/hardcore/HardcoreManager.java |  60 -----------
 .../m724/tweaks/hardcore/HardcoreModule.java  |  52 +++++++++
 ...itchManager.java => KillswitchModule.java} |  25 ++---
 ...backListener.java => KnockbackModule.java} |  12 +--
 .../{MotdManager.java => MotdModule.java}     |  76 +++++--------
 .../java/eu/m724/tweaks/ping/PingChecker.java |   4 +-
 .../m724/tweaks/pomodoro/PomodoroManager.java |  25 -----
 .../m724/tweaks/pomodoro/PomodoroModule.java  |  19 ++++
 ...dstoneManager.java => RedstoneModule.java} |  34 +++---
 .../eu/m724/tweaks/sleep/SleepManager.java    |  18 ----
 .../eu/m724/tweaks/sleep/SleepModule.java     |  19 ++++
 .../{SwingManager.java => SwingModule.java}   |  17 ++-
 .../eu/m724/tweaks/updater/UpdateChecker.java |   9 +-
 ...UpdaterManager.java => UpdaterModule.java} |  30 ++----
 ...nder.java => WorldBorderExpandModule.java} |  13 +--
 .../worldborder/WorldBorderHideModule.java    |  51 +++++++++
 .../tweaks/worldborder/WorldBorderHider.java  |  74 -------------
 34 files changed, 549 insertions(+), 560 deletions(-)
 create mode 100644 src/main/java/eu/m724/tweaks/TweaksModule.java
 delete mode 100644 src/main/java/eu/m724/tweaks/alert/AlertManager.java
 create mode 100644 src/main/java/eu/m724/tweaks/alert/AlertModule.java
 delete mode 100644 src/main/java/eu/m724/tweaks/auth/AuthManager.java
 create mode 100644 src/main/java/eu/m724/tweaks/auth/AuthModule.java
 rename src/main/java/eu/m724/tweaks/chat/{ChatManager.java => ChatModule.java} (87%)
 delete mode 100644 src/main/java/eu/m724/tweaks/compass/CompassManager.java
 rename src/main/java/eu/m724/tweaks/compass/{CompassListener.java => CompassModule.java} (92%)
 rename src/main/java/eu/m724/tweaks/door/{DoorKnockListener.java => DoorKnockModule.java} (90%)
 rename src/main/java/eu/m724/tweaks/door/{DoorOpenListener.java => DoorOpenModule.java} (92%)
 rename src/main/java/eu/m724/tweaks/full/{FullListener.java => FullModule.java} (71%)
 delete mode 100644 src/main/java/eu/m724/tweaks/hardcore/HardcoreManager.java
 create mode 100644 src/main/java/eu/m724/tweaks/hardcore/HardcoreModule.java
 rename src/main/java/eu/m724/tweaks/killswitch/{KillswitchManager.java => KillswitchModule.java} (82%)
 rename src/main/java/eu/m724/tweaks/knockback/{KnockbackListener.java => KnockbackModule.java} (85%)
 rename src/main/java/eu/m724/tweaks/motd/{MotdManager.java => MotdModule.java} (54%)
 delete mode 100644 src/main/java/eu/m724/tweaks/pomodoro/PomodoroManager.java
 create mode 100644 src/main/java/eu/m724/tweaks/pomodoro/PomodoroModule.java
 rename src/main/java/eu/m724/tweaks/redstone/{RedstoneManager.java => RedstoneModule.java} (76%)
 delete mode 100644 src/main/java/eu/m724/tweaks/sleep/SleepManager.java
 create mode 100644 src/main/java/eu/m724/tweaks/sleep/SleepModule.java
 rename src/main/java/eu/m724/tweaks/swing/{SwingManager.java => SwingModule.java} (85%)
 rename src/main/java/eu/m724/tweaks/updater/{UpdaterManager.java => UpdaterModule.java} (68%)
 rename src/main/java/eu/m724/tweaks/worldborder/{WorldBorderExpander.java => WorldBorderExpandModule.java} (78%)
 create mode 100644 src/main/java/eu/m724/tweaks/worldborder/WorldBorderHideModule.java
 delete mode 100644 src/main/java/eu/m724/tweaks/worldborder/WorldBorderHider.java

diff --git a/pom.xml b/pom.xml
index 45ceefd..cec41b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,8 +17,8 @@
         <maven.compiler.target>21</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
-        <project.nms.version>v1_21_R1</project.nms.version>
-        <project.minecraft.version>1.21.1</project.minecraft.version>
+        <project.nms.version>v1_21_R2</project.nms.version>
+        <project.minecraft.version>1.21.3</project.minecraft.version>
         <project.spigot.version>${project.minecraft.version}-R0.1-SNAPSHOT</project.spigot.version>
     </properties>
 
diff --git a/src/main/java/eu/m724/tweaks/DebugLogger.java b/src/main/java/eu/m724/tweaks/DebugLogger.java
index 2b91bee..d74b63c 100644
--- a/src/main/java/eu/m724/tweaks/DebugLogger.java
+++ b/src/main/java/eu/m724/tweaks/DebugLogger.java
@@ -12,36 +12,50 @@ import java.util.logging.Logger;
 public class DebugLogger {
     static Logger logger;
 
-    public static void info(String message) {
-        log(Level.INFO, message);
+    public static void info(String message, Object... format) {
+        log(Level.INFO, message, format);
     }
 
-    public static void warning(String message) {
-        log(Level.WARNING, message);
+    public static void warning(String message, Object... format) {
+        log(Level.WARNING, message, format);
     }
 
-    public static void severe(String message) {
-        log(Level.SEVERE, message);
+    public static void severe(String message, Object... format) {
+        log(Level.SEVERE, message, format);
     }
 
-    public static void fine(String message) {
-        log(Level.FINE, message);
+    public static void fine(String message, Object... format) {
+        log(Level.FINE, message, format);
     }
 
-    private static void log(Level level, String message) {
+    public static void finer(String message, Object... format) {
+        log(Level.FINER, message, format);
+    }
+
+    private static void log(Level level, String message, Object... format) {
         if (logger.getLevel().intValue() > level.intValue()) return;
 
         var caller = Thread.currentThread().getStackTrace()[3].getClassName();
 
+        if (caller.equals(TweaksModule.class.getName())) {
+            var pcaller = Thread.currentThread().getStackTrace()[4].getClassName();
+            if (pcaller.endsWith("Module"))
+                caller = pcaller;
+        }
+
         if (caller.startsWith("eu.m724.tweaks."))
             caller = caller.substring(15);
 
-        message = "[" + caller + "] " + message;
+        message = "[" + caller + "] " + message.formatted(format);
 
         if (level.intValue() < Level.INFO.intValue()) { // levels below info are never logged even if set for some reason
-            level = Level.INFO;
             // colors text gray (cyan is close to gray)
-            message = "\033[36m" + message + "\033[39m";
+            if (level == Level.FINE) {
+                message = "\033[38;5;250m" + message + "\033[39m";
+            } else {
+                message = "\033[38;5;245m" + message + "\033[39m";
+            }
+            level = Level.INFO;
         }
 
         logger.log(level, message);
diff --git a/src/main/java/eu/m724/tweaks/TweaksModule.java b/src/main/java/eu/m724/tweaks/TweaksModule.java
new file mode 100644
index 0000000..a75bdea
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/TweaksModule.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2025  Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.ProtocolLibrary;
+import com.comphenix.protocol.events.ListenerPriority;
+import com.comphenix.protocol.events.PacketAdapter;
+import com.comphenix.protocol.events.PacketEvent;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.event.Listener;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.function.Consumer;
+
+public abstract class TweaksModule {
+    protected abstract void onInit();
+
+    void init() {
+        var name = getClass().getSimpleName();
+        DebugLogger.finer("Initializing " + name);
+        long start = System.nanoTime();
+
+        this.onInit();
+
+        long end = System.nanoTime();
+        DebugLogger.fine("Initialized %s in %d µs", name, (end - start) / 1000);
+    }
+
+    // TODO not static maybe?
+    protected TweaksPlugin getPlugin() {
+        return TweaksPlugin.getInstance();
+    }
+
+    protected TweaksConfig getConfig() {
+        return TweaksConfig.getConfig();
+    }
+
+    protected void registerEvents(Listener listener) {
+        DebugLogger.finer("Registered listener: " + listener.getClass().getName());
+        getPlugin().getServer().getPluginManager().registerEvents(listener, getPlugin());
+    }
+
+    protected void onPacketSend(PacketType packetType, Consumer<PacketEvent> consumer) {
+        ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
+            @Override
+            public void onPacketSending(PacketEvent event) {
+                consumer.accept(event);
+            }
+        });
+
+        DebugLogger.finer("Registered packet send: " + packetType.name());
+    }
+
+    protected void onPacketReceive(PacketType packetType, Consumer<PacketEvent> consumer) {
+        ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(getPlugin(), ListenerPriority.NORMAL, packetType) {
+            @Override
+            public void onPacketReceiving(PacketEvent event) {
+                consumer.accept(event);
+            }
+        });
+
+        DebugLogger.finer("Registered packet receive: " + packetType.name());
+    }
+
+    protected void registerCommand(String command, CommandExecutor executor) {
+        getPlugin().getCommand(command).setExecutor(executor);
+        DebugLogger.finer("Registered command: " + command);
+    }
+
+    public static <T extends TweaksModule> T init(Class<T> clazz) {
+        T module;
+        try {
+            module = clazz.getDeclaredConstructor().newInstance();
+        } catch (InstantiationException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+            throw new RuntimeException(e);
+        }
+
+        module.init();
+
+        return module;
+    }
+}
diff --git a/src/main/java/eu/m724/tweaks/TweaksPlugin.java b/src/main/java/eu/m724/tweaks/TweaksPlugin.java
index b0abd9a..42975fc 100644
--- a/src/main/java/eu/m724/tweaks/TweaksPlugin.java
+++ b/src/main/java/eu/m724/tweaks/TweaksPlugin.java
@@ -7,33 +7,36 @@
 package eu.m724.tweaks;
 
 import eu.m724.mstats.MStatsPlugin;
-import eu.m724.tweaks.alert.AlertManager;
-import eu.m724.tweaks.auth.AuthManager;
-import eu.m724.tweaks.chat.ChatManager;
-import eu.m724.tweaks.door.DoorKnockListener;
-import eu.m724.tweaks.door.DoorOpenListener;
-import eu.m724.tweaks.full.FullListener;
-import eu.m724.tweaks.hardcore.HardcoreManager;
-import eu.m724.tweaks.killswitch.KillswitchManager;
-import eu.m724.tweaks.knockback.KnockbackListener;
-import eu.m724.tweaks.motd.MotdManager;
+import eu.m724.tweaks.alert.AlertModule;
+import eu.m724.tweaks.auth.AuthModule;
+import eu.m724.tweaks.chat.ChatModule;
+import eu.m724.tweaks.door.DoorKnockModule;
+import eu.m724.tweaks.door.DoorOpenModule;
+import eu.m724.tweaks.full.FullModule;
+import eu.m724.tweaks.hardcore.HardcoreModule;
+import eu.m724.tweaks.killswitch.KillswitchModule;
+import eu.m724.tweaks.knockback.KnockbackModule;
+import eu.m724.tweaks.motd.MotdModule;
 import eu.m724.tweaks.ping.F3NameListener;
 import eu.m724.tweaks.ping.PingChecker;
-import eu.m724.tweaks.pomodoro.PomodoroManager;
-import eu.m724.tweaks.redstone.RedstoneManager;
-import eu.m724.tweaks.sleep.SleepManager;
-import eu.m724.tweaks.swing.SwingManager;
-import eu.m724.tweaks.updater.UpdaterManager;
-import eu.m724.tweaks.worldborder.WorldBorderExpander;
-import eu.m724.tweaks.worldborder.WorldBorderHider;
+import eu.m724.tweaks.pomodoro.PomodoroModule;
+import eu.m724.tweaks.redstone.RedstoneModule;
+import eu.m724.tweaks.sleep.SleepModule;
+import eu.m724.tweaks.swing.SwingModule;
+import eu.m724.tweaks.updater.UpdaterModule;
+import eu.m724.tweaks.worldborder.WorldBorderExpandModule;
+import eu.m724.tweaks.worldborder.WorldBorderHideModule;
 
 import java.util.Locale;
 import java.util.logging.Level;
 
 public class TweaksPlugin extends MStatsPlugin {
+    private static TweaksPlugin INSTANCE;
+
     @Override
     public void onEnable() {
         long start = System.nanoTime();
+        INSTANCE = this;
 
         if (getServer().getPluginManager().getPlugin("ProtocolLib") == null) {
             getLogger().severe("ProtocolLib is required for this plugin.");
@@ -46,37 +49,36 @@ public class TweaksPlugin extends MStatsPlugin {
 
         getLogger().setLevel(config.debug() ? Level.FINEST : Level.INFO);
         DebugLogger.logger = getLogger();
-        DebugLogger.fine("Debug enabled. There may be performance issues.");
 
-        DebugLogger.fine("Enabling Language");
+        if (config.debug()) {
+            DebugLogger.warning("Debug harms performance");
+        }
+
+        DebugLogger.fine("Language");
         new Language(Locale.of(config.locale())); // TODO
         DebugLogger.fine(Language.getString("languageNotice", Language.getString("language"), Language.getString("languageEnglish")));
 
         /* start modules */
 
         if (config.worldborderHide()) {
-            DebugLogger.fine("Enabling Worldborder hide");
-            new WorldBorderHider().init(this);
+            TweaksModule.init(WorldBorderHideModule.class);
         }
 
         if (config.worldborderExpand()) {
-            DebugLogger.fine("Enabling Worldborder expand");
-            new WorldBorderExpander().init(this);
+            TweaksModule.init(WorldBorderExpandModule.class);
+
         }
 
         if (config.chatEnabled()) {
-            DebugLogger.fine("Enabling Chat");
-            new ChatManager(this).init(getCommand("chat"), getCommand("chatmanage"));
+            TweaksModule.init(ChatModule.class);
         }
 
         if (config.doorKnocking()) {
-            DebugLogger.fine("Enabling Door knock");
-            getServer().getPluginManager().registerEvents(new DoorKnockListener(), this);
+            TweaksModule.init(DoorKnockModule.class);
         }
 
         if (config.doorDoubleOpen()) {
-            DebugLogger.fine("Enabling Door double open");
-            getServer().getPluginManager().registerEvents(new DoorOpenListener(), this);
+            TweaksModule.init(DoorOpenModule.class);
         }
 
         if (config.brandEnabled()) {
@@ -88,57 +90,45 @@ public class TweaksPlugin extends MStatsPlugin {
         new PingChecker(this).init(getCommand("ping"));
 
         if (config.motdEnabled()) {
-            DebugLogger.fine("Enabling MOTD");
-            new MotdManager(this).init();
+            TweaksModule.init(MotdModule.class);
         }
 
         if (config.pomodoroEnabled()) {
-            DebugLogger.fine("Enabling Pomodoro");
-            new PomodoroManager(this).init(getCommand("pomodoro"));
+            TweaksModule.init(PomodoroModule.class);
         }
 
         if (config.updaterEnabled()) {
-            DebugLogger.fine("Enabling Updater");
-            new UpdaterManager(this).init(getCommand("updates"));
+            TweaksModule.init(UpdaterModule.class);
         }
 
         if (config.hardcoreEnabled()) {
-            DebugLogger.fine("Enabling Hardcore");
-            new HardcoreManager().init(this);
+            TweaksModule.init(HardcoreModule.class);
         }
 
         if (config.sleepEnabled()) {
-            DebugLogger.fine("Enabling Sleep");
-            new SleepManager().init(this);
+            TweaksModule.init(SleepModule.class);
         }
 
         if (config.authEnabled()) {
-            DebugLogger.fine("Enabling Auth");
-            new AuthManager(this).init(getCommand("tauth"));
+            TweaksModule.init(AuthModule.class);
         }
 
-        DebugLogger.fine("Enabling Alert");
-        new AlertManager(this).init(getCommand("emergencyalert"));
+        TweaksModule.init(AlertModule.class);
 
-        DebugLogger.fine("Enabling Full");
-        getServer().getPluginManager().registerEvents(new FullListener(), this);
+        TweaksModule.init(FullModule.class);
 
         if (config.redstoneEnabled()) {
-            DebugLogger.fine("Enabling Redstone");
-            new RedstoneManager(this).init(getCommand("retstone"));
+            TweaksModule.init(RedstoneModule.class);
         }
 
-        DebugLogger.fine("Enabling Knockback");
-        new KnockbackListener(this);
+        TweaksModule.init(KnockbackModule.class);
 
         if (config.killswitchEnabled()) {
-            DebugLogger.fine("Enabling Killswitch");
-            new KillswitchManager(this).init(getCommand("servkill"));
+            TweaksModule.init(KillswitchModule.class);
         }
 
         if (config.swingEnabled()) {
-            DebugLogger.fine("Enabling Swing");
-            new SwingManager(this).init();
+            TweaksModule.init(SwingModule.class);
         }
 
         /* end modules */
@@ -154,4 +144,8 @@ public class TweaksPlugin extends MStatsPlugin {
     public boolean hasResource(String resource) {
         return this.getClassLoader().getResource(resource) != null;
     }
+
+    public static TweaksPlugin getInstance() {
+        return INSTANCE;
+    }
 }
diff --git a/src/main/java/eu/m724/tweaks/alert/AlertCommand.java b/src/main/java/eu/m724/tweaks/alert/AlertCommand.java
index e5748a7..2a5d821 100644
--- a/src/main/java/eu/m724/tweaks/alert/AlertCommand.java
+++ b/src/main/java/eu/m724/tweaks/alert/AlertCommand.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
@@ -20,9 +20,9 @@ public class AlertCommand implements CommandExecutor {
     private List<String> pending;
     private long when;
 
-    private final AlertManager manager;
+    private final AlertModule manager;
 
-    public AlertCommand(AlertManager manager) {
+    public AlertCommand(AlertModule manager) {
         this.manager = manager;
     }
 
@@ -51,7 +51,7 @@ public class AlertCommand implements CommandExecutor {
             } else if (args[0].equalsIgnoreCase("confirm")) {
                 sender.sendMessage("CONFIRM must be in all caps");
             } else if (args[0].equalsIgnoreCase("cancel")) {
-                if (AlertManager.current != null) {
+                if (AlertModule.current != null) {
                     manager.stop();
                     sender.sendMessage("Cancelled alert");
                 } else if (pending != null) {
@@ -86,7 +86,7 @@ public class AlertCommand implements CommandExecutor {
 
                 if (pending != null) {
                     when = System.currentTimeMillis();
-                    if (AlertManager.current != null) {
+                    if (AlertModule.current != null) {
                         sender.sendMessage("Broadcasting a new alert will cancel the currently active one");
                     }
                     sender.sendMessage("Please confirm broadcast with /emergencyalert CONFIRM within 15 seconds");
diff --git a/src/main/java/eu/m724/tweaks/alert/AlertManager.java b/src/main/java/eu/m724/tweaks/alert/AlertManager.java
deleted file mode 100644
index f2b72d9..0000000
--- a/src/main/java/eu/m724/tweaks/alert/AlertManager.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2024  Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.alert;
-
-import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.events.ListenerPriority;
-import com.comphenix.protocol.events.PacketAdapter;
-import com.comphenix.protocol.events.PacketContainer;
-import com.comphenix.protocol.events.PacketEvent;
-import eu.m724.tweaks.TweaksPlugin;
-import org.bukkit.command.PluginCommand;
-import org.bukkit.entity.Player;
-import org.bukkit.scheduler.BukkitTask;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class AlertManager {
-    private final TweaksPlugin plugin;
-
-    private BukkitTask notifyTask;
-    static Alert current;
-    static Map<Player, Integer> pages = new HashMap<>();
-
-    public AlertManager(TweaksPlugin plugin) {
-        this.plugin = plugin;
-    }
-
-    public void init(PluginCommand command) {
-        command.setExecutor(new AlertCommand(this));
-
-        ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
-                plugin,
-                ListenerPriority.NORMAL,
-                PacketType.Play.Client.ENCHANT_ITEM
-        ) {
-            @Override
-            public void onPacketReceiving(PacketEvent event) {
-                if (current == null) return;
-                if (!current.isOpen(event.getPlayer())) return;
-
-                PacketContainer packet = event.getPacket();
-
-                int windowId, buttonId;
-                windowId = packet.getIntegers().read(0);
-                buttonId = packet.getIntegers().read(1);
-
-                var page = pages.getOrDefault(event.getPlayer(),1);
-
-                if (buttonId == 1) { // prev page
-                    page--;
-                } else if (buttonId == 2) { // nextc page
-                    page++;
-                } else {
-                    return;
-                }
-
-                pages.put(event.getPlayer(), page);
-                var npacket = new PacketContainer(PacketType.Play.Server.WINDOW_DATA);
-                npacket.getIntegers().write(0, windowId);
-                npacket.getIntegers().write(1, 0);
-                npacket.getIntegers().write(2, page);
-                ProtocolLibrary.getProtocolManager().sendServerPacket(event.getPlayer(), npacket);
-            }
-        });
-    }
-
-    public Alert start(String... content) {
-        stop();
-        current = new Alert(content);
-        notifyTask = new AlertRunnable(current, v -> this.stop()).runTaskTimer(plugin, 0, 10);
-        return current;
-    }
-
-    public void stop() {
-        if (current == null) return;
-        for (Player player : plugin.getServer().getOnlinePlayers()) {
-            if (current.isOpen(player))
-                player.closeInventory();
-        }
-        pages.clear();
-        notifyTask.cancel();
-        current = null;
-    }
-}
diff --git a/src/main/java/eu/m724/tweaks/alert/AlertModule.java b/src/main/java/eu/m724/tweaks/alert/AlertModule.java
new file mode 100644
index 0000000..b7df6a9
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/alert/AlertModule.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2025  Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.alert;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.ProtocolLibrary;
+import com.comphenix.protocol.events.PacketContainer;
+import eu.m724.tweaks.TweaksModule;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitTask;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AlertModule extends TweaksModule {
+    private BukkitTask notifyTask;
+    static Alert current;
+    static Map<Player, Integer> pages = new HashMap<>();
+
+    @Override
+    protected void onInit() {
+        registerCommand("emergencyalert", new AlertCommand(this));
+
+        onPacketReceive(PacketType.Play.Client.ENCHANT_ITEM, (event) -> {
+            if (current == null) return;
+            if (!current.isOpen(event.getPlayer())) return;
+
+            PacketContainer packet = event.getPacket();
+
+            int windowId, buttonId;
+            windowId = packet.getIntegers().read(0);
+            buttonId = packet.getIntegers().read(1);
+
+            var page = pages.getOrDefault(event.getPlayer(),1);
+
+            if (buttonId == 1) { // prev page
+                page--;
+            } else if (buttonId == 2) { // nextc page
+                page++;
+            } else {
+                return;
+            }
+
+            pages.put(event.getPlayer(), page);
+            var npacket = new PacketContainer(PacketType.Play.Server.WINDOW_DATA);
+            npacket.getIntegers().write(0, windowId);
+            npacket.getIntegers().write(1, 0);
+            npacket.getIntegers().write(2, page);
+            ProtocolLibrary.getProtocolManager().sendServerPacket(event.getPlayer(), npacket);
+        });
+    }
+
+    public Alert start(String... content) {
+        stop();
+        current = new Alert(content);
+        notifyTask = new AlertRunnable(current, v -> this.stop()).runTaskTimer(getPlugin(), 0, 10);
+        return current;
+    }
+
+    public void stop() {
+        if (current == null) return;
+        for (Player player : getPlugin().getServer().getOnlinePlayers()) {
+            if (current.isOpen(player))
+                player.closeInventory();
+        }
+        pages.clear();
+        notifyTask.cancel();
+        current = null;
+    }
+}
diff --git a/src/main/java/eu/m724/tweaks/auth/AuthManager.java b/src/main/java/eu/m724/tweaks/auth/AuthManager.java
deleted file mode 100644
index 402b376..0000000
--- a/src/main/java/eu/m724/tweaks/auth/AuthManager.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2024  Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.auth;
-
-import org.bukkit.command.PluginCommand;
-import org.bukkit.plugin.Plugin;
-
-public class AuthManager {
-    private final AuthStorage authStorage;
-    private final Plugin plugin;
-
-    public AuthManager(Plugin plugin) {
-        this.plugin = plugin;
-        this.authStorage = new AuthStorage(plugin);
-    }
-
-    public void init(PluginCommand command) {
-        plugin.getServer().getPluginManager().registerEvents(new AuthListener(authStorage), plugin);
-        command.setExecutor(new AuthCommands(authStorage));
-    }
-}
diff --git a/src/main/java/eu/m724/tweaks/auth/AuthModule.java b/src/main/java/eu/m724/tweaks/auth/AuthModule.java
new file mode 100644
index 0000000..43d0fcf
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/auth/AuthModule.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025  Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.auth;
+
+import eu.m724.tweaks.TweaksModule;
+
+public class AuthModule extends TweaksModule {
+    @Override
+    protected void onInit() {
+        var authStorage = new AuthStorage(getPlugin());
+
+        registerEvents(new AuthListener(authStorage));
+        registerCommand("tauth", new AuthCommands(authStorage));
+    }
+}
diff --git a/src/main/java/eu/m724/tweaks/chat/ChatCommands.java b/src/main/java/eu/m724/tweaks/chat/ChatCommands.java
index 09e19b7..1ed2180 100644
--- a/src/main/java/eu/m724/tweaks/chat/ChatCommands.java
+++ b/src/main/java/eu/m724/tweaks/chat/ChatCommands.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
@@ -21,9 +21,9 @@ import java.util.Arrays;
 import java.util.stream.Collectors;
 
 public class ChatCommands implements CommandExecutor {
-    private final ChatManager manager;
+    private final ChatModule manager;
 
-    public ChatCommands(ChatManager manager) {
+    public ChatCommands(ChatModule manager) {
         this.manager = manager;
     }
 
@@ -94,9 +94,9 @@ public class ChatCommands implements CommandExecutor {
                             ChatRoom newRoom = manager.createChatRoom(argument, null, player);
                             sender.sendMessage("Created a chat room. Join it: /c " + newRoom.id);
                             sender.sendMessage("You might also want to protect it with a password: /cm setpassword");
-                        } catch (ChatManager.InvalidIdException e) {
+                        } catch (ChatModule.InvalidIdException e) {
                             sender.sendMessage("ID is invalid: " + e.getMessage());
-                        } catch (ChatManager.ChatRoomExistsException e) {
+                        } catch (ChatModule.ChatRoomExistsException e) {
                             sender.sendMessage("Room %s already exists".formatted(argument));
                         } catch (IOException e) {
                             sender.sendMessage("Failed to create room");
diff --git a/src/main/java/eu/m724/tweaks/chat/ChatListener.java b/src/main/java/eu/m724/tweaks/chat/ChatListener.java
index f114e58..83dc4c2 100644
--- a/src/main/java/eu/m724/tweaks/chat/ChatListener.java
+++ b/src/main/java/eu/m724/tweaks/chat/ChatListener.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
@@ -25,13 +25,14 @@ import org.bukkit.event.player.PlayerJoinEvent;
 import org.bukkit.event.player.PlayerQuitEvent;
 
 public class ChatListener implements Listener {
-    private final ChatManager chatManager;
+    private final ChatModule chatModule;
     private final String defaultRoom = TweaksConfig.getConfig().chatDefaultName();
     private final boolean localEvents = TweaksConfig.getConfig().chatLocalEvents();
     private final int radius;
 
-    public ChatListener(ChatManager chatManager) {
-        this.chatManager = chatManager;
+    public ChatListener(ChatModule chatModule) {
+        this.chatModule = chatModule;
+
         if (TweaksConfig.getConfig().chatRadius() < 0) {
             radius = 0;
         } else {
@@ -52,7 +53,7 @@ public class ChatListener implements Listener {
     @EventHandler
     public void onPlayerJoin(PlayerJoinEvent event) {
         Player player = event.getPlayer();
-        ChatRoom chatRoom = chatManager.getPlayerChatRoom(player);
+        ChatRoom chatRoom = chatModule.getPlayerChatRoom(player);
 
         if (localEvents) {
             var cb = new ComponentBuilder()
@@ -77,7 +78,7 @@ public class ChatListener implements Listener {
     @EventHandler
     public void onPlayerQuit(PlayerQuitEvent event) {
         Player player = event.getPlayer();
-        ChatRoom chatRoom = chatManager.removePlayer(player);
+        ChatRoom chatRoom = chatModule.removePlayer(player);
 
         if (localEvents) {
             var cb = new ComponentBuilder()
@@ -103,7 +104,7 @@ public class ChatListener implements Listener {
     public void onPlayerDeath(PlayerDeathEvent event) {
         if (localEvents) {
             Player player = event.getEntity();
-            ChatRoom chatRoom = chatManager.getPlayerChatRoom(player);
+            ChatRoom chatRoom = chatModule.getPlayerChatRoom(player);
 
             // would be easier on Paper but this is not Paper
             BaseComponent deathMessage = ComponentSerializer.deserialize(Component.Serializer.toJson(((CraftPlayer)player).getHandle().getCombatTracker().getDeathMessage(), CraftRegistry.getMinecraftRegistry()));
@@ -124,7 +125,7 @@ public class ChatListener implements Listener {
 
             // broadcast to killer if available
             if (player.getLastDamageCause().getDamageSource().getCausingEntity() instanceof Player killer) {
-                ChatRoom chatRoom2 = chatManager.getPlayerChatRoom(killer);
+                ChatRoom chatRoom2 = chatModule.getPlayerChatRoom(killer);
                 if (chatRoom != chatRoom2) {
                     if (proximityFor(chatRoom)) {
                         chatRoom2.broadcastNear(killer.getLocation(), radius, component);
@@ -142,7 +143,7 @@ public class ChatListener implements Listener {
     @EventHandler
     public void onAsyncPlayerChat(AsyncPlayerChatEvent event) {
         Player player = event.getPlayer();
-        ChatRoom chatRoom = chatManager.getPlayerChatRoom(player);
+        ChatRoom chatRoom = chatModule.getPlayerChatRoom(player);
         String message = event.getMessage();
 
         var component = new ComponentBuilder()
diff --git a/src/main/java/eu/m724/tweaks/chat/ChatManager.java b/src/main/java/eu/m724/tweaks/chat/ChatModule.java
similarity index 87%
rename from src/main/java/eu/m724/tweaks/chat/ChatManager.java
rename to src/main/java/eu/m724/tweaks/chat/ChatModule.java
index 04d1a84..eaad314 100644
--- a/src/main/java/eu/m724/tweaks/chat/ChatManager.java
+++ b/src/main/java/eu/m724/tweaks/chat/ChatModule.java
@@ -1,50 +1,43 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
 
 package eu.m724.tweaks.chat;
 
-import eu.m724.tweaks.TweaksConfig;
+import eu.m724.tweaks.TweaksModule;
 import net.md_5.bungee.api.ChatColor;
 import net.md_5.bungee.api.chat.ComponentBuilder;
 import org.bukkit.NamespacedKey;
 import org.bukkit.OfflinePlayer;
-import org.bukkit.command.PluginCommand;
 import org.bukkit.entity.Player;
 import org.bukkit.persistence.PersistentDataType;
-import org.bukkit.plugin.Plugin;
 
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
 
-public class ChatManager {
-    private final Plugin plugin;
-    private final NamespacedKey chatRoomKey;
-    private final String defaultRoom;
+public class ChatModule extends TweaksModule {
+    private final NamespacedKey chatRoomKey = new NamespacedKey(getPlugin(), "chatRoom");
+    private final String defaultRoom = getConfig().chatDefaultName();
 
     private final Map<Player, ChatRoom> playerMap = new HashMap<>();
     private final Map<String, ChatRoom> roomIdMap = new HashMap<>();
 
-    public ChatManager(Plugin plugin) {
-        this.plugin = plugin;
-        this.chatRoomKey = new NamespacedKey(plugin, "chatRoom");
-        this.defaultRoom = TweaksConfig.getConfig().chatDefaultName();
-    }
 
-    public void init(PluginCommand chatCommand, PluginCommand chatManageCommand) {
-        if (plugin.getServer().isEnforcingSecureProfiles()) {
+    @Override
+    protected void onInit() {
+        if (getPlugin().getServer().isEnforcingSecureProfiles()) {
             throw new RuntimeException("Please disable enforce-secure-profile in server.properties to use chatrooms");
         }
 
         getById(defaultRoom);
-        plugin.getServer().getPluginManager().registerEvents(new ChatListener(this), plugin);
+        registerEvents(new ChatListener(this));
 
         var chatCommands = new ChatCommands(this);
-        chatCommand.setExecutor(chatCommands);
-        chatManageCommand.setExecutor(chatCommands);
+        registerCommand("chat", chatCommands);
+        registerCommand("chatmanage", chatCommands);
     }
 
     /**
diff --git a/src/main/java/eu/m724/tweaks/compass/CompassManager.java b/src/main/java/eu/m724/tweaks/compass/CompassManager.java
deleted file mode 100644
index 0ba9113..0000000
--- a/src/main/java/eu/m724/tweaks/compass/CompassManager.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2024  Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.compass;
-
-import org.bukkit.plugin.Plugin;
-
-public class CompassManager {
-    private final Plugin plugin;
-
-    public CompassManager(Plugin plugin) {
-        this.plugin = plugin;
-    }
-
-    public void init() {
-        plugin.getServer().getPluginManager().registerEvents(new CompassListener(), plugin);
-    }
-}
diff --git a/src/main/java/eu/m724/tweaks/compass/CompassListener.java b/src/main/java/eu/m724/tweaks/compass/CompassModule.java
similarity index 92%
rename from src/main/java/eu/m724/tweaks/compass/CompassListener.java
rename to src/main/java/eu/m724/tweaks/compass/CompassModule.java
index f1873cd..23dfcf7 100644
--- a/src/main/java/eu/m724/tweaks/compass/CompassListener.java
+++ b/src/main/java/eu/m724/tweaks/compass/CompassModule.java
@@ -1,12 +1,12 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
 
 package eu.m724.tweaks.compass;
 
-import eu.m724.tweaks.TweaksConfig;
+import eu.m724.tweaks.TweaksModule;
 import net.md_5.bungee.api.ChatColor;
 import net.md_5.bungee.api.ChatMessageType;
 import net.md_5.bungee.api.chat.ComponentBuilder;
@@ -25,9 +25,9 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 
 // TODO dimension check
-public class CompassListener implements Listener {
-    private final int precision = TweaksConfig.getConfig().compassPrecision(); // degrees every point
-    private final int width = TweaksConfig.getConfig().compassWidth(); // points left to right
+public class CompassModule extends TweaksModule implements Listener {
+    private final int precision = getConfig().compassPrecision(); // degrees every point
+    private final int width = getConfig().compassWidth(); // points left to right
 
     private final Map<Integer, String> points = Map.of(
             0, ChatColor.DARK_GRAY + "S",
@@ -36,6 +36,11 @@ public class CompassListener implements Listener {
             270, ChatColor.DARK_GRAY + "E"
     );
 
+    @Override
+    protected void onInit() {
+        registerEvents(this);
+    }
+
     @EventHandler
     public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent event) {
         if (event.getMainHandItem().getType() == Material.COMPASS || event.getOffHandItem().getType() == Material.COMPASS) {
diff --git a/src/main/java/eu/m724/tweaks/door/DoorKnockListener.java b/src/main/java/eu/m724/tweaks/door/DoorKnockModule.java
similarity index 90%
rename from src/main/java/eu/m724/tweaks/door/DoorKnockListener.java
rename to src/main/java/eu/m724/tweaks/door/DoorKnockModule.java
index b9c718d..2627c18 100644
--- a/src/main/java/eu/m724/tweaks/door/DoorKnockListener.java
+++ b/src/main/java/eu/m724/tweaks/door/DoorKnockModule.java
@@ -1,11 +1,12 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
 
 package eu.m724.tweaks.door;
 
+import eu.m724.tweaks.TweaksModule;
 import org.bukkit.Location;
 import org.bukkit.Material;
 import org.bukkit.Sound;
@@ -22,7 +23,12 @@ import org.bukkit.util.RayTraceResult;
 
 import java.util.concurrent.ThreadLocalRandom;
 
-public class DoorKnockListener implements Listener {
+public class DoorKnockModule extends TweaksModule implements Listener {
+    @Override
+    protected void onInit() {
+        registerEvents(this);
+    }
+
     @EventHandler
     public void onBlockDamageAbort(BlockDamageAbortEvent event) {
         Block block = event.getBlock();
diff --git a/src/main/java/eu/m724/tweaks/door/DoorOpenListener.java b/src/main/java/eu/m724/tweaks/door/DoorOpenModule.java
similarity index 92%
rename from src/main/java/eu/m724/tweaks/door/DoorOpenListener.java
rename to src/main/java/eu/m724/tweaks/door/DoorOpenModule.java
index 1b431eb..e6773df 100644
--- a/src/main/java/eu/m724/tweaks/door/DoorOpenListener.java
+++ b/src/main/java/eu/m724/tweaks/door/DoorOpenModule.java
@@ -1,11 +1,12 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
 
 package eu.m724.tweaks.door;
 
+import eu.m724.tweaks.TweaksModule;
 import org.bukkit.*;
 import org.bukkit.block.Block;
 import org.bukkit.block.BlockFace;
@@ -15,7 +16,11 @@ import org.bukkit.event.Listener;
 import org.bukkit.event.block.Action;
 import org.bukkit.event.player.PlayerInteractEvent;
 
-public class DoorOpenListener implements Listener {
+public class DoorOpenModule extends TweaksModule implements Listener {
+    @Override
+    protected void onInit() {
+        registerEvents(this);
+    }
 
     @EventHandler
     public void onPlayerInteract(PlayerInteractEvent event) {
@@ -71,5 +76,4 @@ public class DoorOpenListener implements Listener {
             location.getBlock().setBlockData(nextDoor);
         }
     }
-
 }
diff --git a/src/main/java/eu/m724/tweaks/full/FullListener.java b/src/main/java/eu/m724/tweaks/full/FullModule.java
similarity index 71%
rename from src/main/java/eu/m724/tweaks/full/FullListener.java
rename to src/main/java/eu/m724/tweaks/full/FullModule.java
index 29f4572..8b40269 100644
--- a/src/main/java/eu/m724/tweaks/full/FullListener.java
+++ b/src/main/java/eu/m724/tweaks/full/FullModule.java
@@ -1,16 +1,22 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
 
 package eu.m724.tweaks.full;
 
+import eu.m724.tweaks.TweaksModule;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
 import org.bukkit.event.player.PlayerLoginEvent;
 
-public class FullListener implements Listener {
+public class FullModule extends TweaksModule implements Listener {
+    @Override
+    protected void onInit() {
+        registerEvents(this);
+    }
+
     @EventHandler
     public void onPlayerLogin(PlayerLoginEvent event) {
         if (event.getResult() == PlayerLoginEvent.Result.KICK_FULL && event.getPlayer().hasPermission("tweaks724.bypass-full")) {
diff --git a/src/main/java/eu/m724/tweaks/hardcore/HardcoreManager.java b/src/main/java/eu/m724/tweaks/hardcore/HardcoreManager.java
deleted file mode 100644
index e03179e..0000000
--- a/src/main/java/eu/m724/tweaks/hardcore/HardcoreManager.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2025  Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.hardcore;
-
-import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.events.ListenerPriority;
-import com.comphenix.protocol.events.PacketAdapter;
-import com.comphenix.protocol.events.PacketContainer;
-import com.comphenix.protocol.events.PacketEvent;
-import eu.m724.tweaks.DebugLogger;
-import eu.m724.tweaks.TweaksConfig;
-import net.minecraft.world.level.levelgen.RandomSupport;
-import net.minecraft.world.level.levelgen.Xoroshiro128PlusPlus;
-import org.bukkit.plugin.Plugin;
-
-import java.math.BigDecimal;
-import java.math.BigInteger;
-
-// how we do it is much faster than any Random
-public class HardcoreManager {
-    private final double chance = TweaksConfig.getConfig().hardcoreChance();
-
-    private final long chanceLong = BigInteger.valueOf(Long.MIN_VALUE)
-            .add(
-                    new BigDecimal(
-                            BigInteger.valueOf(Long.MAX_VALUE).subtract(BigInteger.valueOf(Long.MIN_VALUE))
-                    ).multiply(
-                            BigDecimal.valueOf(chance)
-                    ).toBigInteger()
-            ).longValue();
-
-    private final Xoroshiro128PlusPlus rng = new Xoroshiro128PlusPlus(
-            RandomSupport.generateUniqueSeed(),
-            RandomSupport.generateUniqueSeed()
-    );
-
-    public void init(Plugin plugin) {
-        DebugLogger.fine("Chance long: " + chanceLong);
-
-        ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
-                plugin,
-                ListenerPriority.NORMAL,
-                PacketType.Play.Server.LOGIN
-        ) {
-            @Override
-            public void onPacketSending(PacketEvent event) {
-                PacketContainer packet = event.getPacket();
-
-                if (rng.nextLong() < chanceLong)
-                    // the "is hardcore" boolean https://wiki.vg/Protocol#Login_.28play.29
-                    packet.getBooleans().write(0, true);
-            }
-        });
-    }
-}
diff --git a/src/main/java/eu/m724/tweaks/hardcore/HardcoreModule.java b/src/main/java/eu/m724/tweaks/hardcore/HardcoreModule.java
new file mode 100644
index 0000000..b0df9a4
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/hardcore/HardcoreModule.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2025  Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.hardcore;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.events.PacketContainer;
+import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.TweaksModule;
+import net.minecraft.world.level.levelgen.RandomSupport;
+import net.minecraft.world.level.levelgen.Xoroshiro128PlusPlus;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+// how we do it is much faster than any Random
+public class HardcoreModule extends TweaksModule {
+    private final Xoroshiro128PlusPlus rng;
+    private final long chanceLong;
+
+    public HardcoreModule() {
+        this.rng = new Xoroshiro128PlusPlus(
+                RandomSupport.generateUniqueSeed(),
+                RandomSupport.generateUniqueSeed()
+        );
+
+        this.chanceLong = BigInteger.valueOf(Long.MIN_VALUE)
+                .add(
+                        new BigDecimal(
+                                BigInteger.valueOf(Long.MAX_VALUE).subtract(BigInteger.valueOf(Long.MIN_VALUE))
+                        ).multiply(
+                                BigDecimal.valueOf(getConfig().hardcoreChance())
+                        ).toBigInteger()
+                ).longValue();
+    }
+
+    @Override
+    protected void onInit() {
+        DebugLogger.fine("Chance long: " + chanceLong);
+
+        onPacketSend(PacketType.Play.Server.LOGIN, (event) -> {
+            PacketContainer packet = event.getPacket();
+
+            if (rng.nextLong() < chanceLong)
+                // the "is hardcore" boolean https://wiki.vg/Protocol#Login_.28play.29
+                packet.getBooleans().write(0, true);
+        });
+    }
+}
diff --git a/src/main/java/eu/m724/tweaks/killswitch/KillswitchManager.java b/src/main/java/eu/m724/tweaks/killswitch/KillswitchModule.java
similarity index 82%
rename from src/main/java/eu/m724/tweaks/killswitch/KillswitchManager.java
rename to src/main/java/eu/m724/tweaks/killswitch/KillswitchModule.java
index c16f75d..b511ab9 100644
--- a/src/main/java/eu/m724/tweaks/killswitch/KillswitchManager.java
+++ b/src/main/java/eu/m724/tweaks/killswitch/KillswitchModule.java
@@ -10,13 +10,10 @@ import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpHandler;
 import com.sun.net.httpserver.HttpServer;
 import eu.m724.tweaks.DebugLogger;
-import eu.m724.tweaks.TweaksConfig;
-import eu.m724.tweaks.TweaksPlugin;
+import eu.m724.tweaks.TweaksModule;
 import org.bukkit.command.Command;
 import org.bukkit.command.CommandExecutor;
 import org.bukkit.command.CommandSender;
-import org.bukkit.command.PluginCommand;
-import org.bukkit.plugin.Plugin;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.File;
@@ -27,17 +24,12 @@ import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.util.Base64;
 
-public class KillswitchManager implements CommandExecutor, HttpHandler {
-    private final Plugin plugin;
+public class KillswitchModule extends TweaksModule implements CommandExecutor, HttpHandler {
     private final Ratelimit ratelimit = new Ratelimit();
 
     private byte[] secret;
     private String secretEncoded;
 
-    public KillswitchManager(Plugin plugin) {
-        this.plugin = plugin;
-    }
-
     private void loadKey(File file) {
         if (file.exists()) {
             try {
@@ -63,15 +55,16 @@ public class KillswitchManager implements CommandExecutor, HttpHandler {
         this.secretEncoded = Base64.getEncoder().encodeToString(secret);
     }
 
-    public void init(PluginCommand serverKillCommand) {
-        serverKillCommand.setExecutor(this);
+    @Override
+    protected void onInit() {
+        registerCommand("servkill", this);
 
-        if (TweaksConfig.getConfig().killswitchListen() != null) {
-            loadKey(new File(plugin.getDataFolder(), "storage/killswitch key"));
+        if (getConfig().killswitchListen() != null) {
+            loadKey(new File(getPlugin().getDataFolder(), "storage/killswitch key"));
 
-            ratelimit.runTaskTimerAsynchronously(plugin, 0, 20 * 300);
+            ratelimit.runTaskTimerAsynchronously(getPlugin(), 0, 20 * 300);
 
-            var listenAddress = TweaksConfig.getConfig().killswitchListen().split(":");
+            var listenAddress = getConfig().killswitchListen().split(":");
             InetSocketAddress bindAddress;
             if (listenAddress.length == 1) {
                 bindAddress = new InetSocketAddress(Integer.parseInt(listenAddress[0]));
diff --git a/src/main/java/eu/m724/tweaks/knockback/KnockbackListener.java b/src/main/java/eu/m724/tweaks/knockback/KnockbackModule.java
similarity index 85%
rename from src/main/java/eu/m724/tweaks/knockback/KnockbackListener.java
rename to src/main/java/eu/m724/tweaks/knockback/KnockbackModule.java
index 3239d5c..6b56ffb 100644
--- a/src/main/java/eu/m724/tweaks/knockback/KnockbackListener.java
+++ b/src/main/java/eu/m724/tweaks/knockback/KnockbackModule.java
@@ -7,22 +7,22 @@
 package eu.m724.tweaks.knockback;
 
 import eu.m724.tweaks.DebugLogger;
-import eu.m724.tweaks.TweaksConfig;
+import eu.m724.tweaks.TweaksModule;
 import org.bukkit.entity.EntityType;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
 import org.bukkit.event.entity.EntityKnockbackByEntityEvent;
-import org.bukkit.plugin.Plugin;
 import org.bukkit.util.Vector;
 
 import java.util.HashMap;
 import java.util.Map;
 
-public class KnockbackListener implements Listener {
+public class KnockbackModule extends TweaksModule implements Listener {
     private final Map<EntityType, Vector> modifiers = new HashMap<>();
 
-    public KnockbackListener(Plugin plugin) {
-        TweaksConfig.getConfig().knockbackModifiers().forEach((k, v) -> {
+    @Override
+    protected void onInit() {
+        getConfig().knockbackModifiers().forEach((k, v) -> {
             EntityType type;
             double mod;
 
@@ -50,7 +50,7 @@ public class KnockbackListener implements Listener {
         });
 
         if (!modifiers.isEmpty())
-            plugin.getServer().getPluginManager().registerEvents(this, plugin);
+            registerEvents(this);
     }
 
     @EventHandler
diff --git a/src/main/java/eu/m724/tweaks/motd/MotdManager.java b/src/main/java/eu/m724/tweaks/motd/MotdModule.java
similarity index 54%
rename from src/main/java/eu/m724/tweaks/motd/MotdManager.java
rename to src/main/java/eu/m724/tweaks/motd/MotdModule.java
index 0ac9c79..03043fb 100644
--- a/src/main/java/eu/m724/tweaks/motd/MotdManager.java
+++ b/src/main/java/eu/m724/tweaks/motd/MotdModule.java
@@ -7,7 +7,6 @@
 package eu.m724.tweaks.motd;
 
 import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
 import com.comphenix.protocol.events.*;
 import com.comphenix.protocol.reflect.StructureModifier;
 import com.google.gson.JsonElement;
@@ -15,14 +14,13 @@ import com.google.gson.JsonParseException;
 import com.google.gson.JsonParser;
 import eu.m724.tweaks.DebugLogger;
 import eu.m724.tweaks.TweaksConfig;
-import eu.m724.tweaks.TweaksPlugin;
+import eu.m724.tweaks.TweaksModule;
 import net.md_5.bungee.api.chat.TextComponent;
 import net.md_5.bungee.chat.ComponentSerializer;
 import net.minecraft.SharedConstants;
 import net.minecraft.core.RegistryAccess;
 import net.minecraft.network.chat.Component;
 import net.minecraft.network.protocol.status.ServerStatus;
-import org.bukkit.plugin.Plugin;
 
 import java.io.File;
 import java.io.IOException;
@@ -31,34 +29,29 @@ import java.util.Arrays;
 import java.util.Optional;
 import java.util.concurrent.ThreadLocalRandom;
 
-public class MotdManager {
-    private final TweaksPlugin plugin;
-
+public class MotdModule extends TweaksModule {
     private Component[] motds;
 
-    public MotdManager(TweaksPlugin plugin) {
-        this.plugin = plugin;
-    }
-
-    public void init() {
+    @Override
+    protected void onInit() {
         // TODO adding more MOTD features would require checking whether to enable set
 
         String motdSetName = TweaksConfig.getConfig().motdSet();
         String motdSetPath = "motd sets/" + motdSetName + ".txt";
-        File motdSetsFile = new File(plugin.getDataFolder(), motdSetPath);
+        File motdSetsFile = new File(getPlugin().getDataFolder(), motdSetPath);
 
         // create "motd sets" directory
         motdSetsFile.getParentFile().mkdirs();
 
         // if this is a builtin set
-        if (!motdSetsFile.exists() && plugin.hasResource(motdSetPath))
-            plugin.saveResource(motdSetPath, false);
+        if (!motdSetsFile.exists() && getPlugin().hasResource(motdSetPath))
+            getPlugin().saveResource(motdSetPath, false);
 
         if (!motdSetsFile.exists()) {
             throw new RuntimeException("MOTD set \"%s\" doesn't exist".formatted(motdSetName));
         }
 
-        String fileContent = null;
+        String fileContent;
         try {
             fileContent = Files.readString(motdSetsFile.toPath());
         } catch (IOException e) {
@@ -73,9 +66,9 @@ public class MotdManager {
 
                     try {
                         json = JsonParser.parseString(entry);
-                        DebugLogger.fine("JSON line: " + entry);
+                        DebugLogger.finer("JSON line: %s...", entry.substring(0, Math.min(entry.length(), 10)));
                     } catch (JsonParseException e) {
-                        DebugLogger.fine("Not a JSON line: " + entry);
+                        DebugLogger.finer("JSON line: %s...", entry.substring(0, Math.min(entry.length(), 10)));
                     }
 
                     if (json == null) {
@@ -86,40 +79,29 @@ public class MotdManager {
                 })
                 .toArray(Component[]::new);
 
-        registerListener(plugin);
-    }
+        onPacketSend(PacketType.Status.Server.SERVER_INFO, (event) -> {
+            PacketContainer packet = event.getPacket();
 
-    private void registerListener(Plugin plugin) {
-        ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
-                plugin,
-                ListenerPriority.NORMAL,
-                PacketType.Status.Server.SERVER_INFO
-        ) {
-            @Override
-            public void onPacketSending(PacketEvent event) {
-                PacketContainer packet = event.getPacket();
+            Component motd = motds[ThreadLocalRandom.current().nextInt(motds.length)];
 
-                Component motd = motds[ThreadLocalRandom.current().nextInt(motds.length)];
+            ServerStatus serverStatus = (ServerStatus) packet.getStructures().read(0).getHandle();
 
-                ServerStatus serverStatus = (ServerStatus) packet.getStructures().read(0).getHandle();
+            /* this:
+             * removes server mod prefix (Paper, Spigot, any brand)
+             * hides players
+             */
+            ServerStatus newStatus = new ServerStatus(
+                    motd,
+                    Optional.empty(),
+                    Optional.of(new ServerStatus.Version(
+                            SharedConstants.getCurrentVersion().getName(),
+                            SharedConstants.getProtocolVersion()
+                    )),
+                    serverStatus.favicon(),
+                    false
+            );
 
-                /* this:
-                 * removes server mod prefix (Paper, Spigot, any brand)
-                 * hides players
-                 */
-                ServerStatus newStatus = new ServerStatus(
-                        motd,
-                        Optional.empty(),
-                        Optional.of(new ServerStatus.Version(
-                                SharedConstants.getCurrentVersion().getName(),
-                                SharedConstants.getProtocolVersion()
-                        )),
-                        serverStatus.favicon(),
-                        false
-                );
-
-                packet.getStructures().write(0, new InternalStructure(newStatus, new StructureModifier<>(ServerStatus.class)));
-            }
+            packet.getStructures().write(0, new InternalStructure(newStatus, new StructureModifier<>(ServerStatus.class)));
         });
     }
 }
diff --git a/src/main/java/eu/m724/tweaks/ping/PingChecker.java b/src/main/java/eu/m724/tweaks/ping/PingChecker.java
index 41e95ab..e43ef04 100644
--- a/src/main/java/eu/m724/tweaks/ping/PingChecker.java
+++ b/src/main/java/eu/m724/tweaks/ping/PingChecker.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
@@ -9,8 +9,6 @@ package eu.m724.tweaks.ping;
 import org.bukkit.command.PluginCommand;
 import org.bukkit.plugin.Plugin;
 
-import java.util.Objects;
-
 public class PingChecker {
     private final Plugin plugin;
 
diff --git a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroManager.java b/src/main/java/eu/m724/tweaks/pomodoro/PomodoroManager.java
deleted file mode 100644
index 0483042..0000000
--- a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroManager.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2024  Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.pomodoro;
-
-import org.bukkit.command.PluginCommand;
-import org.bukkit.plugin.Plugin;
-
-public class PomodoroManager {
-    private final Plugin plugin;
-
-    public PomodoroManager(Plugin plugin) {
-        this.plugin = plugin;
-    }
-
-    public void init(PluginCommand pomodoroCommand) {
-        plugin.getServer().getPluginManager().registerEvents(new PomodoroListener(), plugin);
-        new PomodoroRunnable(plugin).runTaskTimerAsynchronously(plugin, 0, 20L);
-
-        pomodoroCommand.setExecutor(new PomodoroCommands());
-    }
-}
diff --git a/src/main/java/eu/m724/tweaks/pomodoro/PomodoroModule.java b/src/main/java/eu/m724/tweaks/pomodoro/PomodoroModule.java
new file mode 100644
index 0000000..564e638
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/pomodoro/PomodoroModule.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025  Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.pomodoro;
+
+import eu.m724.tweaks.TweaksModule;
+
+public class PomodoroModule extends TweaksModule {
+    @Override
+    protected void onInit() {
+        registerEvents(new PomodoroListener());
+        new PomodoroRunnable(getPlugin()).runTaskTimerAsynchronously(getPlugin(), 0, 20L);
+
+        registerCommand("pomodoro", new PomodoroCommands());
+    }
+}
diff --git a/src/main/java/eu/m724/tweaks/redstone/RedstoneManager.java b/src/main/java/eu/m724/tweaks/redstone/RedstoneModule.java
similarity index 76%
rename from src/main/java/eu/m724/tweaks/redstone/RedstoneManager.java
rename to src/main/java/eu/m724/tweaks/redstone/RedstoneModule.java
index 2dde5bc..30cd13f 100644
--- a/src/main/java/eu/m724/tweaks/redstone/RedstoneManager.java
+++ b/src/main/java/eu/m724/tweaks/redstone/RedstoneModule.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
@@ -8,36 +8,29 @@ package eu.m724.tweaks.redstone;
 
 import eu.m724.tweaks.DebugLogger;
 import eu.m724.tweaks.TweaksConfig;
-import org.bukkit.command.PluginCommand;
-import org.bukkit.plugin.Plugin;
+import eu.m724.tweaks.TweaksModule;
 
 import java.io.IOException;
 import java.net.*;
 import java.util.concurrent.Executors;
 import java.util.function.Consumer;
 
-public class RedstoneManager {
-    private final Plugin plugin;
-    private final RedstoneGateways redstoneGateways;
+public class RedstoneModule extends TweaksModule {
+    private final RedstoneGateways redstoneGateways = new RedstoneGateways(getPlugin());
 
     private DatagramSocket socket;
     private RedstoneStateUpdateRunnable runnable;
 
+    @Override
+    protected void onInit() {
+        RedstoneStore.init(getPlugin());
+        var gatewayItem = new GatewayItem(getPlugin());
 
-    public RedstoneManager(Plugin plugin) {
-        this.plugin = plugin;
-        this.redstoneGateways = new RedstoneGateways(plugin);
-    }
-
-    public void init(PluginCommand command) {
-        RedstoneStore.init(plugin);
-        var gatewayItem = new GatewayItem(plugin);
-
-        plugin.getServer().getPluginManager().registerEvents(new RedstoneListener(redstoneGateways, gatewayItem), plugin);
-        command.setExecutor(new RedstoneCommands(gatewayItem));
+        registerEvents(new RedstoneListener(redstoneGateways, gatewayItem));
+        registerCommand("retstone", new RedstoneCommands(gatewayItem));
 
         this.runnable = new RedstoneStateUpdateRunnable(redstoneGateways);
-        this.runnable.runTaskTimer(plugin, 0, 20); // TODO configurable
+        this.runnable.runTaskTimer(getPlugin(), 0, 20); // TODO configurable
 
         var listenAddress = TweaksConfig.getConfig().redstoneListen().split(":");
         InetSocketAddress bindAddress;
@@ -52,7 +45,6 @@ public class RedstoneManager {
         } catch (SocketException e) {
             throw new RuntimeException("Starting socket", e);
         }
-
     }
 
     private void initSocket(SocketAddress bindAddress) throws SocketException {
@@ -96,12 +88,12 @@ public class RedstoneManager {
     }
 
     private void enqueueUpdate(int gatewayId, byte power) {
-        DebugLogger.fine("Update enqueued " + gatewayId + " " + power);
+        DebugLogger.finer("Update enqueued " + gatewayId + " " + power);
         runnable.enqueueUpdate(gatewayId, power);
     }
 
     private void enqueueRetrieve(int gatewayId, Consumer<Byte> consumer) {
-        DebugLogger.fine("Retrieve enqueued " + gatewayId);
+        DebugLogger.finer("Retrieve enqueued " + gatewayId);
         runnable.enqueueRetrieve(gatewayId, consumer);
     }
 }
diff --git a/src/main/java/eu/m724/tweaks/sleep/SleepManager.java b/src/main/java/eu/m724/tweaks/sleep/SleepManager.java
deleted file mode 100644
index 2aec5cf..0000000
--- a/src/main/java/eu/m724/tweaks/sleep/SleepManager.java
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright (C) 2024  Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.sleep;
-
-import eu.m724.tweaks.TweaksConfig;
-import org.bukkit.plugin.Plugin;
-
-public class SleepManager {
-    public void init(Plugin plugin) {
-        plugin.getServer().getPluginManager().registerEvents(new SleepListener(), plugin);
-        if (!TweaksConfig.getConfig().sleepInstant())
-            new TimeForwardRunnable(plugin).runTaskTimer(plugin, 0, 1); // TODO maybe not
-    }
-}
diff --git a/src/main/java/eu/m724/tweaks/sleep/SleepModule.java b/src/main/java/eu/m724/tweaks/sleep/SleepModule.java
new file mode 100644
index 0000000..d467338
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/sleep/SleepModule.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025  Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.sleep;
+
+import eu.m724.tweaks.TweaksModule;
+
+public class SleepModule extends TweaksModule {
+    @Override
+    protected void onInit() {
+        registerEvents(new SleepListener());
+
+        if (!getConfig().sleepInstant())
+            new TimeForwardRunnable(getPlugin()).runTaskTimer(getPlugin(), 0, 1); // TODO maybe not
+    }
+}
diff --git a/src/main/java/eu/m724/tweaks/swing/SwingManager.java b/src/main/java/eu/m724/tweaks/swing/SwingModule.java
similarity index 85%
rename from src/main/java/eu/m724/tweaks/swing/SwingManager.java
rename to src/main/java/eu/m724/tweaks/swing/SwingModule.java
index b3cd2cb..fdd2465 100644
--- a/src/main/java/eu/m724/tweaks/swing/SwingManager.java
+++ b/src/main/java/eu/m724/tweaks/swing/SwingModule.java
@@ -7,6 +7,7 @@
 package eu.m724.tweaks.swing;
 
 import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.TweaksModule;
 import org.bukkit.Material;
 import org.bukkit.attribute.Attribute;
 import org.bukkit.entity.Entity;
@@ -14,29 +15,23 @@ import org.bukkit.entity.LivingEntity;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
 import org.bukkit.event.block.BlockBreakEvent;
-import org.bukkit.plugin.Plugin;
 
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
 
-public class SwingManager implements Listener {
-    private final Plugin plugin;
-
+public class SwingModule extends TweaksModule implements Listener {
     private final Set<Material> tools = new HashSet<>();
 
-    public SwingManager(Plugin plugin) {
-        this.plugin = plugin;
-
+    @Override
+    protected void onInit() {
         Arrays.stream(Material.values())
                 .filter(m -> m.name().contains("SWORD"))
                 .forEach(tools::add);
 
-        DebugLogger.fine("Tools: " + tools.size());
-    }
+        DebugLogger.finer("Tools: " + tools.size());
 
-    public void init() {
-        plugin.getServer().getPluginManager().registerEvents(this, plugin);
+        registerEvents(this);
     }
 
     @EventHandler
diff --git a/src/main/java/eu/m724/tweaks/updater/UpdateChecker.java b/src/main/java/eu/m724/tweaks/updater/UpdateChecker.java
index 03dfd1f..2f4d4d5 100644
--- a/src/main/java/eu/m724/tweaks/updater/UpdateChecker.java
+++ b/src/main/java/eu/m724/tweaks/updater/UpdateChecker.java
@@ -11,6 +11,7 @@ import eu.m724.tweaks.Language;
 import eu.m724.tweaks.updater.cache.VersionedResource;
 import org.bukkit.scheduler.BukkitRunnable;
 
+import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.time.LocalTime;
@@ -24,13 +25,15 @@ import java.util.stream.Collectors;
 
 public class UpdateChecker extends BukkitRunnable {
     private final Logger logger;
+    private final File cacheFile;
     private final Set<VersionedResource> resources;
 
     static final Set<VersionedResource> availableUpdates = new HashSet<>();
     static LocalTime lastChecked = null;
 
-    UpdateChecker(Logger logger, Set<VersionedResource> resources) {
+    UpdateChecker(Logger logger, File cacheFile, Set<VersionedResource> resources) {
         this.logger = logger;
+        this.cacheFile = cacheFile;
         this.resources = resources; // TODO make a copy?
     }
 
@@ -79,7 +82,9 @@ public class UpdateChecker extends BukkitRunnable {
     public void run() {
         checkAll();
 
-        try (FileOutputStream outputStream = new FileOutputStream(UpdaterManager.cacheFile)) {
+        DebugLogger.finer("Done checking, now saving");
+        cacheFile.getParentFile().mkdirs();
+        try (FileOutputStream outputStream = new FileOutputStream(cacheFile)) {
             VersionCheckCache.writeAll(outputStream, resources.stream().map(VersionedResource::running).filter(Objects::nonNull).collect(Collectors.toSet()));
         } catch (IOException e) {
             throw new RuntimeException(e);
diff --git a/src/main/java/eu/m724/tweaks/updater/UpdaterManager.java b/src/main/java/eu/m724/tweaks/updater/UpdaterModule.java
similarity index 68%
rename from src/main/java/eu/m724/tweaks/updater/UpdaterManager.java
rename to src/main/java/eu/m724/tweaks/updater/UpdaterModule.java
index b540a23..7df2351 100644
--- a/src/main/java/eu/m724/tweaks/updater/UpdaterManager.java
+++ b/src/main/java/eu/m724/tweaks/updater/UpdaterModule.java
@@ -7,11 +7,10 @@
 package eu.m724.tweaks.updater;
 
 import eu.m724.tweaks.DebugLogger;
+import eu.m724.tweaks.TweaksModule;
 import eu.m724.tweaks.updater.cache.ResourceVersion;
 import eu.m724.tweaks.updater.cache.SpigotResource;
 import eu.m724.tweaks.updater.cache.VersionedResource;
-import org.bukkit.command.PluginCommand;
-import org.bukkit.plugin.Plugin;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -21,28 +20,21 @@ import java.util.HashSet;
 import java.util.Set;
 import java.util.stream.Collectors;
 
-public class UpdaterManager {
-    static File cacheFile;
-
-    private final Plugin plugin;
-
-    public UpdaterManager(Plugin plugin) {
-        this.plugin = plugin;
-        cacheFile = new File(plugin.getDataFolder(), "storage/cache/updater");
-    }
-
-    public void init(PluginCommand updatesCommand){
+public class UpdaterModule extends TweaksModule {
+    @Override
+    protected void onInit() {
         // scan installed plugins
-        Set<SpigotResource> resources = null;
+        Set<SpigotResource> resources;
         try {
-            resources = new PluginScanner(plugin).load();
+            resources = new PluginScanner(getPlugin()).load();
         } catch (IOException e) {
             throw new RuntimeException("Loading plugins", e);
         }
 
-        cacheFile.getParentFile().mkdirs(); // TODO move this somewhere else
 
         // load installed versions from cache
+        var cacheFile = new File(getPlugin().getDataFolder(), "storage/cache/updater");
+
         Set<ResourceVersion> installedVersions;
         try (FileInputStream inputStream = new FileInputStream(cacheFile)) {
             installedVersions = VersionCheckCache.loadAll(inputStream);
@@ -60,9 +52,9 @@ public class UpdaterManager {
                 )).collect(Collectors.toSet());
 
 
-        new UpdateChecker(plugin.getLogger(), versionedResources)
-                .runTaskTimerAsynchronously(plugin, 600, 12 * 3600 * 20);
+        new UpdateChecker(getPlugin().getLogger(), cacheFile, versionedResources)
+                .runTaskTimerAsynchronously(getPlugin(), 600, 12 * 3600 * 20); // 12 hours
 
-        updatesCommand.setExecutor(new UpdaterCommands());
+        registerCommand("updates", new UpdaterCommands());
     }
 }
diff --git a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderExpander.java b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderExpandModule.java
similarity index 78%
rename from src/main/java/eu/m724/tweaks/worldborder/WorldBorderExpander.java
rename to src/main/java/eu/m724/tweaks/worldborder/WorldBorderExpandModule.java
index 14eae1b..d4e4ecf 100644
--- a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderExpander.java
+++ b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderExpandModule.java
@@ -1,24 +1,25 @@
 /*
- * Copyright (C) 2024  Minecon724
+ * Copyright (C) 2025  Minecon724
  * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
  * in the project root for the full license text.
  */
 
 package eu.m724.tweaks.worldborder;
 
+import eu.m724.tweaks.TweaksModule;
 import net.minecraft.server.level.ServerLevel;
 import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
 import org.bukkit.event.world.WorldLoadEvent;
-import org.bukkit.plugin.Plugin;
 
-public class WorldBorderExpander implements Listener {
-    public void init(Plugin plugin) {
-        plugin.getServer().getPluginManager().registerEvents(this, plugin);
+public class WorldBorderExpandModule extends TweaksModule implements Listener {
+    @Override
+    protected void onInit() {
+        registerEvents(this);
 
         // because the plugin loads "post world"
-        plugin.getServer().getWorlds().forEach(w -> {
+        getPlugin().getServer().getWorlds().forEach(w -> {
             onWorldLoad(new WorldLoadEvent(w));
         });
     }
diff --git a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderHideModule.java b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderHideModule.java
new file mode 100644
index 0000000..0400534
--- /dev/null
+++ b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderHideModule.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2025  Minecon724
+ * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
+ * in the project root for the full license text.
+ */
+
+package eu.m724.tweaks.worldborder;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.events.PacketContainer;
+import eu.m724.tweaks.TweaksModule;
+
+import java.nio.ByteBuffer;
+
+public class WorldBorderHideModule extends TweaksModule {
+    private static final int EXTENSION_RADIUS = 512;
+
+    @Override
+    protected void onInit() {
+        getPlugin().getServer().getMessenger().registerOutgoingPluginChannel(getPlugin(), "tweaks724:worldborder");
+
+        byte[] infoArray = ByteBuffer.allocate(4).putInt(EXTENSION_RADIUS).array();
+
+        onPacketSend(PacketType.Play.Server.INITIALIZE_BORDER, (event) -> {
+            PacketContainer packet = event.getPacket();
+            // old diameter
+            packet.getDoubles().write(2, packet.getDoubles().read(2) + EXTENSION_RADIUS * 2);
+            // new diameter
+            packet.getDoubles().write(3, packet.getDoubles().read(3) + EXTENSION_RADIUS * 2);
+
+            // border radius
+            packet.getIntegers().write(0, packet.getIntegers().read(0) + EXTENSION_RADIUS);
+            // warning distance
+            packet.getIntegers().write(1, packet.getIntegers().read(1) + EXTENSION_RADIUS);
+
+            event.getPlayer().sendPluginMessage(getPlugin(), "tweaks724:worldborder", infoArray);
+        });
+
+        onPacketSend(PacketType.Play.Server.SET_BORDER_SIZE, (event) -> {
+            PacketContainer packet = event.getPacket();
+            // diameter
+            packet.getDoubles().write(0, packet.getDoubles().read(0) + EXTENSION_RADIUS * 2);
+        });
+
+        onPacketSend(PacketType.Play.Server.SET_BORDER_WARNING_DISTANCE, (event) -> {
+            PacketContainer packet = event.getPacket();
+            // warning distance
+            packet.getIntegers().write(0, packet.getIntegers().read(0) + EXTENSION_RADIUS);
+        });
+    }
+}
diff --git a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderHider.java b/src/main/java/eu/m724/tweaks/worldborder/WorldBorderHider.java
deleted file mode 100644
index 06b3491..0000000
--- a/src/main/java/eu/m724/tweaks/worldborder/WorldBorderHider.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2024  Minecon724
- * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file
- * in the project root for the full license text.
- */
-
-package eu.m724.tweaks.worldborder;
-
-import com.comphenix.protocol.PacketType;
-import com.comphenix.protocol.ProtocolLibrary;
-import com.comphenix.protocol.events.ListenerPriority;
-import com.comphenix.protocol.events.PacketAdapter;
-import com.comphenix.protocol.events.PacketContainer;
-import com.comphenix.protocol.events.PacketEvent;
-import org.bukkit.plugin.Plugin;
-
-import java.nio.ByteBuffer;
-
-public class WorldBorderHider {
-    private static final int EXTENSION_RADIUS = 512;
-
-    public void init(Plugin plugin) {
-        plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, "tweaks724:worldborder");
-        byte[] infoArray = ByteBuffer.allocate(4).putInt(EXTENSION_RADIUS).array();
-
-        ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
-                plugin,
-                ListenerPriority.NORMAL,
-                PacketType.Play.Server.INITIALIZE_BORDER
-        ) {
-            @Override
-            public void onPacketSending(PacketEvent event) {
-                PacketContainer packet = event.getPacket();
-                // old diameter
-                packet.getDoubles().write(2, packet.getDoubles().read(2) + EXTENSION_RADIUS * 2);
-                // new diameter
-                packet.getDoubles().write(3, packet.getDoubles().read(3) + EXTENSION_RADIUS * 2);
-
-                // border radius
-                packet.getIntegers().write(0, packet.getIntegers().read(0) + EXTENSION_RADIUS);
-                // warning distance
-                packet.getIntegers().write(1, packet.getIntegers().read(1) + EXTENSION_RADIUS);
-
-                event.getPlayer().sendPluginMessage(plugin, "tweaks724:worldborder", infoArray);
-            }
-        });
-
-        ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
-                plugin,
-                ListenerPriority.NORMAL,
-                PacketType.Play.Server.SET_BORDER_SIZE
-        ) {
-            @Override
-            public void onPacketSending(PacketEvent event) {
-                PacketContainer packet = event.getPacket();
-                // diameter
-                packet.getDoubles().write(0, packet.getDoubles().read(0) + EXTENSION_RADIUS * 2);
-            }
-        });
-
-        ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
-                plugin,
-                ListenerPriority.NORMAL,
-                PacketType.Play.Server.SET_BORDER_WARNING_DISTANCE
-        ) {
-            @Override
-            public void onPacketSending(PacketEvent event) {
-                PacketContainer packet = event.getPacket();
-                // warning distance
-                packet.getIntegers().write(0, packet.getIntegers().read(0) + EXTENSION_RADIUS);
-            }
-        });
-    }
-}