diff --git a/Images/Examples/Message from Discord.png b/Images/Examples/Message from Discord.png new file mode 100644 index 0000000..787a392 Binary files /dev/null and b/Images/Examples/Message from Discord.png differ diff --git a/Images/Examples/Message from HUB.png b/Images/Examples/Message from HUB.png new file mode 100644 index 0000000..1dde618 Binary files /dev/null and b/Images/Examples/Message from HUB.png differ diff --git a/Images/Examples/Message from SMP.png b/Images/Examples/Message from SMP.png new file mode 100644 index 0000000..6112a32 Binary files /dev/null and b/Images/Examples/Message from SMP.png differ diff --git a/README.md b/README.md index c322d05..4442e94 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,11 @@ use-permissions: false # Example: If it is set to '$', then when a player sends $hello, it will be sent through the proxy. proxy-message-prefix: '' +# Messages that start with this character will not be sent through the plugin. +# Set to '' to disable. +# Example: If it is set to '#', then when a player sends #hello, it will not be sent through the proxy. +proxy-message-prefix-blacklist: '' + # Whether to send if the statuses of the servers connected to the proxy when the proxy starts up. # THIS IS FOR DISCORD MESSAGES ONLY. use-initial-server-status: true @@ -208,9 +213,15 @@ minecraft: join: enabled: true message: "&e%player% &ahas joined the network. (%server%)" + recipients: + exclude-self: false + exclude-server: false # excludes every player but the sender leave: enabled: true message: "&e%player% &chas left the network. (%server%)" + recipients: + exclude-self: false + exclude-server: false # excludes every player but the sender chat: enabled: true message: "&8[&3%server%&8] &e%player% &9» &7%message%" @@ -305,7 +316,7 @@ console: update-message: "&7There is an update! You are on &c%old%. New version is &a%new%&7: &6%link%" # DO NOT TOUCH THIS -file-version: 10 +file-version: 11 ``` --- diff --git a/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/chat/ChatHandler.java b/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/chat/ChatHandler.java index 3eaacfb..2277331 100644 --- a/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/chat/ChatHandler.java +++ b/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/chat/ChatHandler.java @@ -55,6 +55,9 @@ public ChatHandler(ISimpleProxyChat plugin, EpochHelper epochHelper) { private Optional getValidMessage(String message) { String messagePrefix = config.get(ConfigKey.PROXY_MESSAGE_PREFIX).asString(); + String messagePrefixBlacklist = config.get(ConfigKey.PROXY_MESSAGE_PREFIX_BLACKLIST).asString(); + + if (!messagePrefixBlacklist.isEmpty() && message.startsWith(messagePrefixBlacklist)) return Optional.empty(); if (messagePrefix.isEmpty()) return Optional.of(message); if (!message.startsWith(messagePrefix)) return Optional.empty(); diff --git a/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/config/ConfigKey.java b/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/config/ConfigKey.java index f789b9c..a16d874 100644 --- a/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/config/ConfigKey.java +++ b/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/config/ConfigKey.java @@ -22,6 +22,7 @@ public enum ConfigKey { ALIASES (ConfigFileType.CONFIG, "aliases", Map.class), USE_PERMISSIONS (ConfigFileType.CONFIG, "use-permissions", Boolean.class), PROXY_MESSAGE_PREFIX (ConfigFileType.CONFIG, "proxy-message-prefix", String.class), + PROXY_MESSAGE_PREFIX_BLACKLIST (ConfigFileType.CONFIG, "proxy-message-prefix-blacklist", String.class), USE_INITIAL_SERVER_STATUS (ConfigFileType.CONFIG, "use-initial-server-status", Boolean.class), USE_FAKE_MESSAGES (ConfigFileType.CONFIG, "use-fake-messages", Boolean.class), TIMESTAMP_USE_API (ConfigFileType.CONFIG, "timestamp.use-api", Boolean.class), @@ -46,8 +47,12 @@ public enum ConfigKey { MINECRAFT_JOIN_ENABLED (ConfigFileType.MESSAGES, "minecraft.join.enabled", Boolean.class), MINECRAFT_JOIN (ConfigFileType.MESSAGES, "minecraft.join.message", String.class), + MINECRAFT_JOIN_RECIPIENTS_EXCLUDE_SELF (ConfigFileType.MESSAGES, "minecraft.join.recipients.exclude-self", Boolean.class), + MINECRAFT_JOIN_RECIPIENTS_EXCLUDE_SERVER (ConfigFileType.MESSAGES, "minecraft.join.recipients.exclude-server", Boolean.class), MINECRAFT_LEAVE_ENABLED (ConfigFileType.MESSAGES, "minecraft.leave.enabled", Boolean.class), MINECRAFT_LEAVE (ConfigFileType.MESSAGES, "minecraft.leave.message", String.class), + MINECRAFT_LEAVE_RECIPIENTS_EXCLUDE_SELF (ConfigFileType.MESSAGES, "minecraft.leave.recipients.exclude-self", Boolean.class), + MINECRAFT_LEAVE_RECIPIENTS_EXCLUDE_SERVER (ConfigFileType.MESSAGES, "minecraft.leave.recipients.exclude-server", Boolean.class), MINECRAFT_CHAT_ENABLED (ConfigFileType.MESSAGES, "minecraft.chat.enabled", Boolean.class), MINECRAFT_CHAT_MESSAGE (ConfigFileType.MESSAGES, "minecraft.chat.message", String.class), MINECRAFT_CHAT_VANISHED_MESSAGE (ConfigFileType.MESSAGES, "minecraft.chat.vanished", String.class), diff --git a/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/listeners/bungee/BungeeServerListener.java b/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/listeners/bungee/BungeeServerListener.java index 89505a8..3e49040 100644 --- a/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/listeners/bungee/BungeeServerListener.java +++ b/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/listeners/bungee/BungeeServerListener.java @@ -106,8 +106,20 @@ void leave(ProxiedPlayer player, boolean isFake) { plugin, () -> { previousServerHandler.get(player.getName()).ifPresent((serverInfo) -> { - if (isFake) chatHandler.runProxyLeaveMessage(player.getName(), player.getUniqueId(), serverInfo.getName(), this::sendToAllServersVanish); - else chatHandler.runProxyLeaveMessage(player.getName(), player.getUniqueId(), serverInfo.getName(), this::sendToAllServers); + if (isFake) + chatHandler.runProxyLeaveMessage( + player.getName(), + player.getUniqueId(), + serverInfo.getName(), + (message, permission) -> sendToAllServersLeaveFilteredVanish(message, permission, player.getUniqueId(), serverInfo.getName()) + ); + else + chatHandler.runProxyLeaveMessage( + player.getName(), + player.getUniqueId(), + serverInfo.getName(), + (message, permission) -> sendToAllServersLeaveFiltered(message, permission, player.getUniqueId(), serverInfo.getName()) + ); }); }, 50L, TimeUnit.MILLISECONDS); // 50ms is 1 tick @@ -155,8 +167,20 @@ public void join(ProxiedPlayer player, @Nullable Server server, boolean isFake) previousServerHandler.put(player.getName(), server.getInfo()); - if (isFake) chatHandler.runProxyJoinMessage(player.getName(), player.getUniqueId(), server.getInfo().getName(), this::sendToAllServersVanish); - else chatHandler.runProxyJoinMessage(player.getName(), player.getUniqueId(), server.getInfo().getName(), this::sendToAllServers); + if (isFake) + chatHandler.runProxyJoinMessage( + player.getName(), + player.getUniqueId(), + server.getInfo().getName(), + (message, permission) -> sendToAllServersJoinFilteredVanish(message, permission, player.getUniqueId(), server.getInfo().getName()) + ); + else + chatHandler.runProxyJoinMessage( + player.getName(), + player.getUniqueId(), + server.getInfo().getName(), + (message, permission) -> sendToAllServersJoinFiltered(message, permission, player.getUniqueId(), server.getInfo().getName()) + ); }, 50L * 2, TimeUnit.MILLISECONDS); // 50ms is 1 tick } catch (Exception e) { @@ -237,4 +261,138 @@ private void sendToAllServersVanish(String parsedMessage, Permission permission) .forEach((player) -> player.sendMessage(ChatMessageType.CHAT, Helper.convertToBungee(parsedMessage))); } + private void sendToAllServersJoinFiltered(String parsedMessage, Permission permission, java.util.UUID subjectUUID, String subjectServerName) { + boolean excludeSelf = plugin.getConfig().get(ConfigKey.MINECRAFT_JOIN_RECIPIENTS_EXCLUDE_SELF).asBoolean(); + boolean excludeServer = plugin.getConfig().get(ConfigKey.MINECRAFT_JOIN_RECIPIENTS_EXCLUDE_SERVER).asBoolean(); + + plugin.getProxy().getPlayers().stream() + .filter((player) -> { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean()) + return player.hasPermission(permission.getPermissionNode()); + return true; + }) + .filter((player) -> { + if (player.getServer() == null || player.getServer().getInfo() == null) return false; + return !Helper.serverHasChatLocked(plugin, player.getServer().getInfo().getName()); + }) + .filter((player) -> !playerIsInDisabledServer(player, plugin)) + .filter((player) -> !excludeSelf || !player.getUniqueId().equals(subjectUUID)) + // Ensure the subject is not included in the stream when excluding the server to avoid duplicates + .filter((player) -> !(excludeServer && player.getUniqueId().equals(subjectUUID))) + .filter((player) -> !excludeServer || (player.getServer() == null || player.getServer().getInfo() == null) || !player.getServer().getInfo().getName().equalsIgnoreCase(subjectServerName)) + .forEach((player) -> player.sendMessage(ChatMessageType.CHAT, Helper.convertToBungee(parsedMessage))); + + // If excluding the server but not the subject, explicitly send to the subject to keep behavior consistent + if (excludeServer && !excludeSelf) { + ProxiedPlayer subject = plugin.getProxy().getPlayer(subjectUUID); + if (subject != null) { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean() + && !subject.hasPermission(permission.getPermissionNode())) return; + if (subject.getServer() == null || subject.getServer().getInfo() == null) return; + if (Helper.serverHasChatLocked(plugin, subject.getServer().getInfo().getName())) return; + if (playerIsInDisabledServer(subject, plugin)) return; + subject.sendMessage(ChatMessageType.CHAT, Helper.convertToBungee(parsedMessage)); + } + } + } + + private void sendToAllServersLeaveFiltered(String parsedMessage, Permission permission, java.util.UUID subjectUUID, String subjectServerName) { + boolean excludeSelf = plugin.getConfig().get(ConfigKey.MINECRAFT_LEAVE_RECIPIENTS_EXCLUDE_SELF).asBoolean(); + boolean excludeServer = plugin.getConfig().get(ConfigKey.MINECRAFT_LEAVE_RECIPIENTS_EXCLUDE_SERVER).asBoolean(); + + plugin.getProxy().getPlayers().stream() + .filter((player) -> { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean()) + return player.hasPermission(permission.getPermissionNode()); + return true; + }) + .filter((player) -> { + if (player.getServer() == null || player.getServer().getInfo() == null) return false; + return !Helper.serverHasChatLocked(plugin, player.getServer().getInfo().getName()); + }) + .filter((player) -> !playerIsInDisabledServer(player, plugin)) + .filter((player) -> !excludeSelf || !player.getUniqueId().equals(subjectUUID)) + // Ensure the subject is not included in the stream when excluding the server to avoid duplicates + .filter((player) -> !(excludeServer && player.getUniqueId().equals(subjectUUID))) + .filter((player) -> !excludeServer || (player.getServer() == null || player.getServer().getInfo() == null) || !player.getServer().getInfo().getName().equalsIgnoreCase(subjectServerName)) + .forEach((player) -> player.sendMessage(ChatMessageType.CHAT, Helper.convertToBungee(parsedMessage))); + + // If excluding the server but not the subject, explicitly send to the subject to keep behavior consistent + if (excludeServer && !excludeSelf) { + ProxiedPlayer subject = plugin.getProxy().getPlayer(subjectUUID); + if (subject != null) { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean() + && !subject.hasPermission(permission.getPermissionNode())) return; + if (subject.getServer() == null || subject.getServer().getInfo() == null) return; + if (Helper.serverHasChatLocked(plugin, subject.getServer().getInfo().getName())) return; + if (playerIsInDisabledServer(subject, plugin)) return; + subject.sendMessage(ChatMessageType.CHAT, Helper.convertToBungee(parsedMessage)); + } + } + } + + private void sendToAllServersJoinFilteredVanish(String parsedMessage, Permission permission, java.util.UUID subjectUUID, String subjectServerName) { + boolean excludeSelf = plugin.getConfig().get(ConfigKey.MINECRAFT_JOIN_RECIPIENTS_EXCLUDE_SELF).asBoolean(); + boolean excludeServer = plugin.getConfig().get(ConfigKey.MINECRAFT_JOIN_RECIPIENTS_EXCLUDE_SERVER).asBoolean(); + + plugin.getProxy().getPlayers().stream() + .filter((player) -> { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean()) + return player.hasPermission(permission.getPermissionNode()); + return true; + }) + .filter((player) -> { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean()) + return player.hasPermission(Permission.READ_FAKE_MESSAGE.getPermissionNode()); + return true; + }) + .filter((player) -> !excludeSelf || !player.getUniqueId().equals(subjectUUID)) + // Ensure the subject is not included in the stream when excluding the server to avoid duplicates + .filter((player) -> !(excludeServer && player.getUniqueId().equals(subjectUUID))) + .filter((player) -> !excludeServer || (player.getServer() == null || player.getServer().getInfo() == null) || !player.getServer().getInfo().getName().equalsIgnoreCase(subjectServerName)) + .forEach((player) -> player.sendMessage(ChatMessageType.CHAT, Helper.convertToBungee(parsedMessage))); + + // If excluding the server but not the subject, explicitly send to the subject to keep behavior consistent + if (excludeServer && !excludeSelf) { + ProxiedPlayer subject = plugin.getProxy().getPlayer(subjectUUID); + if (subject != null) { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean() + && !subject.hasPermission(permission.getPermissionNode())) return; + subject.sendMessage(ChatMessageType.CHAT, Helper.convertToBungee(parsedMessage)); + } + } + } + + private void sendToAllServersLeaveFilteredVanish(String parsedMessage, Permission permission, java.util.UUID subjectUUID, String subjectServerName) { + boolean excludeSelf = plugin.getConfig().get(ConfigKey.MINECRAFT_LEAVE_RECIPIENTS_EXCLUDE_SELF).asBoolean(); + boolean excludeServer = plugin.getConfig().get(ConfigKey.MINECRAFT_LEAVE_RECIPIENTS_EXCLUDE_SERVER).asBoolean(); + + plugin.getProxy().getPlayers().stream() + .filter((player) -> { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean()) + return player.hasPermission(permission.getPermissionNode()); + return true; + }) + .filter((player) -> { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean()) + return player.hasPermission(Permission.READ_FAKE_MESSAGE.getPermissionNode()); + return true; + }) + .filter((player) -> !excludeSelf || !player.getUniqueId().equals(subjectUUID)) + // Ensure the subject is not included in the stream when excluding the server to avoid duplicates + .filter((player) -> !(excludeServer && player.getUniqueId().equals(subjectUUID))) + .filter((player) -> !excludeServer || (player.getServer() == null || player.getServer().getInfo() == null) || !player.getServer().getInfo().getName().equalsIgnoreCase(subjectServerName)) + .forEach((player) -> player.sendMessage(ChatMessageType.CHAT, Helper.convertToBungee(parsedMessage))); + + // If excluding the server but not the subject, explicitly send to the subject to keep behavior consistent + if (excludeServer && !excludeSelf) { + ProxiedPlayer subject = plugin.getProxy().getPlayer(subjectUUID); + if (subject != null) { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean() + && !subject.hasPermission(permission.getPermissionNode())) return; + subject.sendMessage(ChatMessageType.CHAT, Helper.convertToBungee(parsedMessage)); + } + } + } + } diff --git a/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/listeners/velocity/VelocityServerListener.java b/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/listeners/velocity/VelocityServerListener.java index 4a39e46..6fd5540 100644 --- a/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/listeners/velocity/VelocityServerListener.java +++ b/projects/main-app/src/main/java/com/beanbeanjuice/simpleproxychat/utility/listeners/velocity/VelocityServerListener.java @@ -109,11 +109,21 @@ protected void leave(Player player) { } protected void leave(Player player, String serverName) { - chatHandler.runProxyLeaveMessage(player.getUsername(), player.getUniqueId(), serverName, this::sendToAllServers); + chatHandler.runProxyLeaveMessage( + player.getUsername(), + player.getUniqueId(), + serverName, + (message, permission) -> sendToAllServersLeaveFiltered(message, permission, player.getUniqueId(), serverName) + ); } protected void join(Player player, String serverName) { - chatHandler.runProxyJoinMessage(player.getUsername(), player.getUniqueId(), serverName, this::sendToAllServers); + chatHandler.runProxyJoinMessage( + player.getUsername(), + player.getUniqueId(), + serverName, + (message, permission) -> sendToAllServersJoinFiltered(message, permission, player.getUniqueId(), serverName) + ); } private void startServerStatusDetection() { @@ -193,4 +203,74 @@ private void sendToAllServers(String message, Permission permission) { .forEach((player) -> player.sendMessage(MiniMessage.miniMessage().deserialize(message))); } + private void sendToAllServersJoinFiltered(String message, Permission permission, UUID subjectUUID, String subjectServerName) { + boolean excludeSelf = plugin.getConfig().get(ConfigKey.MINECRAFT_JOIN_RECIPIENTS_EXCLUDE_SELF).asBoolean(); + boolean excludeServer = plugin.getConfig().get(ConfigKey.MINECRAFT_JOIN_RECIPIENTS_EXCLUDE_SERVER).asBoolean(); + + plugin.getProxyServer().getAllPlayers().stream() + .filter((player) -> { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean()) + return player.hasPermission(permission.getPermissionNode()); + return true; + }) + .filter((player) -> !playerIsInDisabledServer(player, plugin)) + .filter((player) -> !excludeSelf || !player.getUniqueId().equals(subjectUUID)) + // Ensure the subject is not included in the stream when excluding the server to avoid duplicates + .filter((player) -> !(excludeServer && player.getUniqueId().equals(subjectUUID))) + .filter((player) -> { + if (!excludeServer) return true; + return player.getCurrentServer() + .map(ServerConnection::getServerInfo) + .map(ServerInfo::getName) + .map((name) -> !name.equalsIgnoreCase(subjectServerName)) + .orElse(true); + }) + .forEach((player) -> player.sendMessage(MiniMessage.miniMessage().deserialize(message))); + + // If excluding the server but not the subject, explicitly send to the subject to keep behavior consistent + if (excludeServer && !excludeSelf) { + plugin.getProxyServer().getPlayer(subjectUUID).ifPresent((subjectPlayer) -> { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean() + && !subjectPlayer.hasPermission(permission.getPermissionNode())) return; + if (playerIsInDisabledServer(subjectPlayer, plugin)) return; + subjectPlayer.sendMessage(MiniMessage.miniMessage().deserialize(message)); + }); + } + } + + private void sendToAllServersLeaveFiltered(String message, Permission permission, UUID subjectUUID, String subjectServerName) { + boolean excludeSelf = plugin.getConfig().get(ConfigKey.MINECRAFT_LEAVE_RECIPIENTS_EXCLUDE_SELF).asBoolean(); + boolean excludeServer = plugin.getConfig().get(ConfigKey.MINECRAFT_LEAVE_RECIPIENTS_EXCLUDE_SERVER).asBoolean(); + + plugin.getProxyServer().getAllPlayers().stream() + .filter((player) -> { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean()) + return player.hasPermission(permission.getPermissionNode()); + return true; + }) + .filter((player) -> !playerIsInDisabledServer(player, plugin)) + .filter((player) -> !excludeSelf || !player.getUniqueId().equals(subjectUUID)) + // Ensure the subject is not included in the stream when excluding the server to avoid duplicates + .filter((player) -> !(excludeServer && player.getUniqueId().equals(subjectUUID))) + .filter((player) -> { + if (!excludeServer) return true; + return player.getCurrentServer() + .map(ServerConnection::getServerInfo) + .map(ServerInfo::getName) + .map((name) -> !name.equalsIgnoreCase(subjectServerName)) + .orElse(true); + }) + .forEach((player) -> player.sendMessage(MiniMessage.miniMessage().deserialize(message))); + + // If excluding the server but not the subject, explicitly send to the subject to keep behavior consistent + if (excludeServer && !excludeSelf) { + plugin.getProxyServer().getPlayer(subjectUUID).ifPresent((subjectPlayer) -> { + if (plugin.getConfig().get(ConfigKey.USE_PERMISSIONS).asBoolean() + && !subjectPlayer.hasPermission(permission.getPermissionNode())) return; + if (playerIsInDisabledServer(subjectPlayer, plugin)) return; + subjectPlayer.sendMessage(MiniMessage.miniMessage().deserialize(message)); + }); + } + } + } diff --git a/projects/main-app/src/main/resources/config.yml b/projects/main-app/src/main/resources/config.yml index ff0af4c..ce24123 100644 --- a/projects/main-app/src/main/resources/config.yml +++ b/projects/main-app/src/main/resources/config.yml @@ -56,6 +56,11 @@ use-permissions: false # Example: If it is set to '$', then when a player sends $hello, it will be sent through the proxy. proxy-message-prefix: '' +# Messages that start with this character will not be sent through the plugin. +# Set to '' to disable. +# Example: If it is set to '#', then when a player sends #hello, it will not be sent through the proxy. +proxy-message-prefix-blacklist: '' + # Whether to send if the statuses of the servers connected to the proxy when the proxy starts up. # THIS IS FOR DISCORD MESSAGES ONLY. use-initial-server-status: true diff --git a/projects/main-app/src/main/resources/messages.yml b/projects/main-app/src/main/resources/messages.yml index 34bbf74..8fe3fd0 100644 --- a/projects/main-app/src/main/resources/messages.yml +++ b/projects/main-app/src/main/resources/messages.yml @@ -13,9 +13,15 @@ minecraft: join: enabled: true message: "&e%player% &ahas joined the network. (%server%)" + recipients: + exclude-self: false + exclude-server: false # excludes every player but the sender leave: enabled: true message: "&e%player% &chas left the network. (%server%)" + recipients: + exclude-self: false + exclude-server: false # excludes every player but the sender chat: enabled: true message: "&8[&3%server%&8] &e%player% &9» &7%message%" @@ -110,4 +116,4 @@ console: update-message: "&7There is an update! You are on &c%old%. New version is &a%new%&7: &6%link%" # DO NOT TOUCH THIS -file-version: 10 +file-version: 11