From d88f146abb2e3cbf842f588a421e8c6358c49c07 Mon Sep 17 00:00:00 2001
From: Lyfts <127234178+Lyfts@users.noreply.github.com>
Date: Fri, 20 Dec 2024 17:47:32 +0100
Subject: [PATCH] make notifications individually configurable

Signed-off-by: Lyfts <127234178+Lyfts@users.noreply.github.com>
---
 dependencies.gradle                           |   2 +-
 .../java/serverutils/ServerUtilities.java     |   8 +-
 .../ServerUtilitiesNotifications.java         | 132 +++++---
 .../client/EnumNotificationLocation.java      |  16 -
 .../client/NotificationHandler.java           | 294 ++++++++++++++++++
 .../client/ServerUtilitiesClient.java         |   2 +-
 .../client/ServerUtilitiesClientConfig.java   |   8 -
 .../client/gui/GuiClientConfig.java           |  55 +++-
 .../java/serverutils/command/CmdBackup.java   |   3 +-
 .../serverutils/command/chunks/CmdClaim.java  |  23 +-
 .../command/chunks/CmdClaimAs.java            |  26 +-
 .../serverutils/command/chunks/CmdLoad.java   |  13 +-
 .../command/chunks/CmdUnclaim.java            |  13 +-
 .../command/chunks/CmdUnclaimAll.java         |  10 +-
 .../serverutils/command/chunks/CmdUnload.java |  13 +-
 .../command/chunks/CmdUnloadAll.java          |   7 +-
 .../java/serverutils/command/tp/CmdHome.java  |   5 +-
 .../java/serverutils/command/tp/CmdWarp.java  |   6 +-
 .../data/ServerUtilitiesPlayerData.java       |  16 +-
 .../ServerUtilitiesClientEventHandler.java    | 207 +-----------
 .../ServerUtilitiesServerEventHandler.java    |  28 +-
 .../lib/command/CmdEditConfigBase.java        |  20 +-
 .../lib/data/ServerUtilitiesAPI.java          |   5 +-
 .../serverutils/lib/util/ServerUtils.java     |  17 +-
 .../java/serverutils/lib/util/SidedUtils.java |  10 +-
 .../serverutils/lib/util/StringUtils.java     |   7 +
 .../util/text_components/Notification.java    |  15 +-
 .../net/MessageClaimedChunksModify.java       |   6 +-
 .../serverutils/net/MessageNotification.java  |   4 +-
 .../java/serverutils/task/CleanupTask.java    |  25 +-
 .../java/serverutils/task/NotifyTask.java     |   4 +-
 .../java/serverutils/task/ShutdownTask.java   |  18 +-
 .../java/serverutils/task/TeleportTask.java   |  12 +-
 .../serverutils/task/backup/BackupTask.java   |   6 +-
 .../serverutils/task/backup/ThreadBackup.java |  27 +-
 .../assets/serverutilities/lang/en_US.lang    |  28 ++
 .../serverutilities/textures/icons/bell2.png  | Bin 0 -> 1530 bytes
 37 files changed, 602 insertions(+), 489 deletions(-)
 delete mode 100644 src/main/java/serverutils/client/EnumNotificationLocation.java
 create mode 100644 src/main/java/serverutils/client/NotificationHandler.java
 create mode 100644 src/main/resources/assets/serverutilities/textures/icons/bell2.png

