diff --git a/src/de/steamwar/bungeecore/BungeeCore.java b/src/de/steamwar/bungeecore/BungeeCore.java index c9f3570..24b494b 100644 --- a/src/de/steamwar/bungeecore/BungeeCore.java +++ b/src/de/steamwar/bungeecore/BungeeCore.java @@ -78,6 +78,7 @@ public class BungeeCore extends Plugin { errorLogger = new ErrorLogger(); new ConnectionListener(); new Forge(); + new Forge12(); new LabyMod(); new Badlion(); new ChatListener(); diff --git a/src/de/steamwar/bungeecore/Message.java b/src/de/steamwar/bungeecore/Message.java index b9a6f58..f755cdf 100644 --- a/src/de/steamwar/bungeecore/Message.java +++ b/src/de/steamwar/bungeecore/Message.java @@ -44,24 +44,26 @@ public class Message { }; public static TextComponent parseToComponent(String message, boolean prefixed, CommandSender sender, Object... params){ - return new TextComponent(TextComponent.fromLegacyText(parse(message, prefixed, sender, params))); + return new TextComponent(TextComponent.fromLegacyText(parse(message, prefixed, locale(sender), params))); } public static String parsePrefixed(String message, CommandSender sender, Object... params){ - return parse(message, true, sender, params); + return parse(message, true, locale(sender), params); } public static String parse(String message, CommandSender sender, Object... params){ - return parse(message, false, sender, params); + return parse(message, false, locale(sender), params); } - private static String parse(String message, boolean prefixed, CommandSender sender, Object... params){ - Locale locale = null; - if(sender instanceof ProxiedPlayer) - locale = ((ProxiedPlayer)sender).getLocale(); - if(locale == null) - locale = Locale.getDefault(); + public static String parse(String message, Locale locale, Object... params){ + return parse(message, false, locale, params); + } + private static Locale locale(CommandSender sender) { + return sender instanceof ProxiedPlayer ? ((ProxiedPlayer)sender).getLocale() : Locale.getDefault(); + } + + private static String parse(String message, boolean prefixed, Locale locale, Object... params){ ResourceBundle resourceBundle = ResourceBundle.getBundle("de.steamwar.messages.BungeeCore", locale, CONTROL); String pattern = ""; if(prefixed) @@ -72,7 +74,7 @@ public class Message { for (int i = 0; i < params.length; i++) { if(params[i] instanceof Message) { Message msg = (Message) params[i]; - params[i] = parse(msg.getMessage(), sender, msg.getParams()); + params[i] = parse(msg.getMessage(), false, locale, msg.getParams()); } else if(params[i] instanceof Date) { params[i] = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale).format((Date) params[i]); } @@ -120,7 +122,7 @@ public class Message { public static void broadcast(String message, String onHover, ClickEvent onClick, Object... params){ for(ProxiedPlayer player : ProxyServer.getInstance().getPlayers()) - send(message, player, parse(onHover, false, player, params), onClick, params); + send(message, player, parse(onHover, player, params), onClick, params); } public static void broadcast(String message, Object... params){ diff --git a/src/de/steamwar/bungeecore/listeners/mods/FMLPing.java b/src/de/steamwar/bungeecore/listeners/mods/FMLPing.java new file mode 100644 index 0000000..a8b5994 --- /dev/null +++ b/src/de/steamwar/bungeecore/listeners/mods/FMLPing.java @@ -0,0 +1,86 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2020 SteamWar.de-Serverteam + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +package de.steamwar.bungeecore.listeners.mods; + +import net.md_5.bungee.api.ServerPing; + +import java.util.*; + +public class FMLPing extends ServerPing { + + private final ForgeData forgeData; + + public FMLPing(ServerPing existing, int version) { + super(existing.getVersion(), existing.getPlayers(), existing.getDescriptionComponent(), existing.getFaviconObject()); + forgeData = new ForgeData(version); + } + + private static class ForgeData { + private final List channels = new ArrayList<>(); + private final List mods = new ArrayList<>(); + private final int fmlNetworkVersion = 2; + + public ForgeData(int versionNumber) { + channels.add(new ForgeChannel("minecraft:unregister")); + channels.add(new ForgeChannel("minecraft:register")); + channels.add(new ForgeChannel("fml:handshake")); + mods.add(new ForgeMod("minecraft", ProtocolVersion.getVersion(versionNumber))); + mods.add(new ForgeMod("forge", "ANY")); + } + + public final static class ProtocolVersion { + + private static final HashMap versions; + + static { + versions = new HashMap(); + versions.put(757, "1.18"); + versions.put(756, "1.17.1"); + versions.put(754, "1.16.5"); + versions.put(578, "1.15.2"); + versions.put(498, "1.14.1"); + versions.put(393, "1.13"); + } + + public static String getVersion(int version) { + return versions.get(version); + } + } + + private static class ForgeChannel { + private final String res; + private final String version = "FML2"; + private final boolean required = true; + + private ForgeChannel(String res) { + this.res = res; + } + } + + private static class ForgeMod { + private final String modId; + private final String modmarker; + + private ForgeMod(String modId, String modmarker) { + this.modId = modId; + this.modmarker = modmarker; + } + } + } +} diff --git a/src/de/steamwar/bungeecore/listeners/mods/Forge.java b/src/de/steamwar/bungeecore/listeners/mods/Forge.java index 905eecb..2a6a8ca 100644 --- a/src/de/steamwar/bungeecore/listeners/mods/Forge.java +++ b/src/de/steamwar/bungeecore/listeners/mods/Forge.java @@ -22,102 +22,112 @@ package de.steamwar.bungeecore.listeners.mods; import de.steamwar.bungeecore.BungeeCore; import de.steamwar.bungeecore.listeners.BasicListener; import de.steamwar.bungeecore.sql.Mod; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.UnpooledByteBufAllocator; -import net.md_5.bungee.api.ProxyServer; -import net.md_5.bungee.api.connection.Connection; -import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.api.event.PluginMessageEvent; -import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.connection.PendingConnection; +import net.md_5.bungee.api.event.LoginEvent; +import net.md_5.bungee.api.event.ProxyPingEvent; +import net.md_5.bungee.connection.InitialHandler; import net.md_5.bungee.event.EventHandler; +import net.md_5.bungee.netty.ChannelWrapper; +import net.md_5.bungee.netty.HandlerBoss; +import net.md_5.bungee.netty.PacketHandler; +import net.md_5.bungee.protocol.packet.LoginPayloadRequest; +import net.md_5.bungee.protocol.packet.LoginPayloadResponse; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.TimeUnit; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.logging.Level; public class Forge extends BasicListener { - private static final String FMLHS = "FML|HS"; - private static final String FMLHS13 = "fml:handshake"; - private static final byte[] REGISTER; - private static final byte[] REGISTER13; - private static final byte[] HELLO = new byte[]{0, 2, 0, 0, 0, 0}; - private static final Set unlocked = new HashSet<>(); + private static final String WRAPPER = "fml:loginwrapper"; + private static final Field initialHandlerCh; static{ - ByteBuf buf = UnpooledByteBufAllocator.DEFAULT.directBuffer(7); - buf.writeByte(6); - buf.writeCharSequence(FMLHS, StandardCharsets.UTF_8); - REGISTER = new byte[buf.readableBytes()]; - buf.readBytes(REGISTER); - - buf.clear(); - buf.writeByte(13); - buf.writeCharSequence(FMLHS13, StandardCharsets.UTF_8); - REGISTER13 = new byte[buf.readableBytes()]; - buf.readBytes(REGISTER13); + try { + initialHandlerCh = InitialHandler.class.getDeclaredField("ch"); + } catch (NoSuchFieldException e) { + throw new SecurityException("Could not initialize Reflection", e); + } + initialHandlerCh.setAccessible(true); } @EventHandler - public void onPostLogin(PostLoginEvent event) { - ProxiedPlayer player = event.getPlayer(); + public void onServerPing(ProxyPingEvent event) { + event.setResponse(new FMLPing(event.getResponse(), event.getConnection().getVersion())); + } - synchronized (unlocked) { - if(unlocked.contains(player.getUniqueId())){ - unlocked.remove(player.getUniqueId()); + @EventHandler + public void onServerConnected(LoginEvent event){ + if(event.getConnection().getVersion() < 340) return; //1.13+ + + //fml:handshake without mods, channels and registries + //for more information see https://wiki.vg/Minecraft_Forge_Handshake#FML2_protocol_.281.13_-_Current.29 + event.getConnection().unsafe().sendPacket(new LoginPayloadRequest(1, WRAPPER, new byte[]{13,102,109,108,58,104,97,110,100,115,104,97,107,101,4,1,0,0,0})); + + InitialHandler handler = (InitialHandler) event.getConnection(); + + ChannelWrapper wrapper; + try{ + wrapper = (ChannelWrapper) initialHandlerCh.get(handler); + } catch (IllegalAccessException e) { + BungeeCore.get().getLogger().log(Level.SEVERE, "Could not get Channel", e); + return; + } + + event.registerIntent(BungeeCore.get()); + wrapper.getHandle().pipeline().get(HandlerBoss.class).setHandler(new CustomPacketHandler(event)); + } + + private static class CustomPacketHandler extends PacketHandler { + private final LoginEvent event; + + public CustomPacketHandler(LoginEvent event) { + this.event = event; + } + + @Override + public String toString() { + return "SteamWar Forge Handler"; + } + + @Override + public void handle(LoginPayloadResponse response){ + byte[] data = response.getData(); + if(data == null) { + event.completeIntent(BungeeCore.get()); return; } - } - if(player.getPendingConnection().getVersion() > 340) { - player.sendData("minecraft:register", REGISTER13); //1.13+ - player.sendData(FMLHS13, Forge.HELLO); - }else{ - player.sendData("REGISTER", REGISTER); //1.12- - player.sendData(FMLHS, Forge.HELLO); - } - } + //for more information see https://wiki.vg/Minecraft_Forge_Handshake#FML2_protocol_.281.13_-_Current.29 + Utils.VarInt channelLength = Utils.readVarInt(data, 0); + int pos = channelLength.length; + assert new String(data, pos, channelLength.value).equals("fml:handshake"); - @EventHandler - public void onPluginMessageEvent(PluginMessageEvent e){ - if(!e.getTag().equals(FMLHS) && !e.getTag().equals(FMLHS13)) - return; + Utils.VarInt length = Utils.readVarInt(data, pos); + pos += length.length; + assert channelLength.length + channelLength.value + length.length + length.value == data.length; - e.setCancelled(true); - byte[] data = e.getData(); + Utils.VarInt packetId = Utils.readVarInt(data, pos); + pos += packetId.length; + assert packetId.value == 2; - Connection sender = e.getSender(); - if(!(sender instanceof ProxiedPlayer)) - return; - ProxiedPlayer p = (ProxiedPlayer) sender; + Utils.VarInt modCount = Utils.readVarInt(data, pos); + pos += modCount.length; - if (data[0] == 2) { - Utils.VarInt numMods = Utils.readVarInt(data, 1); - List mods = new LinkedList<>(); + List mods = new ArrayList<>(); + for(int i = 0; i < modCount.value; i++) { + Utils.VarInt nameLength = Utils.readVarInt(data, pos); + pos += nameLength.length; - int bytePos = 1 + numMods.length; - for (int i = 0; i < numMods.value; i++) { - byte[] name = Arrays.copyOfRange(data, bytePos + 1, bytePos + data[bytePos] + 1); - bytePos += 1 + data[bytePos]; - //Version information is unused - bytePos += 1 + data[bytePos]; - - mods.add(Mod.get(new String(name), Mod.Platform.FORGE)); + mods.add(Mod.get(new String(data, pos, nameLength.value), Mod.Platform.FORGE)); + pos += nameLength.value; } - if (Utils.handleMods(p, mods)) { - synchronized (unlocked) { - unlocked.add(p.getUniqueId()); - } - ProxyServer.getInstance().getScheduler().schedule(BungeeCore.get(), - () -> p.disconnect(BungeeCore.stringToText("§7Deine installierten Mods wurden überprüft\n§aDu kannst nun §eSteam§8War §abetreten")), - 2, TimeUnit.SECONDS); - ProxyServer.getInstance().getScheduler().schedule(BungeeCore.get(), () -> { - synchronized (unlocked) { - unlocked.remove(p.getUniqueId()); - } - }, 30, TimeUnit.SECONDS); - } + PendingConnection connection = event.getConnection(); + if(Utils.handleMods(connection.getUniqueId(), Locale.getDefault(), connection::disconnect, mods)) + event.completeIntent(BungeeCore.get()); } } } diff --git a/src/de/steamwar/bungeecore/listeners/mods/Forge12.java b/src/de/steamwar/bungeecore/listeners/mods/Forge12.java new file mode 100644 index 0000000..eb463f4 --- /dev/null +++ b/src/de/steamwar/bungeecore/listeners/mods/Forge12.java @@ -0,0 +1,113 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2022 SteamWar.de-Serverteam + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package de.steamwar.bungeecore.listeners.mods; + +import de.steamwar.bungeecore.BungeeCore; +import de.steamwar.bungeecore.listeners.BasicListener; +import de.steamwar.bungeecore.sql.Mod; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.connection.Connection; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.PluginMessageEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.event.EventHandler; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.TimeUnit; + +public class Forge12 extends BasicListener { + private static final String FMLHS = "FML|HS"; + private static final byte[] REGISTER; + private static final byte[] HELLO = new byte[]{0, 2, 0, 0, 0, 0}; + + private static final Set unlocked = new HashSet<>(); + + static { + ByteBuf buf = UnpooledByteBufAllocator.DEFAULT.directBuffer(7); + buf.writeByte(6); + buf.writeCharSequence(FMLHS, StandardCharsets.UTF_8); + REGISTER = new byte[buf.readableBytes()]; + buf.readBytes(REGISTER); + } + + + @EventHandler + public void onPostLogin(PostLoginEvent event) { + ProxiedPlayer player = event.getPlayer(); + + synchronized (unlocked) { + if(unlocked.contains(player.getUniqueId())){ + unlocked.remove(player.getUniqueId()); + return; + } + } + + if(player.getPendingConnection().getVersion() <= 340) { + player.sendData("REGISTER", REGISTER); //1.12- + player.sendData(FMLHS, HELLO); + } + } + + @EventHandler + public void onPluginMessageEvent(PluginMessageEvent e){ + if(!e.getTag().equals(FMLHS)) + return; + + e.setCancelled(true); + byte[] data = e.getData(); + + Connection sender = e.getSender(); + if(!(sender instanceof ProxiedPlayer)) + return; + ProxiedPlayer p = (ProxiedPlayer) sender; + + if (data[0] == 2) { + Utils.VarInt numMods = Utils.readVarInt(data, 1); + List mods = new LinkedList<>(); + + int bytePos = 1 + numMods.length; + for (int i = 0; i < numMods.value; i++) { + byte[] name = Arrays.copyOfRange(data, bytePos + 1, bytePos + data[bytePos] + 1); + bytePos += 1 + data[bytePos]; + //Version information is unused + bytePos += 1 + data[bytePos]; + + mods.add(Mod.get(new String(name), Mod.Platform.FORGE)); + } + + if (Utils.handleMods(p, mods)) { + synchronized (unlocked) { + unlocked.add(p.getUniqueId()); + } + ProxyServer.getInstance().getScheduler().schedule(BungeeCore.get(), + () -> p.disconnect(BungeeCore.stringToText("§7Deine installierten Mods wurden überprüft\n§aDu kannst nun §eSteam§8War §abetreten")), + 2, TimeUnit.SECONDS); + ProxyServer.getInstance().getScheduler().schedule(BungeeCore.get(), () -> { + synchronized (unlocked) { + unlocked.remove(p.getUniqueId()); + } + }, 30, TimeUnit.SECONDS); + } + } + } +} diff --git a/src/de/steamwar/bungeecore/listeners/mods/ModLoaderBlocker.java b/src/de/steamwar/bungeecore/listeners/mods/ModLoaderBlocker.java index 281830c..a4d54e6 100644 --- a/src/de/steamwar/bungeecore/listeners/mods/ModLoaderBlocker.java +++ b/src/de/steamwar/bungeecore/listeners/mods/ModLoaderBlocker.java @@ -56,26 +56,6 @@ public class ModLoaderBlocker extends BasicListener { } } - @EventHandler - public void onPluginMessage(PluginMessageEvent e){ - if(!e.getTag().equals("minecraft:register")) - return; - - Connection sender = e.getSender(); - if(!(sender instanceof ProxiedPlayer)) - return; - ProxiedPlayer p = (ProxiedPlayer) sender; - - if(p.getPendingConnection().getVersion() <= 340) - return; - - String registered = new String(e.getData(), StandardCharsets.UTF_8); - if(registered.contains("fml:loginwrapper") || registered.contains("fml:handshake") || registered.contains("fml:play")){ - Storage.fabricPlayers.add(p); - Message.send("MODLOADER_INSTALLED", p, "Forge"); - } - } - @EventHandler public void onDisconnect(PlayerDisconnectEvent e){ Storage.fabricPlayers.remove(e.getPlayer()); diff --git a/src/de/steamwar/bungeecore/listeners/mods/Utils.java b/src/de/steamwar/bungeecore/listeners/mods/Utils.java index 9ed160a..417f846 100644 --- a/src/de/steamwar/bungeecore/listeners/mods/Utils.java +++ b/src/de/steamwar/bungeecore/listeners/mods/Utils.java @@ -25,6 +25,8 @@ import de.steamwar.bungeecore.sql.Mod; import de.steamwar.bungeecore.sql.Mod.ModType; import de.steamwar.bungeecore.sql.Punishment; import de.steamwar.bungeecore.sql.SteamwarUser; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.connection.ProxiedPlayer; import java.sql.Timestamp; @@ -32,6 +34,9 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Iterator; import java.util.List; +import java.util.Locale; +import java.util.UUID; +import java.util.function.Consumer; import java.util.logging.Level; class Utils { @@ -55,9 +60,13 @@ class Utils { return new VarInt(numRead, result); } - static boolean handleMods(ProxiedPlayer player, List mods){ - SteamwarUser user = SteamwarUser.get(player.getUniqueId()); - boolean privileged = player.hasPermission("bungeecore.youtubermods"); + static boolean handleMods(ProxiedPlayer player, List mods) { + return handleMods(player.getUniqueId(), player.getLocale(), player::disconnect, mods); + } + + static boolean handleMods(UUID uuid, Locale locale, Consumer disconnect, List mods){ + SteamwarUser user = SteamwarUser.get(uuid); + boolean privileged = user.getUserGroup().privilegedMods(); ModType max = ModType.YELLOW; Iterator it = mods.iterator(); @@ -74,9 +83,9 @@ class Utils { if(mods.size() == 1){ if(max == ModType.YELLOW) - player.disconnect(BungeeCore.stringToText(Message.parse("MOD_YELLOW_SING", player, mods.get(0).getModName()))); + disconnect.accept(TextComponent.fromLegacyText(Message.parse("MOD_YELLOW_SING", locale, mods.get(0).getModName()))); else{ - user.punish(Punishment.PunishmentType.Ban, Timestamp.from(Instant.now().plus(7, ChronoUnit.DAYS)), Message.parse("MOD_RED_SING", player, mods.get(0).getModName()), 0, false); + user.punish(Punishment.PunishmentType.Ban, Timestamp.from(Instant.now().plus(7, ChronoUnit.DAYS)), Message.parse("MOD_RED_SING", locale, mods.get(0).getModName()), 0, false); BungeeCore.log(Level.SEVERE, user.getUserName() + " " + user.getId() + " wurde automatisch wegen des Mods " + mods.get(0).getModName() + " gebannt."); } }else{ @@ -84,10 +93,10 @@ class Utils { mods.forEach(mod -> sb.append(mod.getModName()).append('\n')); if(max == ModType.YELLOW) - player.disconnect(BungeeCore.stringToText(Message.parse("MOD_YELLOW_PLUR", player, sb.toString()))); + disconnect.accept(TextComponent.fromLegacyText(Message.parse("MOD_YELLOW_PLUR", locale, sb.toString()))); else{ - user.punish(Punishment.PunishmentType.Ban, Timestamp.from(Instant.now().plus(7, ChronoUnit.DAYS)), Message.parse("MOD_RED_PLUR", player, sb.toString()), 0, false); - BungeeCore.log(Level.SEVERE, user.getUserName() + " " + user.getId() + " wurde automatisch wegen der Mods " + sb.toString() + " gebannt."); + user.punish(Punishment.PunishmentType.Ban, Timestamp.from(Instant.now().plus(7, ChronoUnit.DAYS)), Message.parse("MOD_RED_PLUR", locale, sb.toString()), 0, false); + BungeeCore.log(Level.SEVERE, user.getUserName() + " " + user.getId() + " wurde automatisch wegen der Mods " + sb + " gebannt."); } } diff --git a/src/de/steamwar/bungeecore/sql/UserGroup.java b/src/de/steamwar/bungeecore/sql/UserGroup.java index 73273dc..e6c30f9 100644 --- a/src/de/steamwar/bungeecore/sql/UserGroup.java +++ b/src/de/steamwar/bungeecore/sql/UserGroup.java @@ -24,26 +24,28 @@ import java.util.stream.Collectors; public enum UserGroup { - Admin("§4", "§e", true, true, true), - Developer("§3", "§f", true, true, true), - Moderator("§c", "§f", true, true, true), - Supporter("§9", "§f", false, true, true), - Builder("§2", "§f", false, true, false), - YouTuber("§5", "§f", false, false, false), - Member("§7", "§7", false, false, false); + Admin("§4", "§e", true, true, true, true), + Developer("§3", "§f", true, true, true, true), + Moderator("§c", "§f", true, true, true, true), + Supporter("§9", "§f", false, true, true, true), + Builder("§2", "§f", false, true, false, true), + YouTuber("§5", "§f", false, false, false, true), + Member("§7", "§7", false, false, false, false); private final String colorCode; private final String chatColorCode; private final boolean adminGroup; private final boolean teamGroup; private final boolean checkSchematics; + private final boolean privilegedMods; - UserGroup(String colorCode, String chatColorCode, boolean adminGroup, boolean teamGroup, boolean checkSchematics) { + UserGroup(String colorCode, String chatColorCode, boolean adminGroup, boolean teamGroup, boolean checkSchematics, boolean privilegedMods) { this.colorCode = colorCode; this.chatColorCode = chatColorCode; this.adminGroup = adminGroup; this.teamGroup = teamGroup; this.checkSchematics = checkSchematics; + this.privilegedMods = privilegedMods; } public String getColorCode() { @@ -62,6 +64,10 @@ public enum UserGroup { return checkSchematics; } + public boolean privilegedMods() { + return privilegedMods; + } + public String getChatColorCode() { return chatColorCode; }