diff --git a/dependencies.gradle b/dependencies.gradle
index a52bb64a1..fdd749550 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -2,7 +2,7 @@
 
 dependencies {
 
-    api("com.github.GTNewHorizons:GTNHLib:0.5.22:dev")
+    api("com.github.GTNewHorizons:GTNHLib:0.6.0:dev")
     compileOnly("com.github.GTNewHorizons:NotEnoughItems:2.7.0-GTNH:dev")
     compileOnly("com.github.GTNewHorizons:EnderIO:2.8.22:dev")
     compileOnly("com.github.GTNewHorizons:Navigator:1.0.15:dev")
diff --git a/src/main/java/serverutils/ServerUtilities.java b/src/main/java/serverutils/ServerUtilities.java
index 3d8db9732..6fb303fc6 100644
--- a/src/main/java/serverutils/ServerUtilities.java
+++ b/src/main/java/serverutils/ServerUtilities.java
@@ -7,6 +7,7 @@
 
 import net.minecraft.command.CommandException;
 import net.minecraft.command.ICommandSender;
+import net.minecraft.util.ChatComponentTranslation;
 import net.minecraft.util.IChatComponent;
 
 import org.apache.logging.log4j.LogManager;
@@ -24,7 +25,6 @@
 import cpw.mods.fml.common.network.NetworkCheckHandler;
 import cpw.mods.fml.relauncher.Side;
 import serverutils.lib.command.CommandUtils;
-import serverutils.lib.util.SidedUtils;
 
 @Mod(
         modid = ServerUtilities.MOD_ID,
@@ -49,7 +49,11 @@ public class ServerUtilities {
     public static ServerUtilitiesCommon PROXY;
 
     public static IChatComponent lang(@Nullable ICommandSender sender, String key, Object... args) {
-        return SidedUtils.lang(sender, MOD_ID, key, args);
+        return lang(key, args);
+    }
+
+    public static IChatComponent lang(String key, Object... args) {
+        return new ChatComponentTranslation(key, args);
     }
 
     public static CommandException error(@Nullable ICommandSender sender, String key, Object... args) {
diff --git a/src/main/java/serverutils/ServerUtilitiesNotifications.java b/src/main/java/serverutils/ServerUtilitiesNotifications.java
index b2d5dc9af..e17a3875e 100644
--- a/src/main/java/serverutils/ServerUtilitiesNotifications.java
+++ b/src/main/java/serverutils/ServerUtilitiesNotifications.java
@@ -1,54 +1,74 @@
 package serverutils;
 
+import static serverutils.lib.EnumMessageLocation.ACTION_BAR;
+import static serverutils.lib.EnumMessageLocation.CHAT;
+
+import javax.annotation.Nullable;
+
+import net.minecraft.entity.player.EntityPlayer;
 import net.minecraft.entity.player.EntityPlayerMP;
-import net.minecraft.server.MinecraftServer;
 import net.minecraft.util.ChatComponentText;
-import net.minecraft.util.ChatComponentTranslation;
 import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
 import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.StatCollector;
 
 import serverutils.data.ClaimedChunk;
 import serverutils.data.ClaimedChunks;
 import serverutils.data.ServerUtilitiesPlayerData;
+import serverutils.lib.EnumMessageLocation;
 import serverutils.lib.data.ForgeTeam;
 import serverutils.lib.math.ChunkDimPos;
-import serverutils.lib.util.ServerUtils;
 import serverutils.lib.util.StringUtils;
 import serverutils.lib.util.text_components.Notification;
 
-public class ServerUtilitiesNotifications {
-
-    public static final ResourceLocation CHUNK_MODIFIED = new ResourceLocation(
-            ServerUtilities.MOD_ID,
-            "chunk_modified");
-    public static final ResourceLocation CHUNK_CHANGED = new ResourceLocation(ServerUtilities.MOD_ID, "chunk_changed");
-    public static final ResourceLocation CHUNK_CANT_CLAIM = new ResourceLocation(
-            ServerUtilities.MOD_ID,
-            "cant_claim_chunk");
-    public static final ResourceLocation UNCLAIMED_ALL = new ResourceLocation(ServerUtilities.MOD_ID, "unclaimed_all");
-    public static final String TELEPORT = "teleport";
-    public static final ResourceLocation TELEPORT_WARMUP = new ResourceLocation(
-            ServerUtilities.MOD_ID,
-            "teleport_warmup");
-    public static final ResourceLocation RELOAD_SERVER = new ResourceLocation(ServerUtilities.MOD_ID, "reload_server");
-    public static final ResourceLocation BACKUP_START = new ResourceLocation(ServerUtilities.MOD_ID, "backup_start");
-    public static final ResourceLocation BACKUP_END1 = new ResourceLocation(ServerUtilities.MOD_ID, "backup_end1");
-    public static final ResourceLocation BACKUP_END2 = new ResourceLocation(ServerUtilities.MOD_ID, "backup_end2");
-    public static final ResourceLocation CONFIG_CHANGED = new ResourceLocation(
-            ServerUtilities.MOD_ID,
-            "config_changed");
-    public static final String RESTART_TIMER_ID = "restart_timer";
-
-    public static final Notification NO_TEAM = Notification.of(
-            new ResourceLocation(ServerUtilities.MOD_ID, "no_team"),
-            new ChatComponentTranslation("serverutilities.lang.team.error.no_team")).setError();
-
-    public static void sendCantModifyChunk(MinecraftServer server, EntityPlayerMP player) {
-        Notification
-                .of(
-                        new ResourceLocation(ServerUtilities.MOD_ID, "cant_modify_chunk"),
-                        ServerUtilities.lang(player, "serverutilities.lang.chunks.cant_modify_chunk"))
-                .setError().send(server, player);
+public enum ServerUtilitiesNotifications {
+
+    CHUNK_MODIFIED("chunk_modified", ACTION_BAR),
+    CHUNK_CHANGED("chunk_changed", ACTION_BAR),
+    CANT_MODIFY_CHUNK("cant_modify_chunk", ACTION_BAR),
+    TELEPORT("teleport", ACTION_BAR),
+    TELEPORT_WARMUP("teleport_warmup", ACTION_BAR),
+    BACKUP("backup", ACTION_BAR),
+    CONFIG_CHANGED("config_changed", ACTION_BAR),
+    RESTART_TIMER("restart_timer", ACTION_BAR),
+    CLEANUP("cleanup", ACTION_BAR),
+    PLAYER_AFK("player_afk", CHAT);
+
+    public static final ServerUtilitiesNotifications[] VALUES = values();
+
+    private final String id;
+    private final String desc;
+    private EnumMessageLocation location;
+
+    ServerUtilitiesNotifications(String id, EnumMessageLocation defaultLocation) {
+        this.id = id;
+        this.desc = StatCollector.translateToLocal(ServerUtilities.MOD_ID + ".notifications." + id + ".desc");
+        this.location = defaultLocation;
+    }
+
+    public Notification createNotification(IChatComponent component) {
+        return Notification.of(id, component);
+    }
+
+    public Notification createNotification(String key, Object... args) {
+        return createNotification(ServerUtilities.lang(key, args));
+    }
+
+    public void send(EntityPlayer player, String key, Object... args) {
+        createNotification(key, args).send(player);
+    }
+
+    public void send(EntityPlayer player, IChatComponent component) {
+        createNotification(component).send(player);
+    }
+
+    public void sendAll(String key, Object... args) {
+        createNotification(key, args).sendToAll();
+    }
+
+    public void sendAll(IChatComponent component) {
+        createNotification(component).sendToAll();
     }
 
     public static void updateChunkMessage(EntityPlayerMP player, ChunkDimPos pos) {
@@ -68,29 +88,43 @@ public static void updateChunkMessage(EntityPlayerMP player, ChunkDimPos pos) {
             }
 
             if (team != null) {
-                Notification notification = Notification.of(CHUNK_CHANGED, team.getTitle());
+                Notification notification = CHUNK_CHANGED.createNotification(team.getTitle());
 
                 if (!team.getDesc().isEmpty()) {
                     notification.addLine(StringUtils.italic(new ChatComponentText(team.getDesc()), true));
                 }
 
-                notification.send(player.mcServer, player);
+                notification.send(player);
             } else {
-                Notification.of(
-                        CHUNK_CHANGED,
-                        StringUtils.color(
-                                ServerUtilities.lang(player, "serverutilities.lang.chunks.wilderness"),
-                                EnumChatFormatting.DARK_GREEN))
-                        .send(player.mcServer, player);
+                CHUNK_CHANGED.send(
+                        player,
+                        StringUtils.color("serverutilities.lang.chunks.wilderness", EnumChatFormatting.DARK_GREEN));
             }
         }
     }
 
-    public static void backupNotification(ResourceLocation id, String key, Object... args) {
-        if (!ServerUtilitiesConfig.backups.silent_backup) {
-            Notification
-                    .of(id, StringUtils.color(ServerUtilities.lang(null, key, args), EnumChatFormatting.LIGHT_PURPLE))
-                    .send(ServerUtils.getServer(), null);
+    public String getId() {
+        return id;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    public void setLocation(EnumMessageLocation enabled) {
+        this.location = enabled;
+    }
+
+    public EnumMessageLocation getLocation() {
+        return location;
+    }
+
+    public static @Nullable ServerUtilitiesNotifications getFromId(ResourceLocation id) {
+        for (ServerUtilitiesNotifications n : VALUES) {
+            if (n.id.equalsIgnoreCase(id.getResourcePath())) {
+                return n;
+            }
         }
+        return null;
     }
 }
diff --git a/src/main/java/serverutils/client/EnumNotificationLocation.java b/src/main/java/serverutils/client/EnumNotificationLocation.java
deleted file mode 100644
index fb275abd6..000000000
--- a/src/main/java/serverutils/client/EnumNotificationLocation.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package serverutils.client;
-
-public enum EnumNotificationLocation {
-
-    DISABLED,
-    CHAT,
-    SCREEN;
-
-    public boolean chat() {
-        return this == CHAT;
-    }
-
-    public boolean disabled() {
-        return this == DISABLED;
-    }
-}
diff --git a/src/main/java/serverutils/client/NotificationHandler.java b/src/main/java/serverutils/client/NotificationHandler.java
new file mode 100644
index 000000000..abe38d7a2
--- /dev/null
+++ b/src/main/java/serverutils/client/NotificationHandler.java
@@ -0,0 +1,294 @@
+package serverutils.client;
+
+import static serverutils.client.ServerUtilitiesClient.CLIENT_FOLDER;
+
+import java.io.File;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.gui.ScaledResolution;
+import net.minecraft.command.ICommandSender;
+import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.ChatComponentTranslation;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.IChatComponent;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.StatCollector;
+import net.minecraftforge.client.event.RenderGameOverlayEvent;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber;
+
+import cpw.mods.fml.common.eventhandler.EventPriority;
+import cpw.mods.fml.common.eventhandler.SubscribeEvent;
+import cpw.mods.fml.common.gameevent.TickEvent;
+import cpw.mods.fml.relauncher.Side;
+import serverutils.ServerUtilities;
+import serverutils.ServerUtilitiesNotifications;
+import serverutils.lib.EnumMessageLocation;
+import serverutils.lib.client.GlStateManager;
+import serverutils.lib.config.ConfigGroup;
+import serverutils.lib.config.ConfigString;
+import serverutils.lib.config.ConfigValueInstance;
+import serverutils.lib.util.JsonUtils;
+import serverutils.lib.util.text_components.Notification;
+
+@EventBusSubscriber(side = Side.CLIENT)
+public class NotificationHandler {
+
+    private static final Deque<Notification> NOTIFICATIONS = new ArrayDeque<>();
+    private static final Map<String, IChatComponent> lastMessages = new HashMap<>();
+    private static ConfigGroup notificationConfig;
+    private static NotificationWidget currentNotification;
+
+    public static void onNotify(IChatComponent component) {
+        Minecraft mc = Minecraft.getMinecraft();
+        if (!(component instanceof Notification notification)) {
+            mc.thePlayer.addChatMessage(component);
+            return;
+        }
+
+        ResourceLocation id = notification.getId();
+        ServerUtilitiesNotifications notificationType = ServerUtilitiesNotifications.getFromId(id);
+        EnumMessageLocation location = (notificationType == null || notification.isImportant())
+                ? EnumMessageLocation.ACTION_BAR
+                : notificationType.getLocation();
+
+        if (location == EnumMessageLocation.OFF) return;
+
+        lastMessages.put(id.getResourcePath(), notification);
+        if (location == EnumMessageLocation.CHAT) {
+            mc.thePlayer.addChatMessage(component);
+            return;
+        }
+
+        if (notification.isVanilla()) {
+            mc.ingameGUI.func_110326_a(component.getFormattedText(), false);
+            return;
+        }
+
+        NOTIFICATIONS.remove(notification);
+        if (currentNotification != null && currentNotification.id.equals(id)) {
+            currentNotification = null;
+        }
+
+        NOTIFICATIONS.add(notification);
+    }
+
+    public static void updateDescription(ConfigValueInstance inst) {
+        IChatComponent notification = lastMessages.get(inst.getId());
+        if (notification == null) return;
+        IChatComponent info = inst.getInfo();
+        if (info == null) return;
+        info.getSiblings().clear();
+        info.appendText("\n" + StatCollector.translateToLocal("serverutilities.notifications.last_received") + "\n")
+                .appendSibling(notification);
+    }
+
+    static void loadNotifications() {
+        JsonElement file = JsonUtils.fromJson(new File(CLIENT_FOLDER + "notifications.json"));
+        if (JsonUtils.isNull(file)) return;
+
+        ConfigGroup group = getNotificationConfig();
+        for (Map.Entry<String, JsonElement> entry : file.getAsJsonObject().entrySet()) {
+            if (!entry.getValue().isJsonObject()) continue;
+
+            JsonObject obj = entry.getValue().getAsJsonObject();
+            group.getValue(entry.getKey())
+                    .setValueFromString(null, obj.getAsJsonPrimitive("location").getAsString().toLowerCase(), false);
+            if (obj.has("lastReceived")) {
+                IChatComponent last = JsonUtils.deserializeTextComponent(obj.getAsJsonObject("lastReceived"));
+                lastMessages.put(entry.getKey(), last);
+            }
+        }
+    }
+
+    public static void saveConfig(ConfigGroup group, ICommandSender sender) {
+        JsonObject json = new JsonObject();
+        for (ServerUtilitiesNotifications notification : ServerUtilitiesNotifications.VALUES) {
+            JsonObject obj = new JsonObject();
+            obj.add("location", new JsonPrimitive(notification.getLocation().name()));
+            IChatComponent last = lastMessages.get(notification.getId());
+            if (last != null) {
+                obj.add("lastReceived", JsonUtils.serializeTextComponent(last));
+            }
+
+            json.add(notification.getId(), obj);
+        }
+
+        JsonUtils.toJsonSafe(new File(CLIENT_FOLDER + "notifications.json"), json);
+    }
+
+    public static ConfigGroup getNotificationConfig() {
+        if (notificationConfig != null) return notificationConfig;
+
+        notificationConfig = ConfigGroup.newGroup("notifications")
+                .setDisplayName(ServerUtilities.lang("serverutilities.notifications.config"));
+        for (ServerUtilitiesNotifications notification : ServerUtilitiesNotifications.values()) {
+            notificationConfig
+                    .addEnum(
+                            notification.getId(),
+                            notification::getLocation,
+                            notification::setLocation,
+                            EnumMessageLocation.NAME_MAP)
+                    .setDefaultValue(new ConfigString(notification.getLocation().name()))
+                    .setDisplayName(
+                            new ChatComponentTranslation("serverutilities.notifications." + notification.getId()))
+                    .setInfo(new ChatComponentText(notification.getDesc()));
+        }
+
+        return notificationConfig;
+    }
+
+    @SubscribeEvent
+    public static void onClientTick(TickEvent.ClientTickEvent event) {
+        if (event.phase == TickEvent.Phase.START) {
+            if (Minecraft.getMinecraft().theWorld == null) {
+                currentNotification = null;
+                NOTIFICATIONS.clear();
+            }
+
+            if (currentNotification != null) {
+                if (currentNotification.tick()) {
+                    currentNotification = null;
+                }
+            }
+
+            if (currentNotification == null && !NOTIFICATIONS.isEmpty()) {
+                currentNotification = new NotificationWidget(NOTIFICATIONS.pop());
+            }
+        }
+    }
+
+    @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true)
+    public static void onGameOverlayRender(RenderGameOverlayEvent.Text event) {
+        if (currentNotification != null && !currentNotification.isImportant()) {
+            currentNotification.render(event.resolution, event.partialTicks);
+            GlStateManager.color(1F, 1F, 1F, 1F);
+            GlStateManager.disableLighting();
+            GlStateManager.enableBlend();
+            GlStateManager.enableTexture2D();
+        }
+    }
+
+    @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true)
+    public static void onRenderTick(TickEvent.RenderTickEvent event) {
+        if (currentNotification != null && currentNotification.isImportant()) {
+            Minecraft mc = Minecraft.getMinecraft();
+            currentNotification
+                    .render(new ScaledResolution(mc, mc.displayWidth, mc.displayHeight), event.renderTickTime);
+            GlStateManager.color(1F, 1F, 1F, 1F);
+            GlStateManager.disableLighting();
+            GlStateManager.enableBlend();
+            GlStateManager.enableTexture2D();
+        }
+    }
+
+    private static class NotificationWidget {
+
+        private static final FontRenderer font = Minecraft.getMinecraft().fontRenderer;
+        public final Notification notification;
+        public final ResourceLocation id;
+        public final List<String> text;
+        public int width, height;
+        public final long timer;
+        private long tick, endTick;
+
+        public NotificationWidget(Notification n) {
+            notification = n;
+            id = n.getId();
+            width = 0;
+            text = new ArrayList<>();
+            timer = n.getTimer().ticks();
+            tick = endTick = -1L;
+
+            String s0;
+
+            try {
+                s0 = notification.getFormattedText();
+            } catch (Exception ex) {
+                s0 = EnumChatFormatting.RED + ex.toString();
+            }
+
+            Minecraft mc = Minecraft.getMinecraft();
+            for (String s : font.listFormattedStringToWidth(
+                    s0,
+                    new ScaledResolution(mc, mc.displayWidth, mc.displayHeight).getScaledWidth())) {
+                for (String line : s.split("\n")) {
+                    if (!line.isEmpty()) {
+                        line = line.trim();
+                        text.add(line);
+                        width = Math.max(width, font.getStringWidth(line));
+                    }
+                }
+            }
+
+            width += 4;
+            height = text.size() * 11;
+
+            if (text.isEmpty()) {
+                width = 20;
+                height = 20;
+            }
+        }
+
+        public void render(ScaledResolution screen, float partialTicks) {
+            if (tick == -1L || tick >= endTick) {
+                return;
+            }
+
+            int alpha = (int) Math.min(255F, (endTick - tick - partialTicks) * 255F / 20F);
+
+            if (alpha <= 2) {
+                return;
+            }
+
+            GlStateManager.pushMatrix();
+            GlStateManager.disableDepth();
+            GlStateManager.depthMask(false);
+            GlStateManager.disableLighting();
+            GlStateManager.enableBlend();
+            GlStateManager.color(1F, 1F, 1F, 1F);
+
+            int width = screen.getScaledWidth() / 2;
+            int height = screen.getScaledHeight() - 67;
+            int offy = (text.size() * 11) / 2;
+
+            for (int i = 0; i < text.size(); i++) {
+                String string = text.get(i);
+                font.drawStringWithShadow(
+                        string,
+                        width - font.getStringWidth(string) / 2,
+                        height - offy + i * 11,
+                        0xFFFFFF | (alpha << 24));
+            }
+
+            GlStateManager.depthMask(true);
+            GlStateManager.color(1F, 1F, 1F, 1F);
+            GlStateManager.enableLighting();
+            GlStateManager.popMatrix();
+            GlStateManager.enableDepth();
+        }
+
+        private boolean tick() {
+            tick = Minecraft.getMinecraft().theWorld.getTotalWorldTime();
+
+            if (endTick == -1L) {
+                endTick = tick + timer;
+            }
+            return tick >= endTick || Math.min(255F, (endTick - tick) * 255F / 20F) <= 2F;
+        }
+
+        private boolean isImportant() {
+            return notification.isImportant();
+        }
+    }
+}
diff --git a/src/main/java/serverutils/client/ServerUtilitiesClient.java b/src/main/java/serverutils/client/ServerUtilitiesClient.java
index 417617197..f85c471f6 100644
--- a/src/main/java/serverutils/client/ServerUtilitiesClient.java
+++ b/src/main/java/serverutils/client/ServerUtilitiesClient.java
@@ -43,7 +43,6 @@ public class ServerUtilitiesClient extends ServerUtilitiesCommon {
     public static KeyBinding KEY_NBT, KEY_TRASH;
     public static final String KEY_CATEGORY = "key.categories.serverutilities";
     public static final String CLIENT_FOLDER = ServerUtilities.MOD_ID + "/client/";
-
     static {
         try {
             ConfigurationManager.registerConfig(ServerUtilitiesClientConfig.class);
@@ -73,6 +72,7 @@ public void preInit(FMLPreInitializationEvent event) {
     @Override
     public void postInit(FMLPostInitializationEvent event) {
         super.postInit(event);
+        NotificationHandler.loadNotifications();
 
         for (Map.Entry<String, String> entry : ServerUtilitiesCommon.KAOMOJIS.entrySet()) {
             ClientCommandHandler.instance.registerCommand(new CommandKaomoji(entry.getKey(), entry.getValue()));
diff --git a/src/main/java/serverutils/client/ServerUtilitiesClientConfig.java b/src/main/java/serverutils/client/ServerUtilitiesClientConfig.java
index 3e6b46c74..5d9066415 100644
--- a/src/main/java/serverutils/client/ServerUtilitiesClientConfig.java
+++ b/src/main/java/serverutils/client/ServerUtilitiesClientConfig.java
@@ -36,14 +36,6 @@ public class ServerUtilitiesClientConfig {
     @Config.DefaultEnum("GROUPED")
     public static EnumPlacement sidebar_placement;
 
-    @Config.Comment("""
-            SCREEN: Receive notifications as normal above the hotbar.
-            CHAT: Convert all non-important notifications to chat messages.
-            DISABLED: Disable non-important notifications entirely.""")
-    @Config.LangKey("serverutilities_client.notification_location")
-    @Config.DefaultEnum("SCREEN")
-    public static EnumNotificationLocation notifications;
-
     @Config.Comment("Draw dotted lines on loaded chunks to improve noticeability.")
     @Config.LangKey("serverutilities_client.show_dotted_lines")
     @Config.DefaultBoolean(true)
diff --git a/src/main/java/serverutils/client/gui/GuiClientConfig.java b/src/main/java/serverutils/client/gui/GuiClientConfig.java
index 96cc1491f..c41a6c393 100644
--- a/src/main/java/serverutils/client/gui/GuiClientConfig.java
+++ b/src/main/java/serverutils/client/gui/GuiClientConfig.java
@@ -9,14 +9,20 @@
 import com.gtnewhorizon.gtnhlib.config.SimpleGuiConfig;
 
 import serverutils.ServerUtilities;
+import serverutils.client.NotificationHandler;
 import serverutils.client.ServerUtilitiesClientConfig;
+import serverutils.lib.EnumMessageLocation;
 import serverutils.lib.client.ClientUtils;
+import serverutils.lib.config.ConfigEnum;
+import serverutils.lib.config.ConfigValueInstance;
 import serverutils.lib.gui.GuiHelper;
 import serverutils.lib.gui.GuiIcons;
 import serverutils.lib.gui.Panel;
+import serverutils.lib.gui.SimpleButton;
 import serverutils.lib.gui.SimpleTextButton;
 import serverutils.lib.gui.WidgetType;
 import serverutils.lib.gui.misc.GuiButtonListBase;
+import serverutils.lib.gui.misc.GuiEditConfig;
 import serverutils.lib.gui.misc.GuiLoading;
 import serverutils.lib.icon.Icon;
 import serverutils.lib.icon.ItemIcon;
@@ -72,7 +78,18 @@ public void onClicked(MouseButton button) {
                         new GuiSidebarButtonConfig().openGui();
                     }
                 });
+        panel.add(
+                new SimpleTextButton(
+                        panel,
+                        StatCollector.translateToLocal("serverutilities.notifications.config"),
+                        Icon.getIcon("serverutilities:textures/icons/bell2.png")) {
 
+                    @Override
+                    public void onClicked(MouseButton button) {
+                        GuiHelper.playClickSound();
+                        new GuiNotificationConfig().openGui();
+                    }
+                });
         panel.add(
                 new SimpleTextButton(
                         panel,
@@ -89,7 +106,6 @@ public void onClicked(MouseButton button) {
                         }
                     }
                 });
-
         panel.add(
                 new SimpleTextButton(
                         panel,
@@ -116,4 +132,41 @@ public void onClosed() {
         super.onClosed();
         SidebarButtonManager.INSTANCE.saveConfig();
     }
+
+    @SuppressWarnings("unchecked")
+    public static class GuiNotificationConfig extends GuiEditConfig {
+
+        private final SimpleButton toggleAllButton;
+
+        public GuiNotificationConfig() {
+            super(NotificationHandler.getNotificationConfig(), NotificationHandler::saveConfig);
+            toggleAllButton = new SimpleButton(this, "Toggle All", GuiIcons.REFRESH, (widget, button) -> {
+                for (ConfigValueInstance inst : group.getValues()) {
+                    ConfigEnum<EnumMessageLocation> value = (ConfigEnum<EnumMessageLocation>) inst.getValue();
+                    value.onClicked(widget.getGui(), inst, button, () -> {});
+                    widget.getGui().initGui();
+                }
+            });
+        }
+
+        @Override
+        public void onPostInit() {
+            for (ConfigValueInstance inst : group.getValues()) {
+                NotificationHandler.updateDescription(inst);
+            }
+            super.onPostInit();
+        }
+
+        @Override
+        public void addWidgets() {
+            super.addWidgets();
+            add(toggleAllButton);
+        }
+
+        @Override
+        public void alignWidgets() {
+            super.alignWidgets();
+            toggleAllButton.setPos(width - 58, 2);
+        }
+    }
 }
diff --git a/src/main/java/serverutils/command/CmdBackup.java b/src/main/java/serverutils/command/CmdBackup.java
index d6bbd0c31..b029db278 100644
--- a/src/main/java/serverutils/command/CmdBackup.java
+++ b/src/main/java/serverutils/command/CmdBackup.java
@@ -29,8 +29,7 @@ public void processCommand(ICommandSender sender, String[] args) {
             BackupTask task = new BackupTask(sender, args.length == 0 ? "" : args[0]);
             if (BackupTask.thread == null) {
                 task.execute(Universe.get());
-                sender.addChatMessage(
-                        ServerUtilities.lang(null, "cmd.backup_manual_launch", sender.getCommandSenderName()));
+                sender.addChatMessage(ServerUtilities.lang("cmd.backup_manual_launch", sender.getCommandSenderName()));
             } else {
                 sender.addChatMessage(ServerUtilities.lang(sender, "cmd.backup_already_running"));
             }
diff --git a/src/main/java/serverutils/command/chunks/CmdClaim.java b/src/main/java/serverutils/command/chunks/CmdClaim.java
index 47709c445..301eb959e 100644
--- a/src/main/java/serverutils/command/chunks/CmdClaim.java
+++ b/src/main/java/serverutils/command/chunks/CmdClaim.java
@@ -1,5 +1,8 @@
 package serverutils.command.chunks;
 
+import static serverutils.ServerUtilitiesNotifications.CANT_MODIFY_CHUNK;
+import static serverutils.ServerUtilitiesNotifications.CHUNK_MODIFIED;
+
 import net.minecraft.command.CommandException;
 import net.minecraft.command.ICommandSender;
 import net.minecraft.entity.player.EntityPlayerMP;
@@ -12,7 +15,6 @@
 import serverutils.lib.command.CommandUtils;
 import serverutils.lib.data.ForgePlayer;
 import serverutils.lib.math.ChunkDimPos;
-import serverutils.lib.util.text_components.Notification;
 
 public class CmdClaim extends CmdBase {
 
@@ -32,30 +34,25 @@ public void processCommand(ICommandSender sender, String[] args) throws CommandE
 
         if (!player.getUniqueID().equals(p.getId())
                 && !ClaimedChunks.instance.canPlayerModify(p, pos, ServerUtilitiesPermissions.CLAIMS_OTHER_CLAIM)) {
-            ServerUtilitiesNotifications.sendCantModifyChunk(player.mcServer, player);
+            CANT_MODIFY_CHUNK.createNotification("serverutilities.lang.chunks.cant_modify_chunk").setError()
+                    .send(player);
             return;
         }
 
         switch (ClaimedChunks.instance.claimChunk(p, pos)) {
             case SUCCESS:
-                Notification
-                        .of(
-                                ServerUtilitiesNotifications.CHUNK_MODIFIED,
-                                ServerUtilities.lang(player, "serverutilities.lang.chunks.chunk_claimed"))
-                        .send(player.mcServer, player);
+                CHUNK_MODIFIED.send(player, "serverutilities.lang.chunks.chunk_claimed");
                 ServerUtilitiesNotifications.updateChunkMessage(player, pos);
                 break;
             case DIMENSION_BLOCKED:
-                Notification
-                        .of(
-                                ServerUtilitiesNotifications.CHUNK_CANT_CLAIM,
-                                ServerUtilities.lang(player, "serverutilities.lang.chunks.claiming_not_enabled_dim"))
-                        .setError().send(player.mcServer, player);
+                CANT_MODIFY_CHUNK.createNotification("serverutilities.lang.chunks.claiming_not_enabled_dim").setError()
+                        .send(player);
                 break;
             case NO_POWER:
                 break;
             default:
-                ServerUtilitiesNotifications.sendCantModifyChunk(player.mcServer, player);
+                CANT_MODIFY_CHUNK.createNotification("serverutilities.lang.chunks.cant_modify_chunk").setError()
+                        .send(player);
                 break;
         }
     }
diff --git a/src/main/java/serverutils/command/chunks/CmdClaimAs.java b/src/main/java/serverutils/command/chunks/CmdClaimAs.java
index 2fd89265b..02a50dd98 100644
--- a/src/main/java/serverutils/command/chunks/CmdClaimAs.java
+++ b/src/main/java/serverutils/command/chunks/CmdClaimAs.java
@@ -1,5 +1,8 @@
 package serverutils.command.chunks;
 
+import static serverutils.ServerUtilitiesNotifications.CANT_MODIFY_CHUNK;
+import static serverutils.ServerUtilitiesNotifications.CHUNK_MODIFIED;
+
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
@@ -19,7 +22,6 @@
 import serverutils.lib.data.ForgeTeam;
 import serverutils.lib.data.Universe;
 import serverutils.lib.math.ChunkDimPos;
-import serverutils.lib.util.text_components.Notification;
 
 public class CmdClaimAs extends CmdBase {
 
@@ -81,24 +83,18 @@ public void processCommand(ICommandSender sender, String[] args) throws CommandE
                 if (x == 0 && z == 0) {
                     switch (result) {
                         case SUCCESS:
-                            Notification
-                                    .of(
-                                            ServerUtilitiesNotifications.CHUNK_MODIFIED,
-                                            ServerUtilities.lang(player, "serverutilities.lang.chunks.chunk_claimed"))
-                                    .send(player.mcServer, player);
-                            ServerUtilitiesNotifications.updateChunkMessage(player, pos1);
+                            CHUNK_MODIFIED.send(player, "serverutilities.lang.chunks.chunk_claimed");
+                            ServerUtilitiesNotifications.updateChunkMessage(player, pos);
                             break;
                         case DIMENSION_BLOCKED:
-                            Notification
-                                    .of(
-                                            ServerUtilitiesNotifications.CHUNK_CANT_CLAIM,
-                                            ServerUtilities.lang(
-                                                    player,
-                                                    "serverutilities.lang.chunks.claiming_not_enabled_dim"))
-                                    .setError().send(player.mcServer, player);
+                            CANT_MODIFY_CHUNK.createNotification("serverutilities.lang.chunks.claiming_not_enabled_dim")
+                                    .setError().send(player);
+                            break;
+                        case NO_POWER:
                             break;
                         default:
-                            ServerUtilitiesNotifications.sendCantModifyChunk(player.mcServer, player);
+                            CANT_MODIFY_CHUNK.createNotification("serverutilities.lang.chunks.cant_modify_chunk")
+                                    .setError().send(player);
                             break;
                     }
                 }
diff --git a/src/main/java/serverutils/command/chunks/CmdLoad.java b/src/main/java/serverutils/command/chunks/CmdLoad.java
index db9f15466..0201264a1 100644
--- a/src/main/java/serverutils/command/chunks/CmdLoad.java
+++ b/src/main/java/serverutils/command/chunks/CmdLoad.java
@@ -1,5 +1,8 @@
 package serverutils.command.chunks;
 
+import static serverutils.ServerUtilitiesNotifications.CANT_MODIFY_CHUNK;
+import static serverutils.ServerUtilitiesNotifications.CHUNK_MODIFIED;
+
 import net.minecraft.command.CommandException;
 import net.minecraft.command.ICommandSender;
 import net.minecraft.entity.player.EntityPlayerMP;
@@ -12,7 +15,6 @@
 import serverutils.lib.command.CommandUtils;
 import serverutils.lib.data.ForgePlayer;
 import serverutils.lib.math.ChunkDimPos;
-import serverutils.lib.util.text_components.Notification;
 
 public class CmdLoad extends CmdBase {
 
@@ -32,14 +34,11 @@ public void processCommand(ICommandSender sender, String[] args) throws CommandE
 
         if (p.hasTeam() && ClaimedChunks.instance.canPlayerModify(p, pos, ServerUtilitiesPermissions.CLAIMS_OTHER_LOAD)
                 && ClaimedChunks.instance.loadChunk(p, p.team, pos)) {
-            Notification
-                    .of(
-                            ServerUtilitiesNotifications.CHUNK_MODIFIED,
-                            ServerUtilities.lang(player, "serverutilities.lang.chunks.chunk_loaded"))
-                    .send(player.mcServer, player);
+            CHUNK_MODIFIED.send(player, "serverutilities.lang.chunks.chunk_loaded");
             ServerUtilitiesNotifications.updateChunkMessage(player, pos);
         } else {
-            ServerUtilitiesNotifications.sendCantModifyChunk(player.mcServer, player);
+            CANT_MODIFY_CHUNK.createNotification("serverutilities.lang.chunks.cant_modify_chunk").setError()
+                    .send(player);
         }
     }
 }
diff --git a/src/main/java/serverutils/command/chunks/CmdUnclaim.java b/src/main/java/serverutils/command/chunks/CmdUnclaim.java
index 9cc3b981d..cea4d37a9 100644
--- a/src/main/java/serverutils/command/chunks/CmdUnclaim.java
+++ b/src/main/java/serverutils/command/chunks/CmdUnclaim.java
@@ -1,5 +1,8 @@
 package serverutils.command.chunks;
 
+import static serverutils.ServerUtilitiesNotifications.CANT_MODIFY_CHUNK;
+import static serverutils.ServerUtilitiesNotifications.CHUNK_MODIFIED;
+
 import net.minecraft.command.CommandException;
 import net.minecraft.command.ICommandSender;
 import net.minecraft.entity.player.EntityPlayerMP;
@@ -12,7 +15,6 @@
 import serverutils.lib.command.CommandUtils;
 import serverutils.lib.data.ForgePlayer;
 import serverutils.lib.math.ChunkDimPos;
-import serverutils.lib.util.text_components.Notification;
 
 public class CmdUnclaim extends CmdBase {
 
@@ -32,14 +34,11 @@ public void processCommand(ICommandSender sender, String[] args) throws CommandE
 
         if (ClaimedChunks.instance.canPlayerModify(p, pos, ServerUtilitiesPermissions.CLAIMS_OTHER_UNCLAIM)
                 && ClaimedChunks.instance.unclaimChunk(p, pos)) {
-            Notification
-                    .of(
-                            ServerUtilitiesNotifications.CHUNK_MODIFIED,
-                            ServerUtilities.lang(player, "serverutilities.lang.chunks.chunk_unclaimed"))
-                    .send(player.mcServer, player);
+            CHUNK_MODIFIED.send(player, "serverutilities.lang.chunks.chunk_unclaimed");
             ServerUtilitiesNotifications.updateChunkMessage(player, pos);
         } else {
-            ServerUtilitiesNotifications.sendCantModifyChunk(player.mcServer, player);
+            CANT_MODIFY_CHUNK.createNotification("serverutilities.lang.chunks.cant_modify_chunk").setError()
+                    .send(player);
         }
     }
 }
diff --git a/src/main/java/serverutils/command/chunks/CmdUnclaimAll.java b/src/main/java/serverutils/command/chunks/CmdUnclaimAll.java
index 980810edc..1936335e8 100644
--- a/src/main/java/serverutils/command/chunks/CmdUnclaimAll.java
+++ b/src/main/java/serverutils/command/chunks/CmdUnclaimAll.java
@@ -1,5 +1,7 @@
 package serverutils.command.chunks;
 
+import static serverutils.ServerUtilitiesNotifications.CHUNK_MODIFIED;
+
 import java.util.List;
 import java.util.OptionalInt;
 
@@ -8,13 +10,11 @@
 import net.minecraft.entity.player.EntityPlayerMP;
 
 import serverutils.ServerUtilities;
-import serverutils.ServerUtilitiesNotifications;
 import serverutils.ServerUtilitiesPermissions;
 import serverutils.data.ClaimedChunks;
 import serverutils.lib.command.CmdBase;
 import serverutils.lib.command.CommandUtils;
 import serverutils.lib.data.ForgePlayer;
-import serverutils.lib.util.text_components.Notification;
 
 public class CmdUnclaimAll extends CmdBase {
 
@@ -48,11 +48,7 @@ public void processCommand(ICommandSender sender, String[] args) throws CommandE
         if (p.hasTeam()) {
             OptionalInt dimension = CommandUtils.parseDimension(sender, args, 0);
             ClaimedChunks.instance.unclaimAllChunks(p, p.team, dimension);
-            Notification
-                    .of(
-                            ServerUtilitiesNotifications.UNCLAIMED_ALL,
-                            ServerUtilities.lang(sender, "serverutilities.lang.chunks.unclaimed_all"))
-                    .send(player.mcServer, player);
+            CHUNK_MODIFIED.send(player, "serverutilities.lang.chunks.unclaimed_all");
         } else {
             throw ServerUtilities.error(sender, "serverutilities.lang.team.error.no_team");
         }
diff --git a/src/main/java/serverutils/command/chunks/CmdUnload.java b/src/main/java/serverutils/command/chunks/CmdUnload.java
index bb361d910..a579ef713 100644
--- a/src/main/java/serverutils/command/chunks/CmdUnload.java
+++ b/src/main/java/serverutils/command/chunks/CmdUnload.java
@@ -1,5 +1,8 @@
 package serverutils.command.chunks;
 
+import static serverutils.ServerUtilitiesNotifications.CANT_MODIFY_CHUNK;
+import static serverutils.ServerUtilitiesNotifications.CHUNK_MODIFIED;
+
 import net.minecraft.command.CommandException;
 import net.minecraft.command.ICommandSender;
 import net.minecraft.entity.player.EntityPlayerMP;
@@ -12,7 +15,6 @@
 import serverutils.lib.command.CommandUtils;
 import serverutils.lib.data.ForgePlayer;
 import serverutils.lib.math.ChunkDimPos;
-import serverutils.lib.util.text_components.Notification;
 
 public class CmdUnload extends CmdBase {
 
@@ -33,14 +35,11 @@ public void processCommand(ICommandSender sender, String[] args) throws CommandE
 
         if (ClaimedChunks.instance.canPlayerModify(p, pos, ServerUtilitiesPermissions.CLAIMS_OTHER_UNLOAD)
                 && ClaimedChunks.instance.unloadChunk(p, pos)) {
-            Notification
-                    .of(
-                            ServerUtilitiesNotifications.CHUNK_MODIFIED,
-                            ServerUtilities.lang(player, "serverutilities.lang.chunks.chunk_unloaded"))
-                    .send(player.mcServer, player);
+            CHUNK_MODIFIED.send(player, "serverutilities.lang.chunks.chunk_unloaded");
             ServerUtilitiesNotifications.updateChunkMessage(player, pos);
         } else {
-            ServerUtilitiesNotifications.sendCantModifyChunk(player.mcServer, player);
+            CANT_MODIFY_CHUNK.createNotification("serverutilities.lang.chunks.cant_modify_chunk").setError()
+                    .send(player);
         }
     }
 }
diff --git a/src/main/java/serverutils/command/chunks/CmdUnloadAll.java b/src/main/java/serverutils/command/chunks/CmdUnloadAll.java
index d8b8693bf..b38915f68 100644
--- a/src/main/java/serverutils/command/chunks/CmdUnloadAll.java
+++ b/src/main/java/serverutils/command/chunks/CmdUnloadAll.java
@@ -15,7 +15,6 @@
 import serverutils.lib.command.CmdBase;
 import serverutils.lib.command.CommandUtils;
 import serverutils.lib.data.ForgePlayer;
-import serverutils.lib.util.text_components.Notification;
 
 public class CmdUnloadAll extends CmdBase {
 
@@ -53,11 +52,7 @@ public void processCommand(ICommandSender sender, String[] args) throws CommandE
                 chunk.setLoaded(false);
             }
 
-            Notification
-                    .of(
-                            ServerUtilitiesNotifications.UNCLAIMED_ALL,
-                            ServerUtilities.lang(sender, "serverutilities.lang.chunks.unloaded_all"))
-                    .send(player.mcServer, player);
+            ServerUtilitiesNotifications.CHUNK_MODIFIED.send(player, "serverutilities.lang.chunks.unloaded_all");
         } else {
             throw ServerUtilities.error(sender, "serverutilities.lang.team.error.no_team");
         }
diff --git a/src/main/java/serverutils/command/tp/CmdHome.java b/src/main/java/serverutils/command/tp/CmdHome.java
index 880b3f8a2..d5b1950d1 100644
--- a/src/main/java/serverutils/command/tp/CmdHome.java
+++ b/src/main/java/serverutils/command/tp/CmdHome.java
@@ -23,7 +23,6 @@
 import serverutils.lib.data.Universe;
 import serverutils.lib.math.BlockDimPos;
 import serverutils.lib.util.permission.PermissionAPI;
-import serverutils.lib.util.text_components.Notification;
 import serverutils.task.NotifyTask;
 import serverutils.task.Task;
 
@@ -113,9 +112,7 @@ public void processCommand(ICommandSender sender, String[] args0) throws Command
             throw ServerUtilities.error(sender, "serverutilities.lang.homes.cross_dim");
         }
 
-        IChatComponent component = ServerUtilities.lang(sender, "serverutilities.lang.warps.tp", args[0]);
-        Notification notification = Notification.of(TELEPORT, component);
-        Task task = new NotifyTask(-1, player, notification);
+        Task task = new NotifyTask(-1, player, TELEPORT.createNotification("serverutilities.lang.warps.tp", args[0]));
         data.checkTeleportCooldown(sender, ServerUtilitiesPlayerData.Timer.HOME);
         ServerUtilitiesPlayerData.Timer.HOME.teleport(player, playerMP -> pos.teleporter(), task);
     }
diff --git a/src/main/java/serverutils/command/tp/CmdWarp.java b/src/main/java/serverutils/command/tp/CmdWarp.java
index 8371a0524..ee688f089 100644
--- a/src/main/java/serverutils/command/tp/CmdWarp.java
+++ b/src/main/java/serverutils/command/tp/CmdWarp.java
@@ -9,7 +9,6 @@
 import net.minecraft.command.ICommandSender;
 import net.minecraft.entity.player.EntityPlayerMP;
 import net.minecraft.util.ChatComponentText;
-import net.minecraft.util.IChatComponent;
 
 import cpw.mods.fml.common.eventhandler.Event;
 import serverutils.ServerUtilities;
@@ -21,7 +20,6 @@
 import serverutils.lib.math.BlockDimPos;
 import serverutils.lib.util.StringJoiner;
 import serverutils.lib.util.permission.PermissionAPI;
-import serverutils.lib.util.text_components.Notification;
 import serverutils.ranks.Rank;
 import serverutils.ranks.Ranks;
 import serverutils.task.NotifyTask;
@@ -76,9 +74,7 @@ public void processCommand(ICommandSender sender, String[] args) throws CommandE
             throw ServerUtilities.error(sender, "serverutilities.lang.warps.cross_dim");
         }
 
-        IChatComponent component = ServerUtilities.lang(sender, "serverutilities.lang.warps.tp", args[0]);
-        Notification notification = Notification.of(TELEPORT, component);
-        Task task = new NotifyTask(-1, player, notification);
+        Task task = new NotifyTask(-1, player, TELEPORT.createNotification("serverutilities.lang.warps.tp", args[0]));
         ServerUtilitiesPlayerData.Timer.WARP.teleport(player, playerMP -> p.teleporter(), task);
     }
 }
diff --git a/src/main/java/serverutils/data/ServerUtilitiesPlayerData.java b/src/main/java/serverutils/data/ServerUtilitiesPlayerData.java
index be802207d..7b50441e3 100644
--- a/src/main/java/serverutils/data/ServerUtilitiesPlayerData.java
+++ b/src/main/java/serverutils/data/ServerUtilitiesPlayerData.java
@@ -23,7 +23,6 @@
 import serverutils.ServerUtilitiesCommon;
 import serverutils.ServerUtilitiesConfig;
 import serverutils.ServerUtilitiesPermissions;
-import serverutils.lib.EnumMessageLocation;
 import serverutils.lib.config.ConfigGroup;
 import serverutils.lib.config.RankConfigAPI;
 import serverutils.lib.data.ForgePlayer;
@@ -34,7 +33,6 @@
 import serverutils.lib.util.NBTUtils;
 import serverutils.lib.util.ServerUtils;
 import serverutils.lib.util.StringUtils;
-import serverutils.lib.util.text_components.Notification;
 import serverutils.lib.util.text_components.TextComponentParser;
 import serverutils.net.MessageUpdateTabName;
 import serverutils.ranks.Ranks;
@@ -76,10 +74,9 @@ public void teleport(EntityPlayerMP player, Function<EntityPlayerMP, TeleporterD
 
             if (seconds > 0) {
                 IChatComponent component = StringUtils.color(
-                        ServerUtilities.lang(player, "stand_still", seconds).appendText(" [" + seconds + "]"),
+                        ServerUtilities.lang("stand_still", seconds).appendText(" [" + seconds + "]"),
                         EnumChatFormatting.GOLD);
-                Notification.of(TELEPORT_WARMUP, component).setVanilla(true).send(player.mcServer, player);
-
+                TELEPORT_WARMUP.createNotification(component).setVanilla(true).send(player);
                 universe.scheduleTask(new TeleportTask(teleportType, player, this, seconds, pos, extraTask));
             } else {
                 new TeleportTask(teleportType, player, this, 0, pos, extraTask).execute(universe);
@@ -94,7 +91,6 @@ public static ServerUtilitiesPlayerData get(ForgePlayer player) {
     private boolean enablePVP = true;
     private boolean showTeamPrefix = false;
     private String nickname = "";
-    private EnumMessageLocation afkMesageLocation = EnumMessageLocation.CHAT;
 
     public final Collection<ForgePlayer> tpaRequestsFrom;
     public long afkTime;
@@ -126,7 +122,6 @@ public NBTTagCompound serializeNBT() {
         nbt.setTag("Homes", homes.serializeNBT());
         nbt.setTag("TeleportTracker", teleportTracker.serializeNBT());
         nbt.setString("Nickname", nickname);
-        nbt.setString("AFK", EnumMessageLocation.NAME_MAP.getName(afkMesageLocation));
         return nbt;
     }
 
@@ -138,7 +133,6 @@ public void deserializeNBT(NBTTagCompound nbt) {
         teleportTracker.deserializeNBT(nbt.getCompoundTag("TeleportTracker"));
         setLastDeath(BlockDimPos.fromIntArray(nbt.getIntArray("LastDeath")), 0);
         nickname = nbt.getString("Nickname");
-        afkMesageLocation = EnumMessageLocation.NAME_MAP.get(nbt.getString("AFK"));
     }
 
     public void addConfig(ConfigGroup main) {
@@ -150,8 +144,6 @@ public void addConfig(ConfigGroup main) {
         config.addString("nickname", () -> nickname, v -> nickname = v, "").setCanEdit(
                 ServerUtilitiesConfig.commands.nick
                         && player.hasPermission(ServerUtilitiesPermissions.CHAT_NICKNAME_SET));
-        config.addEnum("afk", () -> afkMesageLocation, v -> afkMesageLocation = v, EnumMessageLocation.NAME_MAP)
-                .setExcluded(!ServerUtilitiesConfig.afk.isEnabled(player.team.universe.server));
         IChatComponent info = new ChatComponentTranslation(
                 "player_config.serverutilities.show_team_prefix.info",
                 player.team.getTitle());
@@ -173,10 +165,6 @@ public void setNickname(String name) {
         clearCache();
     }
 
-    public EnumMessageLocation getAFKMessageLocation() {
-        return afkMesageLocation;
-    }
-
     public void setLastDeath(@Nullable BlockDimPos pos) {
         setLastDeath(pos, MinecraftServer.getSystemTimeMillis());
     }
diff --git a/src/main/java/serverutils/handlers/ServerUtilitiesClientEventHandler.java b/src/main/java/serverutils/handlers/ServerUtilitiesClientEventHandler.java
index f464b8261..c1f759541 100644
--- a/src/main/java/serverutils/handlers/ServerUtilitiesClientEventHandler.java
+++ b/src/main/java/serverutils/handlers/ServerUtilitiesClientEventHandler.java
@@ -2,22 +2,16 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.LinkedHashMap;
 import java.util.List;
 
 import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.FontRenderer;
 import net.minecraft.client.gui.GuiButton;
 import net.minecraft.client.gui.GuiScreen;
-import net.minecraft.client.gui.ScaledResolution;
 import net.minecraft.client.gui.inventory.GuiContainer;
 import net.minecraft.client.resources.I18n;
 import net.minecraft.nbt.NBTTagCompound;
 import net.minecraft.stats.StatList;
 import net.minecraft.util.EnumChatFormatting;
-import net.minecraft.util.IChatComponent;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.client.event.ClientChatReceivedEvent;
 import net.minecraftforge.client.event.GuiScreenEvent;
 import net.minecraftforge.client.event.RenderGameOverlayEvent;
 import net.minecraftforge.event.entity.player.ItemTooltipEvent;
@@ -29,7 +23,6 @@
 import cpw.mods.fml.common.network.FMLNetworkEvent;
 import serverutils.ServerUtilities;
 import serverutils.ServerUtilitiesConfig;
-import serverutils.client.EnumNotificationLocation;
 import serverutils.client.ServerUtilitiesClient;
 import serverutils.client.ServerUtilitiesClientConfig;
 import serverutils.client.gui.GuiClaimedChunks;
@@ -40,7 +33,6 @@
 import serverutils.integration.navigator.NavigatorIntegration;
 import serverutils.lib.OtherMods;
 import serverutils.lib.client.ClientUtils;
-import serverutils.lib.client.GlStateManager;
 import serverutils.lib.gui.Widget;
 import serverutils.lib.icon.IconRenderer;
 import serverutils.lib.math.Ticks;
@@ -48,7 +40,6 @@
 import serverutils.lib.util.NBTUtils;
 import serverutils.lib.util.SidedUtils;
 import serverutils.lib.util.StringUtils;
-import serverutils.lib.util.text_components.Notification;
 import serverutils.net.MessageAdminPanelGui;
 import serverutils.net.MessageClaimedChunksUpdate;
 import serverutils.net.MessageEditNBTRequest;
@@ -58,7 +49,6 @@
 public class ServerUtilitiesClientEventHandler {
 
     public static final ServerUtilitiesClientEventHandler INST = new ServerUtilitiesClientEventHandler();
-    private static Temp currentNotification;
     public static boolean shouldRenderIcons = false;
     public static long shutdownTime = 0L;
 
@@ -191,40 +181,6 @@ public void onCustomClick(CustomClickEvent event) {
         }
     }
 
-    public void onNotify(IChatComponent component) {
-        boolean importantNotification = component instanceof Notification noti && noti.isImportant();
-
-        if (ServerUtilitiesClientConfig.notifications == EnumNotificationLocation.DISABLED && !importantNotification) {
-            return;
-        }
-
-        if (ServerUtilitiesClientConfig.notifications == EnumNotificationLocation.CHAT && !importantNotification) {
-            Minecraft.getMinecraft().thePlayer.addChatMessage(component);
-        } else if (component instanceof Notification notification) {
-            ResourceLocation id = notification.getId();
-
-            if (notification.isVanilla()) {
-                Minecraft.getMinecraft().ingameGUI.func_110326_a(component.getFormattedText(), false);
-                return;
-            }
-
-            Temp.MAP.remove(id);
-            if (currentNotification != null && currentNotification.widget.id.equals(id)) {
-                currentNotification = null;
-            }
-            Temp.MAP.put(id, notification);
-        }
-    }
-
-    @SubscribeEvent
-    public void onClientChatEvent(ClientChatReceivedEvent event) {
-        IChatComponent component = event.message;
-        if (component instanceof Notification notification) {
-            onNotify(notification);
-            event.message = null;
-        }
-    }
-
     @SubscribeEvent
     public void onTooltip(ItemTooltipEvent event) {
         if (ServerUtilitiesClientConfig.item_ore_names) {
@@ -259,29 +215,11 @@ public void onGuiInit(final GuiScreenEvent.InitGuiEvent.Post event) {
 
     @SubscribeEvent
     public void onClientTick(TickEvent.ClientTickEvent event) {
-        if (event.phase == TickEvent.Phase.START) {
-            if (Minecraft.getMinecraft().theWorld == null) {
-                currentNotification = null;
-                Temp.MAP.clear();
-            }
-
-            if (currentNotification != null) {
-                if (currentNotification.tick()) {
-                    currentNotification = null;
-                }
-            }
-
-            if (currentNotification == null && !Temp.MAP.isEmpty()) {
-                currentNotification = new Temp(Temp.MAP.values().iterator().next());
-                Temp.MAP.remove(currentNotification.widget.id);
-            }
-        } else if (event.phase == TickEvent.Phase.END) {
-            if (!ClientUtils.RUN_LATER.isEmpty()) {
-                for (Runnable runnable : new ArrayList<>(ClientUtils.RUN_LATER)) {
-                    runnable.run();
-                }
-                ClientUtils.RUN_LATER.clear();
+        if (!ClientUtils.RUN_LATER.isEmpty()) {
+            for (Runnable runnable : new ArrayList<>(ClientUtils.RUN_LATER)) {
+                runnable.run();
             }
+            ClientUtils.RUN_LATER.clear();
         }
     }
 
@@ -302,143 +240,10 @@ public void onGuiScreenDraw(final GuiScreenEvent.DrawScreenEvent.Post event) {
         }
     }
 
-    @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true)
-    public void onGameOverlayRender(RenderGameOverlayEvent.Text event) {
-        if (currentNotification != null && !currentNotification.isImportant()) {
-            currentNotification.render(event.resolution, event.partialTicks);
-            GlStateManager.color(1F, 1F, 1F, 1F);
-            GlStateManager.disableLighting();
-            GlStateManager.enableBlend();
-            GlStateManager.enableTexture2D();
-        }
-    }
-
     @SubscribeEvent(priority = EventPriority.HIGHEST, receiveCanceled = true)
     public void onRenderTick(TickEvent.RenderTickEvent event) {
-
-        if (event.phase == TickEvent.Phase.START) {
-            if (shouldRenderIcons) {
-                IconRenderer.render();
-            }
-        } else if (currentNotification != null && currentNotification.isImportant()) {
-            Minecraft mc = Minecraft.getMinecraft();
-            currentNotification
-                    .render(new ScaledResolution(mc, mc.displayWidth, mc.displayHeight), event.renderTickTime);
-            GlStateManager.color(1F, 1F, 1F, 1F);
-            GlStateManager.disableLighting();
-            GlStateManager.enableBlend();
-            GlStateManager.enableTexture2D();
-        }
-    }
-
-    public static class NotificationWidget {
-
-        public final IChatComponent notification;
-        public final ResourceLocation id;
-        public final List<String> text;
-        public int width, height;
-        public final FontRenderer font;
-        public final long timer;
-
-        public NotificationWidget(IChatComponent n, FontRenderer f) {
-            notification = n;
-            id = n instanceof Notification ? ((Notification) n).getId() : Notification.VANILLA_STATUS;
-            width = 0;
-            font = f;
-            text = new ArrayList<>();
-            timer = n instanceof Notification ? ((Notification) n).getTimer().ticks() : 60L;
-
-            String s0;
-
-            try {
-                s0 = notification.getFormattedText();
-            } catch (Exception ex) {
-                s0 = EnumChatFormatting.RED + ex.toString();
-            }
-
-            Minecraft mc = Minecraft.getMinecraft();
-            for (String s : font.listFormattedStringToWidth(
-                    s0,
-                    new ScaledResolution(mc, mc.displayWidth, mc.displayHeight).getScaledWidth())) {
-                for (String line : s.split("\n")) {
-                    if (!line.isEmpty()) {
-                        line = line.trim();
-                        text.add(line);
-                        width = Math.max(width, font.getStringWidth(line));
-                    }
-                }
-            }
-
-            width += 4;
-            height = text.size() * 11;
-
-            if (text.isEmpty()) {
-                width = 20;
-                height = 20;
-            }
-        }
-    }
-
-    private static class Temp {
-
-        private static final LinkedHashMap<ResourceLocation, IChatComponent> MAP = new LinkedHashMap<>();
-        private final NotificationWidget widget;
-        private long tick, endTick;
-
-        private Temp(IChatComponent n) {
-            widget = new NotificationWidget(n, Minecraft.getMinecraft().fontRenderer);
-            tick = endTick = -1L;
-        }
-
-        public void render(ScaledResolution screen, float partialTicks) {
-            if (tick == -1L || tick >= endTick) {
-                return;
-            }
-
-            int alpha = (int) Math.min(255F, (endTick - tick - partialTicks) * 255F / 20F);
-
-            if (alpha <= 2) {
-                return;
-            }
-
-            GlStateManager.pushMatrix();
-            GlStateManager.disableDepth();
-            GlStateManager.depthMask(false);
-            GlStateManager.disableLighting();
-            GlStateManager.enableBlend();
-            GlStateManager.color(1F, 1F, 1F, 1F);
-
-            int width = screen.getScaledWidth() / 2;
-            int height = screen.getScaledHeight() - 67;
-            int offy = (widget.text.size() * 11) / 2;
-
-            for (int i = 0; i < widget.text.size(); i++) {
-                String string = widget.text.get(i);
-                widget.font.drawStringWithShadow(
-                        string,
-                        width - widget.font.getStringWidth(string) / 2,
-                        height - offy + i * 11,
-                        0xFFFFFF | (alpha << 24));
-            }
-
-            GlStateManager.depthMask(true);
-            GlStateManager.color(1F, 1F, 1F, 1F);
-            GlStateManager.enableLighting();
-            GlStateManager.popMatrix();
-            GlStateManager.enableDepth();
-        }
-
-        private boolean tick() {
-            tick = Minecraft.getMinecraft().theWorld.getTotalWorldTime();
-
-            if (endTick == -1L) {
-                endTick = tick + widget.timer;
-            }
-            return tick >= endTick || Math.min(255F, (endTick - tick) * 255F / 20F) <= 2F;
-        }
-
-        private boolean isImportant() {
-            return widget.notification instanceof Notification notification && notification.isImportant();
+        if (event.phase == TickEvent.Phase.START && shouldRenderIcons) {
+            IconRenderer.render();
         }
     }
 }
diff --git a/src/main/java/serverutils/handlers/ServerUtilitiesServerEventHandler.java b/src/main/java/serverutils/handlers/ServerUtilitiesServerEventHandler.java
index 57ec73c4a..9b127108d 100644
--- a/src/main/java/serverutils/handlers/ServerUtilitiesServerEventHandler.java
+++ b/src/main/java/serverutils/handlers/ServerUtilitiesServerEventHandler.java
@@ -1,5 +1,7 @@
 package serverutils.handlers;
 
+import static serverutils.ServerUtilitiesNotifications.PLAYER_AFK;
+
 import java.util.Collections;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -11,7 +13,6 @@
 import net.minecraft.util.ChatComponentTranslation;
 import net.minecraft.util.EnumChatFormatting;
 import net.minecraft.util.IChatComponent;
-import net.minecraft.util.ResourceLocation;
 import net.minecraftforge.common.ForgeHooks;
 import net.minecraftforge.event.ServerChatEvent;
 
@@ -28,7 +29,6 @@
 import serverutils.data.ServerUtilitiesPlayerData;
 import serverutils.data.ServerUtilitiesUniverseData;
 import serverutils.events.universe.UniverseClearCacheEvent;
-import serverutils.lib.EnumMessageLocation;
 import serverutils.lib.config.ConfigEnum;
 import serverutils.lib.config.RankConfigAPI;
 import serverutils.lib.data.ForgePlayer;
@@ -37,7 +37,6 @@
 import serverutils.lib.util.ServerUtils;
 import serverutils.lib.util.StringUtils;
 import serverutils.lib.util.permission.PermissionAPI;
-import serverutils.lib.util.text_components.Notification;
 import serverutils.lib.util.text_components.TextComponentParser;
 import serverutils.net.MessageUpdatePlayTime;
 import serverutils.net.MessageUpdateTabName;
@@ -46,7 +45,6 @@
 public class ServerUtilitiesServerEventHandler {
 
     public static final ServerUtilitiesServerEventHandler INST = new ServerUtilitiesServerEventHandler();
-    private static final ResourceLocation AFK_ID = new ResourceLocation(ServerUtilities.MOD_ID, "afk");
     private static final Pattern STRIKETHROUGH_PATTERN = Pattern.compile("~~(.+?)~~");
     private static final String STRIKETHROUGH_REPLACE = "&m$1&m";
     private static final Pattern BOLD_PATTERN = Pattern.compile("\\*\\*(.+?)\\*\\*|__(.+?)__");
@@ -194,26 +192,12 @@ public void onServerTick(TickEvent.ServerTickEvent event) {
                             new MessageUpdateTabName(Collections.singleton(forgePlayer)).sendToAll();
                         }
 
-                        for (EntityPlayerMP player1 : universe.server.getConfigurationManager().playerEntityList) {
-                            EnumMessageLocation location = ServerUtilitiesPlayerData.get(universe.getPlayer(player1))
-                                    .getAFKMessageLocation();
-
-                            if (location != EnumMessageLocation.OFF) {
-                                IChatComponent component = ServerUtilities.lang(
-                                        player1,
+                        PLAYER_AFK.sendAll(
+                                StringUtils.color(
                                         isAFK ? "permission.serverutilities.afk.timer.is_afk"
                                                 : "permission.serverutilities.afk.timer.isnt_afk",
-                                        player.getDisplayName());
-                                component.getChatStyle().setColor(EnumChatFormatting.GRAY);
-
-                                if (location == EnumMessageLocation.CHAT) {
-                                    player1.addChatMessage(component);
-                                } else {
-                                    Notification.of(AFK_ID, component).send(universe.server, player1);
-                                }
-                            }
-                        }
-
+                                        EnumChatFormatting.GRAY,
+                                        player.getDisplayName()));
                         ServerUtilities.LOGGER
                                 .info("{}{}", player.getDisplayName(), isAFK ? " is now AFK" : " is no longer AFK");
 
diff --git a/src/main/java/serverutils/lib/command/CmdEditConfigBase.java b/src/main/java/serverutils/lib/command/CmdEditConfigBase.java
index fe4fc7bc1..afed0c511 100644
--- a/src/main/java/serverutils/lib/command/CmdEditConfigBase.java
+++ b/src/main/java/serverutils/lib/command/CmdEditConfigBase.java
@@ -1,5 +1,7 @@
 package serverutils.lib.command;
 
+import static serverutils.ServerUtilitiesNotifications.CONFIG_CHANGED;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -9,14 +11,12 @@
 
 import serverutils.ServerUtilities;
 import serverutils.ServerUtilitiesConfig;
-import serverutils.ServerUtilitiesNotifications;
 import serverutils.lib.config.ConfigGroup;
 import serverutils.lib.config.ConfigValue;
 import serverutils.lib.config.ConfigValueInstance;
 import serverutils.lib.config.IConfigCallback;
 import serverutils.lib.data.ServerUtilitiesAPI;
 import serverutils.lib.util.StringUtils;
-import serverutils.lib.util.text_components.Notification;
 
 public abstract class CmdEditConfigBase extends CmdBase {
 
@@ -89,20 +89,16 @@ public void processCommand(ICommandSender sender, String[] args) throws CommandE
             }
 
             if (ServerUtilitiesConfig.debugging.log_config_editing) {
-                ServerUtilities.LOGGER.info("Setting " + instance.getPath() + " to " + valueString);
+                ServerUtilities.LOGGER.info("Setting {} to {}", instance.getPath(), valueString);
             }
 
             instance.getValue().setValueFromString(sender, valueString, false);
             getCallback(sender).onConfigSaved(group, sender);
-            Notification
-                    .of(
-                            ServerUtilitiesNotifications.CONFIG_CHANGED,
-                            ServerUtilities.lang(
-                                    sender,
-                                    "serverutilities.lang.config_command.set",
-                                    instance.getDisplayName(),
-                                    group.getValue(args[0]).toString()))
-                    .send(getCommandSenderAsPlayer(sender).mcServer, getCommandSenderAsPlayer(sender));
+            CONFIG_CHANGED.send(
+                    getCommandSenderAsPlayer(sender),
+                    "serverutilities.lang.config_command.set",
+                    instance.getDisplayName(),
+                    group.getValue(args[0]).toString());
         }
 
         sender.addChatMessage(instance.getValue().getStringForGUI());
diff --git a/src/main/java/serverutils/lib/data/ServerUtilitiesAPI.java b/src/main/java/serverutils/lib/data/ServerUtilitiesAPI.java
index 1e55181cd..7765ae1b5 100644
--- a/src/main/java/serverutils/lib/data/ServerUtilitiesAPI.java
+++ b/src/main/java/serverutils/lib/data/ServerUtilitiesAPI.java
@@ -15,7 +15,6 @@
 import serverutils.ServerUtilities;
 import serverutils.ServerUtilitiesCommon;
 import serverutils.ServerUtilitiesConfig;
-import serverutils.ServerUtilitiesNotifications;
 import serverutils.events.IReloadHandler;
 import serverutils.events.ServerReloadEvent;
 import serverutils.lib.EnumReloadType;
@@ -66,7 +65,7 @@ public static void reloadServer(Universe universe, ICommandSender sender, EnumRe
 
         if (type == EnumReloadType.RELOAD_COMMAND) {
             for (EntityPlayerMP player : universe.server.getConfigurationManager().playerEntityList) {
-                Notification notification = Notification.of(ServerUtilitiesNotifications.RELOAD_SERVER);
+                Notification notification = Notification.of("reload_server");
                 notification.addLine(ServerUtilities.lang(player, "serverutilities.lang.reload_server", millis));
 
                 if (event.isClientReloadRequired()) {
@@ -93,7 +92,7 @@ public static void reloadServer(Universe universe, ICommandSender sender, EnumRe
 
                 notification.setImportant(true);
                 notification.setTimer(Ticks.SECOND.x(7));
-                notification.send(universe.server, player);
+                notification.send(player);
             }
         }
         reload(universe.server);
diff --git a/src/main/java/serverutils/lib/util/ServerUtils.java b/src/main/java/serverutils/lib/util/ServerUtils.java
index 689143ac4..59bb09216 100644
--- a/src/main/java/serverutils/lib/util/ServerUtils.java
+++ b/src/main/java/serverutils/lib/util/ServerUtils.java
@@ -13,7 +13,6 @@
 import net.minecraft.entity.EnumCreatureType;
 import net.minecraft.entity.player.EntityPlayer;
 import net.minecraft.entity.player.EntityPlayerMP;
-import net.minecraft.network.NetHandlerPlayServer;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.util.AxisAlignedBB;
 import net.minecraft.util.ChatComponentText;
@@ -30,9 +29,8 @@
 import com.mojang.authlib.GameProfile;
 
 import cpw.mods.fml.common.FMLCommonHandler;
-import cpw.mods.fml.common.network.NetworkRegistry;
 import crazypants.enderio.machine.farm.FakeFarmPlayer;
-import serverutils.handlers.ServerUtilitiesClientEventHandler;
+import serverutils.client.NotificationHandler;
 import serverutils.lib.OtherMods;
 import serverutils.net.MessageNotification;
 
@@ -68,15 +66,6 @@ public static IChatComponent getDimensionName(int dim) {
         };
     }
 
-    public static boolean isVanillaClient(ICommandSender sender) {
-        if (sender instanceof EntityPlayerMP) {
-            NetHandlerPlayServer connection = ((EntityPlayerMP) sender).playerNetServerHandler;
-            return connection != null && connection.netManager.channel().attr(NetworkRegistry.MOD_CONTAINER) == null;
-        }
-
-        return false;
-    }
-
     public static boolean isFake(EntityPlayerMP player) {
         return player.playerNetServerHandler == null || player instanceof FakePlayer || isFakeFarmPlayer(player);
     }
@@ -168,13 +157,13 @@ public static void notifyAllChat(MinecraftServer server, String message) {
         notifyChat(server, null, new ChatComponentText(message));
     }
 
-    public static void notify(MinecraftServer server, @Nullable EntityPlayer player, IChatComponent notification) {
+    public static void notify(@Nullable EntityPlayer player, IChatComponent notification) {
         if (player == null) {
             new MessageNotification(notification).sendToAll();
         } else if (player instanceof EntityPlayerMP playerMP) {
             new MessageNotification(notification).sendTo(playerMP);
         } else if (player instanceof EntityClientPlayerMP) {
-            ServerUtilitiesClientEventHandler.INST.onNotify(notification);
+            NotificationHandler.onNotify(notification);
         }
     }
 
diff --git a/src/main/java/serverutils/lib/util/SidedUtils.java b/src/main/java/serverutils/lib/util/SidedUtils.java
index 9554e8652..6585b9afa 100644
--- a/src/main/java/serverutils/lib/util/SidedUtils.java
+++ b/src/main/java/serverutils/lib/util/SidedUtils.java
@@ -5,11 +5,6 @@
 import java.util.Map;
 import java.util.UUID;
 
-import javax.annotation.Nullable;
-
-import net.minecraft.client.resources.I18n;
-import net.minecraft.command.ICommandSender;
-import net.minecraft.util.ChatComponentText;
 import net.minecraft.util.ChatComponentTranslation;
 import net.minecraft.util.IChatComponent;
 import net.minecraft.util.ResourceLocation;
@@ -20,10 +15,7 @@ public class SidedUtils {
     public static UUID UNIVERSE_UUID_CLIENT = null;
     public static boolean trashCan = false, teams = false, chunkClaiming = false;
 
-    public static IChatComponent lang(@Nullable ICommandSender sender, String mod, String key, Object... args) {
-        if (ServerUtils.isVanillaClient(sender)) {
-            return new ChatComponentText(I18n.format(key, args));
-        }
+    public static IChatComponent lang(String key, Object... args) {
         return new ChatComponentTranslation(key, args);
     }
 
diff --git a/src/main/java/serverutils/lib/util/StringUtils.java b/src/main/java/serverutils/lib/util/StringUtils.java
index 7f201e3ad..39eb6139e 100644
--- a/src/main/java/serverutils/lib/util/StringUtils.java
+++ b/src/main/java/serverutils/lib/util/StringUtils.java
@@ -20,6 +20,7 @@
 import net.minecraft.util.EnumChatFormatting;
 import net.minecraft.util.IChatComponent;
 
+import serverutils.ServerUtilities;
 import serverutils.lib.io.Bits;
 
 public class StringUtils {
@@ -478,6 +479,12 @@ public static IChatComponent color(IChatComponent component, @Nullable EnumChatF
         return component;
     }
 
+    public static IChatComponent color(String key, @Nullable EnumChatFormatting color, Object... args) {
+        IChatComponent component = ServerUtilities.lang(key, args);
+        component.getChatStyle().setColor(color);
+        return component;
+    }
+
     public static IChatComponent bold(IChatComponent component, boolean value) {
         component.getChatStyle().setBold(value);
         return component;
diff --git a/src/main/java/serverutils/lib/util/text_components/Notification.java b/src/main/java/serverutils/lib/util/text_components/Notification.java
index 08efad764..68ffe7c2a 100644
--- a/src/main/java/serverutils/lib/util/text_components/Notification.java
+++ b/src/main/java/serverutils/lib/util/text_components/Notification.java
@@ -3,7 +3,6 @@
 import javax.annotation.Nullable;
 
 import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.server.MinecraftServer;
 import net.minecraft.util.ChatComponentText;
 import net.minecraft.util.EnumChatFormatting;
 import net.minecraft.util.IChatComponent;
@@ -89,14 +88,18 @@ public Notification appendSibling(IChatComponent component) {
         return this;
     }
 
+    @Override
     public int hashCode() {
         return id.hashCode();
     }
 
+    @Override
     public boolean equals(Object o) {
-        return o == this || (o instanceof Notification notification && notification.getId().equals(getId()));
+        return o == this || (o instanceof Notification notification
+                && notification.getId().toString().equals(getId().toString()));
     }
 
+    @Override
     public String toString() {
         return "Notification{" + StringJoiner.with(", ").joinObjects(
                 "id=" + id,
@@ -142,11 +145,11 @@ public Notification createCopy() {
         return new Notification(this);
     }
 
-    public void sendToAll(MinecraftServer server) {
-        send(server, null);
+    public void sendToAll() {
+        send(null);
     }
 
-    public void send(MinecraftServer server, @Nullable EntityPlayer player) {
-        ServerUtils.notify(server, player, this);
+    public void send(@Nullable EntityPlayer player) {
+        ServerUtils.notify(player, this);
     }
 }
diff --git a/src/main/java/serverutils/net/MessageClaimedChunksModify.java b/src/main/java/serverutils/net/MessageClaimedChunksModify.java
index 47a8f68b8..f801428f2 100644
--- a/src/main/java/serverutils/net/MessageClaimedChunksModify.java
+++ b/src/main/java/serverutils/net/MessageClaimedChunksModify.java
@@ -5,7 +5,7 @@
 import net.minecraft.entity.player.EntityPlayerMP;
 import net.minecraft.world.ChunkCoordIntPair;
 
-import serverutils.ServerUtilitiesNotifications;
+import serverutils.ServerUtilities;
 import serverutils.ServerUtilitiesPermissions;
 import serverutils.data.ClaimedChunks;
 import serverutils.lib.data.ForgePlayer;
@@ -14,6 +14,7 @@
 import serverutils.lib.math.ChunkDimPos;
 import serverutils.lib.net.MessageToServer;
 import serverutils.lib.net.NetworkWrapper;
+import serverutils.lib.util.text_components.Notification;
 
 public class MessageClaimedChunksModify extends MessageToServer {
 
@@ -65,7 +66,8 @@ public void onMessage(EntityPlayerMP player) {
         ForgePlayer p = ClaimedChunks.instance.universe.getPlayer(player);
 
         if (!p.hasTeam()) {
-            ServerUtilitiesNotifications.NO_TEAM.send(player.mcServer, player);
+            Notification.of("no_team", ServerUtilities.lang("serverutilities.lang.team.error.no_team")).setError()
+                    .send(player);
             return;
         }
 
diff --git a/src/main/java/serverutils/net/MessageNotification.java b/src/main/java/serverutils/net/MessageNotification.java
index 529e7c7e4..a5ea6f918 100644
--- a/src/main/java/serverutils/net/MessageNotification.java
+++ b/src/main/java/serverutils/net/MessageNotification.java
@@ -4,7 +4,7 @@
 
 import cpw.mods.fml.relauncher.Side;
 import cpw.mods.fml.relauncher.SideOnly;
-import serverutils.handlers.ServerUtilitiesClientEventHandler;
+import serverutils.client.NotificationHandler;
 import serverutils.lib.io.DataIn;
 import serverutils.lib.io.DataOut;
 import serverutils.lib.net.MessageToClient;
@@ -38,6 +38,6 @@ public void readData(DataIn data) {
     @Override
     @SideOnly(Side.CLIENT)
     public void onMessage() {
-        ServerUtilitiesClientEventHandler.INST.onNotify(notification);
+        NotificationHandler.onNotify(notification);
     }
 }
diff --git a/src/main/java/serverutils/task/CleanupTask.java b/src/main/java/serverutils/task/CleanupTask.java
index e08703c27..1f813c524 100644
--- a/src/main/java/serverutils/task/CleanupTask.java
+++ b/src/main/java/serverutils/task/CleanupTask.java
@@ -1,6 +1,7 @@
 package serverutils.task;
 
 import static serverutils.ServerUtilitiesConfig.tasks;
+import static serverutils.ServerUtilitiesNotifications.CLEANUP;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -23,7 +24,6 @@
 import serverutils.lib.data.Universe;
 import serverutils.lib.math.Ticks;
 import serverutils.lib.util.StringUtils;
-import serverutils.lib.util.text_components.Notification;
 
 public class CleanupTask extends Task {
 
@@ -57,25 +57,22 @@ public void execute(Universe universe) {
                 }
             }
         }
-        Notification.of(
-                "removed_entities",
-                new ChatComponentText(
-                        StatCollector.translateToLocalFormatted("serverutilities.task.cleanup_removed", removed)))
-                .sendToAll(universe.server);
+
+        CLEANUP.sendAll("serverutilities.task.cleanup_removed", removed);
     }
 
     @Override
     public List<NotifyTask> getNotifications() {
         List<NotifyTask> notifications = new ArrayList<>();
         if (tasks.cleanup.silent) return notifications;
-
-        Notification notification = Notification.of("cleanup_30", getNotificationString(30));
-        NotifyTask task = new NotifyTask(nextTime - Ticks.SECOND.x(30).millis(), notification);
-        notifications.add(task);
-
-        notification = Notification.of("cleanup_60", getNotificationString(60));
-        task = new NotifyTask(nextTime - Ticks.SECOND.x(60).millis(), notification);
-        notifications.add(task);
+        notifications.add(
+                new NotifyTask(
+                        nextTime - Ticks.SECOND.x(30).millis(),
+                        CLEANUP.createNotification(getNotificationString(30))));
+        notifications.add(
+                new NotifyTask(
+                        nextTime - Ticks.SECOND.x(60).millis(),
+                        CLEANUP.createNotification(getNotificationString(60))));
         return notifications;
     }
 
diff --git a/src/main/java/serverutils/task/NotifyTask.java b/src/main/java/serverutils/task/NotifyTask.java
index 878a09248..2ddc20210 100644
--- a/src/main/java/serverutils/task/NotifyTask.java
+++ b/src/main/java/serverutils/task/NotifyTask.java
@@ -25,9 +25,9 @@ public NotifyTask(long whenToRun, Notification notification) {
     @Override
     public void execute(Universe universe) {
         if (player == null) {
-            notification.sendToAll(universe.server);
+            notification.sendToAll();
         } else {
-            notification.send(universe.server, player);
+            notification.send(player);
         }
     }
 }
diff --git a/src/main/java/serverutils/task/ShutdownTask.java b/src/main/java/serverutils/task/ShutdownTask.java
index f0c848939..bbaeacb02 100644
--- a/src/main/java/serverutils/task/ShutdownTask.java
+++ b/src/main/java/serverutils/task/ShutdownTask.java
@@ -1,15 +1,12 @@
 package serverutils.task;
 
-import static serverutils.ServerUtilitiesNotifications.RESTART_TIMER_ID;
+import static serverutils.ServerUtilitiesNotifications.RESTART_TIMER;
 
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.List;
 
-import net.minecraft.util.ChatComponentText;
 import net.minecraft.util.EnumChatFormatting;
-import net.minecraft.util.IChatComponent;
-import net.minecraft.util.StatCollector;
 
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import serverutils.ServerUtilities;
@@ -80,14 +77,11 @@ public List<NotifyTask> getNotifications() {
                     Ticks.SECOND.x(5), Ticks.SECOND.x(4), Ticks.SECOND.x(3), Ticks.SECOND.x(2), Ticks.SECOND.x(1) };
 
             for (Ticks t : ticks) {
-                IChatComponent component = StringUtils.color(
-                        new ChatComponentText(
-                                StatCollector.translateToLocalFormatted(
-                                        "serverutilities.lang.timer.shutdown",
-                                        t.toTimeString())),
-                        EnumChatFormatting.LIGHT_PURPLE);
-
-                Notification notification = Notification.of(RESTART_TIMER_ID, component);
+                Notification notification = RESTART_TIMER.createNotification(
+                        StringUtils.color(
+                                "serverutilities.lang.timer.shutdown",
+                                EnumChatFormatting.LIGHT_PURPLE,
+                                t.toTimeString()));
                 NotifyTask task = new NotifyTask(shutdownTime - t.millis(), notification);
                 notifications.add(task);
             }
diff --git a/src/main/java/serverutils/task/TeleportTask.java b/src/main/java/serverutils/task/TeleportTask.java
index 026828f49..f026799dc 100644
--- a/src/main/java/serverutils/task/TeleportTask.java
+++ b/src/main/java/serverutils/task/TeleportTask.java
@@ -19,7 +19,6 @@
 import serverutils.lib.math.TeleporterDimPos;
 import serverutils.lib.math.Ticks;
 import serverutils.lib.util.StringUtils;
-import serverutils.lib.util.text_components.Notification;
 
 public class TeleportTask extends Task {
 
@@ -50,8 +49,7 @@ public TeleportTask(TeleportType teleportType, EntityPlayerMP player, ServerUtil
     @Override
     public void execute(Universe universe) {
         if (!startPos.equalsPos(new BlockDimPos(player)) || startHP > player.getHealth()) {
-            player.addChatMessage(
-                    StringUtils.color(ServerUtilities.lang(player, "stand_still_failed"), EnumChatFormatting.RED));
+            player.addChatMessage(StringUtils.color("serverutilities.lang.warps.cancelled", EnumChatFormatting.RED));
         } else if (secondsLeft <= 1) {
             TeleporterDimPos teleporter = pos.apply(player);
             if (teleporter != null) {
@@ -78,13 +76,11 @@ public void execute(Universe universe) {
         } else {
             secondsLeft -= 1;
             setNextTime(System.currentTimeMillis() + Ticks.SECOND.millis());
-            universe.scheduleTask(this);
-
             IChatComponent component = StringUtils.color(
-                    ServerUtilities.lang(player, "stand_still", startSeconds).appendText(" [" + secondsLeft + "]"),
+                    ServerUtilities.lang("stand_still", startSeconds).appendText(" [" + secondsLeft + "]"),
                     EnumChatFormatting.GOLD);
-
-            Notification.of(TELEPORT_WARMUP, component).setVanilla(true).send(player.mcServer, player);
+            TELEPORT_WARMUP.createNotification(component).setVanilla(true).send(player);
+            universe.scheduleTask(this);
         }
     }
 }
diff --git a/src/main/java/serverutils/task/backup/BackupTask.java b/src/main/java/serverutils/task/backup/BackupTask.java
index 9a3680705..c92fc2551 100644
--- a/src/main/java/serverutils/task/backup/BackupTask.java
+++ b/src/main/java/serverutils/task/backup/BackupTask.java
@@ -1,7 +1,7 @@
 package serverutils.task.backup;
 
 import static serverutils.ServerUtilitiesConfig.backups;
-import static serverutils.ServerUtilitiesNotifications.BACKUP_START;
+import static serverutils.ServerUtilitiesNotifications.BACKUP;
 import static serverutils.lib.util.FileUtils.SizeUnit;
 
 import java.io.File;
@@ -26,13 +26,13 @@
 import it.unimi.dsi.fastutil.ints.Int2BooleanMap;
 import serverutils.ServerUtilities;
 import serverutils.ServerUtilitiesConfig;
-import serverutils.ServerUtilitiesNotifications;
 import serverutils.data.ClaimedChunks;
 import serverutils.lib.data.Universe;
 import serverutils.lib.math.ChunkDimPos;
 import serverutils.lib.math.Ticks;
 import serverutils.lib.util.FileUtils;
 import serverutils.lib.util.ServerUtils;
+import serverutils.lib.util.StringUtils;
 import serverutils.lib.util.compression.ICompress;
 import serverutils.task.Task;
 
@@ -113,7 +113,7 @@ public void execute(Universe universe) {
         }
 
         server.getConfigurationManager().saveAllPlayerData();
-        ServerUtilitiesNotifications.backupNotification(BACKUP_START, "cmd.backup_start");
+        BACKUP.sendAll(StringUtils.color("cmd.backup_start", EnumChatFormatting.LIGHT_PURPLE));
         Set<ChunkDimPos> backupChunks = new HashSet<>();
         if (backups.only_backup_claimed_chunks && ClaimedChunks.isActive()) {
             backupChunks.addAll(ClaimedChunks.instance.getAllClaimedPositions());
diff --git a/src/main/java/serverutils/task/backup/ThreadBackup.java b/src/main/java/serverutils/task/backup/ThreadBackup.java
index d84ad15fc..215a4d75d 100644
--- a/src/main/java/serverutils/task/backup/ThreadBackup.java
+++ b/src/main/java/serverutils/task/backup/ThreadBackup.java
@@ -1,8 +1,7 @@
 package serverutils.task.backup;
 
 import static serverutils.ServerUtilitiesConfig.backups;
-import static serverutils.ServerUtilitiesNotifications.BACKUP_END1;
-import static serverutils.ServerUtilitiesNotifications.BACKUP_END2;
+import static serverutils.ServerUtilitiesNotifications.BACKUP;
 import static serverutils.task.backup.BackupTask.BACKUP_TEMP_FOLDER;
 
 import java.io.DataInputStream;
@@ -18,7 +17,6 @@
 import net.minecraft.nbt.CompressedStreamTools;
 import net.minecraft.nbt.NBTTagCompound;
 import net.minecraft.util.EnumChatFormatting;
-import net.minecraft.util.IChatComponent;
 import net.minecraft.world.WorldServer;
 import net.minecraft.world.chunk.storage.RegionFile;
 import net.minecraft.world.chunk.storage.RegionFileCache;
@@ -35,7 +33,6 @@
 import it.unimi.dsi.fastutil.objects.ObjectSet;
 import serverutils.ServerUtilities;
 import serverutils.ServerUtilitiesConfig;
-import serverutils.ServerUtilitiesNotifications;
 import serverutils.lib.math.ChunkDimPos;
 import serverutils.lib.math.Ticks;
 import serverutils.lib.util.FileUtils;
@@ -91,20 +88,22 @@ public static void doBackup(ICompress compressor, File src, String customName, S
 
                 if (backups.display_file_size) {
                     String sizeT = FileUtils.getSizeString(BackupTask.BACKUP_FOLDER);
-                    ServerUtilitiesNotifications.backupNotification(
-                            BACKUP_END2,
-                            "cmd.backup_end_2",
-                            getDoneTime(start),
-                            (backupSize.equals(sizeT) ? backupSize : (backupSize + " | " + sizeT)));
+                    BACKUP.sendAll(
+                            StringUtils.color(
+                                    "cmd.backup_end_2",
+                                    EnumChatFormatting.LIGHT_PURPLE,
+                                    getDoneTime(start),
+                                    (backupSize.equals(sizeT) ? backupSize : (backupSize + " | " + sizeT))));
                 } else {
-                    ServerUtilitiesNotifications
-                            .backupNotification(BACKUP_END1, "cmd.backup_end_1", getDoneTime(start));
+                    BACKUP.sendAll(
+                            StringUtils.color("cmd.backup_end_1", EnumChatFormatting.LIGHT_PURPLE, getDoneTime(start)));
                 }
             }
         } catch (Exception e) {
-            IChatComponent c = StringUtils
-                    .color(ServerUtilities.lang(null, "cmd.backup_fail", e), EnumChatFormatting.RED);
-            ServerUtils.notifyChat(ServerUtils.getServer(), null, c);
+            ServerUtils.notifyChat(
+                    ServerUtils.getServer(),
+                    null,
+                    StringUtils.color("cmd.backup_fail", EnumChatFormatting.RED, e.getMessage()));
             ServerUtilities.LOGGER.error("Error while backing up", e);
 
             if (dstFile != null) FileUtils.delete(dstFile);
diff --git a/src/main/resources/assets/serverutilities/lang/en_US.lang b/src/main/resources/assets/serverutilities/lang/en_US.lang
index 6febdd308..4e6acee54 100644
--- a/src/main/resources/assets/serverutilities/lang/en_US.lang
+++ b/src/main/resources/assets/serverutilities/lang/en_US.lang
@@ -193,6 +193,32 @@ serverutilities_client.sidebar_placement=Sidebar Placement
 serverutilities_client.notification_location=Notification Location
 serverutilities_client.show_dotted_lines=Show Dotted Line On Loaded Chunks
 
+# Notification Config
+serverutilities.notifications.config=Notification Config
+serverutilities.notifications.last_received=Last received notification:
+serverutilities.notifications.chunk_modified=Chunk Modified
+serverutilities.notifications.chunk_modified.desc=Notifies when claiming/loading a chunk
+serverutilities.notifications.chunk_changed=Chunk Changed
+serverutilities.notifications.chunk_changed.desc=Notifies when a entering a claimed chunk
+serverutilities.notifications.cant_modify_chunk=Can't Modify Chunk
+serverutilities.notifications.cant_modify_chunk.desc=Notifies when unable to modify a chunk
+serverutilities.notifications.teleport=Teleport
+serverutilities.notifications.teleport.desc=Notifies when teleporting to homes/warps
+serverutilities.notifications.teleport_warmup=Teleport Warmup
+serverutilities.notifications.teleport_warmup.desc=Counts down until teleport is complete
+serverutilities.notifications.backup=Backup
+serverutilities.notifications.backup.desc=Notifies when a backup is started/stopped
+serverutilities.notifications.config_changed=Config Changed
+serverutilities.notifications.config_changed.desc=Notifies when configs are changed via commands
+serverutilities.notifications.restart_timer=Restart Timer
+serverutilities.notifications.restart_timer.desc=Notifies when server is about to restart
+serverutilities.notifications.cleanup=Cleanup
+serverutilities.notifications.cleanup.desc=Notifies when entity cleanup is about to start or finish
+serverutilities.notifications.player_afk=Player AFK
+serverutilities.notifications.player_afk.desc=Notifies when a player goes AFK
+
+
+
 # Rank Config
 permission.serverutilities.badge=Badge
 permission.serverutilities.badge.tooltip=Badge (icon on chest). Must be an URL
@@ -469,6 +495,8 @@ serverutilities.lang.warps.tp=Teleported to '%s'!
 serverutilities.lang.warps.no_dp=You can only go back once!
 serverutilities.lang.warps.no_pos_found=No position to go back to!
 serverutilities.lang.warps.cross_dim=You can't teleport to another dimension!
+serverutilities.lang.warps.warmup=Please, stand still for %d seconds [%d].
+serverutilities.lang.warps.cancelled=You moved! Teleport cancelled.
 
 # Homes
 serverutilities.lang.homes.set=Home '%s' set!
diff --git a/src/main/resources/assets/serverutilities/textures/icons/bell2.png b/src/main/resources/assets/serverutilities/textures/icons/bell2.png
new file mode 100644
index 0000000000000000000000000000000000000000..1cca60e1be23e15833b7e95c7b9b0a572fed4a40
GIT binary patch
literal 1530
zcmV<W1qJ$vP)<h;3K|Lk000e1NJLTq001BW001Be1^@s71bs?k00001b5ch_0Itp)
z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGfk^le;k^%Sb@^1hD1&>KYK~zXf%~xA&
zR7Dt`bIzV#a9b?awzOP=8fjWW#fVy>7$3AR;)5ZWs1ah+fN0PsZ44&b2V+bV0~&&O
zNh}&5UIJ-B3>ayZsFa&P+cgN5Ubo#|p_kn~7r*b^Jal(^u{`)C-_AcXXa1S_=l}ni
z6-i<Ct0oCilI<oN;ZWeD)u0<<oMrw}t&?RUd^!Y&1SvQOuwjhS{)74j6t7aYphwhK
zz;h5KyTylr%W8qE*Qft>_PJkYp>f!eJ($)qY)GRW>PeIbem<m}Z~0x}R8N6R*f~9L
z-u_FfHE#l9VfE1o3rx3roJBf~*Fl;W=SMLbCUM_37V^ik{)Le79!o!<^&$$@9lZz*
ztrQGH7)oyZD8vr%6Sj%5F7@@-Hc1~rtaH}$7FZ6>1rwq5s!urL%z@d~aTY&~KD7;9
zNjGe5(gi=xNEdsZss~9D3(%)WE(YF5bq<@X4nT2A(SIpWmYE@KbgD@)CsSG#iI}HO
zoEF`vM%m_SVwsJlhB2FWimC9&1>=pf2lWpjj=Ec*Wux*f@l|#;HAY6o=$Z3oF+_e_
z=(j=iyBfL2ku6}RHn_}@AM1?}>yW<*E{K1%ev{FTY9W~Cw<_hEz`MZ;a6Whq>;t)9
z4dlQxAf1G<se?Bd2#x$$kGjmMP6_`OEYKHN_V$^@vLyO)@6aMv$21{Bfk7pHM#Jip
z=u4Q4nCOXd6bT=usbbGitGXVd=XfqW1qh(cD6UiJM|PI@9K}2^U^O`K2FS8_#}QNF
zSgo=S1#_)8&Rs6|&YZ2<)#Qjq#X!b<%UdsjF}Asyq<CDMb>x&tPZv2Tn5!~CYjRhC
z{L?Q+r6W&@;VXe1k5yQ<u*SMupkbKT{<5o6qrKC-ci}S4zOw)Wyw(#j>+Nc4G>43)
z((?F=7)+Z5T?O>M(fxJCHoJN~y1LDQ4KHc+h{nSu<$6m~NGwHjDVXb55Pj|$<paBl
zxE~SY+jeS=FKp53)6#_U^g5Xaq+2sT@^&2pV}7qwjDA%+d(QY`#X~x6!T}oWvWA_Q
zy&A>eAnnAkX6_jBE8w6a@DXq&$i9A%7boNXD%b=zgM%RR18w&|N3>flLb47qsu`?^
zIXALh&I(g0km5b++CV_;bLuX}@c!VzUqcxJzeTdInY%bnlUw<0x|nPM60+;Fsw#Ft
zo;1e&KuEutmLKN@%5&#pCS6d{1q`;T+1WvH0(az{PScEl$7HzSxUu@-$8~xula9NX
z1h{eJhlI&%|Lf$0B8&X;mGYrA71k^8W{}B?$vk1h6C0%AD{5(0P@IB1ZL_CBXSaE{
zs6^XJ5oG8w{V?=AYfQ9&j3hUVM;`C<S);H(s%SW<@IB1@z_(x;$T_`@`xn4QspNib
zaA45fX*bOt_uV6Jtg4ijL%bE_ZN`M<Ex>;SlzC2k-Q?lk_%bUfF{9luAVQi(M$PJ+
zCE9LOnF-B_N5He;%V2EBXZqV1!1Q^;pz9)2)5MC}Ju)4f8}%OfE?B3DXsY~%I%Vwx
z<#H7}W`bx;XdRtq->;2ESIu4{{QXhO0r^Jk0{Jc^DMuzQfS^_AEZJQxfy#nB>EAzf
zNzgxFUOnGzhQHo#gtzZ9$KKs41v|qcw*6ys1l^2(T9n>T#~uKCK}LmjEhbw4+vJfB
z_2oI4!n<E_9ODS9-s8U+J-hcALkEutJ)j8l&&EL6;zd#(o{^rePiyoap5J0zhR8{I
z@4BQV{2WfXfYZm60w^smTdngM%?CZtjZbc#9lxOyG0R1(tE8;EmdiP9?Pg@dW`p;&
zf4981Dbt^*i(_aN>pD)g07e7<4CR80z}rA~awZZg;v^*B{&Zd*o15|2#xtj>dDh%5
g$5@^Z5-5fE2i^`p<UH&X4FCWD07*qoM6N<$g0@lHRR910

literal 0
HcmV?d00001