Upload folder via GUI - src
This commit is contained in:
@@ -10,6 +10,7 @@ import net.viper.status.modules.tablist.TablistModule;
|
||||
import net.viper.status.modules.scoreboard.ScoreboardModule;
|
||||
import net.viper.status.modules.antibot.AntiBotModule;
|
||||
import net.viper.status.modules.network.NetworkInfoModule;
|
||||
import net.viper.status.modules.network.MultiAccountGuard;
|
||||
import net.viper.status.modules.AutoMessage.AutoMessageModule;
|
||||
import net.viper.status.modules.customcommands.CustomCommandModule;
|
||||
import net.viper.status.modules.serverswitcher.ServerSwitcherModule;
|
||||
@@ -20,6 +21,7 @@ import net.viper.status.modules.commandblocker.CommandBlockerModule;
|
||||
import net.viper.status.modules.broadcast.BroadcastModule;
|
||||
import net.viper.status.modules.chat.ChatModule;
|
||||
import net.viper.status.modules.vanish.VanishModule;
|
||||
import net.viper.status.modules.help.HelpModule;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
@@ -106,6 +108,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
// Module in korrekter Reihenfolge registrieren
|
||||
// VanishModule MUSS vor ChatModule registriert werden (VanishProvider-Abhängigkeit)
|
||||
moduleManager.registerModule(new StatsModule());
|
||||
moduleManager.registerModule(new HelpModule());
|
||||
moduleManager.registerModule(new VerifyModule());
|
||||
moduleManager.registerModule(new BroadcastModule());
|
||||
moduleManager.registerModule(new CommandBlockerModule());
|
||||
@@ -113,6 +116,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
moduleManager.registerModule(new ChatModule());
|
||||
moduleManager.registerModule(new AntiBotModule());
|
||||
moduleManager.registerModule(new NetworkInfoModule());
|
||||
moduleManager.registerModule(new MultiAccountGuard());
|
||||
moduleManager.registerModule(new AutoMessageModule());
|
||||
moduleManager.registerModule(new CustomCommandModule());
|
||||
moduleManager.registerModule(new ServerSwitcherModule());
|
||||
@@ -1400,9 +1404,15 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
@Override
|
||||
public void execute(net.md_5.bungee.api.CommandSender sender, String[] args) {
|
||||
if (args.length == 0 || args[0].equalsIgnoreCase("help")) {
|
||||
boolean isAdmin = sender.hasPermission("statusapi.admin")
|
||||
|| !(sender instanceof net.md_5.bungee.api.connection.ProxiedPlayer);
|
||||
send(sender, "&8&m──────────────────────────────────────────");
|
||||
send(sender, "&6&lStatusAPI &7| Befehle");
|
||||
send(sender, "&e/statusapi reload &7– Scoreboard & Tablist neu laden");
|
||||
if (isAdmin) {
|
||||
send(sender, "&e/statusapi reload &7– Scoreboard & Tablist neu laden");
|
||||
} else {
|
||||
send(sender, "&7Keine weiteren Unterbefehle verfügbar.");
|
||||
}
|
||||
send(sender, "&8&m──────────────────────────────────────────");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -329,6 +329,19 @@ public class AntiBotModule implements Module, Listener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sperrt eine IP für die konfigurierte Block-Dauer (antibot.ip.block_seconds).
|
||||
* Kann von anderen Modulen aufgerufen werden (z. B. MultiAccountGuard).
|
||||
* @param ip Die zu sperrende IP-Adresse
|
||||
* @param durationSeconds Sperrdauer in Sekunden (0 = antibot-Standard verwenden)
|
||||
*/
|
||||
public void blockIpExternal(String ip, int durationSeconds) {
|
||||
long now = System.currentTimeMillis();
|
||||
long duration = durationSeconds > 0 ? durationSeconds : Math.max(1, ipBlockSeconds);
|
||||
blockedIpsUntil.put(ip, now + duration * 1000L);
|
||||
blockedConnectionsTotal.incrementAndGet();
|
||||
}
|
||||
|
||||
private void blockIp(String ip, long now) {
|
||||
blockedIpsUntil.put(ip, now + Math.max(1, ipBlockSeconds) * 1000L);
|
||||
blockedConnectionsTotal.incrementAndGet();
|
||||
@@ -837,4 +850,4 @@ public class AntiBotModule implements Module, Listener {
|
||||
sender.sendMessage(ChatColor.YELLOW + "/antibot reload");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package net.viper.status.modules.help;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.viper.status.StatusAPI;
|
||||
import net.viper.status.module.Module;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* HelpModule – seitenbasierte Ingame-Hilfe.
|
||||
*
|
||||
* verify.properties:
|
||||
* statusapi.help=vn → /vn help [seite]
|
||||
* statusapi.help.permission=statusapi.admin
|
||||
*/
|
||||
public class HelpModule implements Module {
|
||||
|
||||
private String commandName = "help";
|
||||
private String adminPermission = "statusapi.admin";
|
||||
|
||||
@Override
|
||||
public String getName() { return "HelpModule"; }
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
Properties props = ((StatusAPI) plugin).getVerifyProperties();
|
||||
if (props != null) {
|
||||
String cn = props.getProperty("statusapi.help", "help").trim();
|
||||
if (!cn.isEmpty()) commandName = cn;
|
||||
String ap = props.getProperty("statusapi.help.permission", "statusapi.admin").trim();
|
||||
if (!ap.isEmpty()) adminPermission = ap;
|
||||
}
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin,
|
||||
new HelpCommand(commandName, adminPermission));
|
||||
plugin.getLogger().info("[HelpModule] /" + commandName + " help registriert (Admin-Permission: " + adminPermission + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private static class HelpCommand extends Command {
|
||||
|
||||
private final String adminPerm;
|
||||
|
||||
// Jede Seite ist eine Liste von Zeilen
|
||||
// Seiten werden zur Laufzeit je nach Berechtigung zusammengebaut
|
||||
HelpCommand(String name, String adminPerm) {
|
||||
super(name, null);
|
||||
this.adminPerm = adminPerm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (args.length == 0) {
|
||||
send(sender, "&7Nutze &e/" + getName() + " help &7für eine Befehlsübersicht.");
|
||||
return;
|
||||
}
|
||||
if (!args[0].equalsIgnoreCase("help")) {
|
||||
send(sender, "&cUnbekannter Unterbefehl. Nutze &e/" + getName() + " help&c.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isAdmin = !(sender instanceof ProxiedPlayer)
|
||||
|| sender.hasPermission(adminPerm)
|
||||
|| sender.hasPermission("statusapi.admin");
|
||||
|
||||
// Seiten aufbauen
|
||||
List<List<String>> pages = buildPages(isAdmin);
|
||||
|
||||
int totalPages = pages.size();
|
||||
int page = 1;
|
||||
|
||||
if (args.length >= 2) {
|
||||
try {
|
||||
page = Integer.parseInt(args[1].trim());
|
||||
} catch (NumberFormatException e) {
|
||||
send(sender, "&cUngültige Seitenzahl. Nutze &e/" + getName() + " help <1-" + totalPages + ">&c.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (page < 1 || page > totalPages) {
|
||||
send(sender, "&cSeite &e" + page + " &cexistiert nicht. Verfügbar: &e1&c-&e" + totalPages + "&c.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Header
|
||||
send(sender, "");
|
||||
send(sender, "&8&m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
send(sender, " &6&lStatusAPI &8| &7Hilfe &8– &7Seite &e" + page + "&8/&e" + totalPages);
|
||||
send(sender, "&8&m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
send(sender, "");
|
||||
|
||||
// Seiteninhalte
|
||||
for (String line : pages.get(page - 1)) {
|
||||
send(sender, line);
|
||||
}
|
||||
|
||||
send(sender, "");
|
||||
|
||||
// Navigation
|
||||
sendNavigation(sender, getName(), page, totalPages);
|
||||
send(sender, "");
|
||||
}
|
||||
|
||||
/** Baut alle Seiten zusammen. Admins bekommen zusätzliche Seiten. */
|
||||
private List<List<String>> buildPages(boolean isAdmin) {
|
||||
List<List<String>> pages = new ArrayList<>();
|
||||
|
||||
// ── Seite 1: Allgemein & Chat ─────────────────────────────────────
|
||||
List<String> p1 = new ArrayList<>();
|
||||
p1.add(" &e&lAllgemein");
|
||||
p1.add(" &a/verify <token> &8– &7Account verifizieren");
|
||||
p1.add(" &a/forumlink &8(&7/fl&8) &8– &7Forum-Account verknüpfen");
|
||||
p1.add(" &a/forum &8– &7Forum-Benachrichtigungen");
|
||||
p1.add(" &a/go [server] &8(&7/wechsel, /switch&8) &8– &7Serverwechsel");
|
||||
p1.add(" &a/scoreboard &8(&7/sb&8) [hide|show] &8– &7Scoreboard umschalten");
|
||||
p1.add("");
|
||||
p1.add(" &e&lChat");
|
||||
p1.add(" &a/msg <Spieler> <Text> &8(&7/w, /tell&8) &8– &7Private Nachricht");
|
||||
p1.add(" &a/r <Text> &8(&7/reply, /antwort&8) &8– &7Auf PN antworten");
|
||||
p1.add(" &a/ignore <Spieler> &8(&7/block&8) &8– &7Spieler ignorieren");
|
||||
p1.add(" &a/unignore <Spieler> &8(&7/unblock&8) &8– &7Ignorierung aufheben");
|
||||
p1.add(" &a/channel [kanal] &8(&7/ch, /kanal&8) &8– &7Kanal wechseln");
|
||||
p1.add(" &a/chataus &8(&7/togglechat&8) &8– &7Chat-Empfang umschalten");
|
||||
pages.add(p1);
|
||||
|
||||
// ── Seite 2: Chat (weiter) & Account-Verknüpfungen ───────────────
|
||||
List<String> p2 = new ArrayList<>();
|
||||
p2.add(" &e&lChat (Fortsetzung)");
|
||||
p2.add(" &a/emoji &8(&7/emojis&8) &8– &7Alle Emojis anzeigen");
|
||||
p2.add(" &a/mentions &8(&7/mention&8) &8– &7Mention-Benachrichtigungen");
|
||||
p2.add(" &a/helpop <Nachricht> &8– &7Team um Hilfe bitten");
|
||||
p2.add(" &a/report <Spieler> <Grund> &8– &7Spieler melden");
|
||||
p2.add(" &a/chatbypass &8(&7/cbp&8) &8– &7ChatModule überspringen");
|
||||
p2.add("");
|
||||
p2.add(" &e&lAccount-Verknüpfungen");
|
||||
p2.add(" &a/discordlink &8(&7/dlink&8) &8– &7Discord verknüpfen");
|
||||
p2.add(" &a/telegramlink &8(&7/tlink&8) &8– &7Telegram verknüpfen");
|
||||
p2.add(" &a/unlink <discord|telegram|all> &8– &7Verknüpfung aufheben");
|
||||
pages.add(p2);
|
||||
|
||||
// ── Admin-Seiten nur für Berechtigte ──────────────────────────────
|
||||
if (isAdmin) {
|
||||
// ── Seite 3: StatusAPI, AntiBot, Vanish ───────────────────────
|
||||
List<String> p3 = new ArrayList<>();
|
||||
p3.add(" &c&lAdmin &8– &eStatusAPI & AntiBot");
|
||||
p3.add(" &c/statusapi reload &8(&7/sapi reload&8) &8– &7Scoreboard & Tablist neu laden");
|
||||
p3.add(" &c/netinfo &8– &7Proxy- & Systeminfos");
|
||||
p3.add("");
|
||||
p3.add(" &c/antibot status &8– &7AntiBot-Status anzeigen");
|
||||
p3.add(" &c/antibot clearblocks &8– &7IP-Blockliste leeren");
|
||||
p3.add(" &c/antibot unblock <IP> &8– &7IP entsperren");
|
||||
p3.add(" &c/antibot profile &8– &7Schutzprofil wechseln");
|
||||
p3.add(" &c/antibot reload &8– &7AntiBot neu laden");
|
||||
p3.add("");
|
||||
p3.add(" &c&lAdmin &8– &eVanish");
|
||||
p3.add(" &c/vanish [Spieler] &8(&7/v&8) &8– &7Unsichtbar schalten");
|
||||
p3.add(" &c/vanishlist &8(&7/vlist&8) &8– &7Unsichtbare Spieler anzeigen");
|
||||
pages.add(p3);
|
||||
|
||||
// ── Seite 4: Chat-Admin, Reports, sonstige ────────────────────
|
||||
List<String> p4 = new ArrayList<>();
|
||||
p4.add(" &c&lAdmin &8– &eChat-Administration");
|
||||
p4.add(" &c/broadcast <Text> &8(&7/bc, /alert&8) &8– &7Broadcast an alle");
|
||||
p4.add(" &c/chatmute <Spieler> [Min.] &8(&7/gmute&8) &8– &7Spieler muten");
|
||||
p4.add(" &c/chatunmute <Spieler> &8(&7/gunmute&8) &8– &7Mute aufheben");
|
||||
p4.add(" &c/socialspy &8(&7/spy&8) &8– &7Private Nachrichten mitlesen");
|
||||
p4.add(" &c/chatinfo <Spieler> &8– &7Chat-Info eines Spielers");
|
||||
p4.add(" &c/chathist [Spieler] [n] &8– &7Chat-Verlauf anzeigen");
|
||||
p4.add(" &c/chatreload &8– &7Chat-Konfiguration neu laden");
|
||||
p4.add("");
|
||||
p4.add(" &c&lAdmin &8– &eReports, Tools");
|
||||
p4.add(" &c/reports [all] &8– &7Offene Reports anzeigen");
|
||||
p4.add(" &c/reportclose <ID> &8– &7Report schließen");
|
||||
p4.add(" &c/automessage reload &8– &7AutoMessage neu laden");
|
||||
p4.add(" &c/bcmds reload &8– &7Custom-Commands neu laden");
|
||||
p4.add(" &c/cb <Befehl> &8– &7Command-Blocker verwalten");
|
||||
p4.add(" &c/scoreboard admin|player &8– &7Admin/Spieler-Ansicht wechseln");
|
||||
pages.add(p4);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
/** Sendet eine klickbare Navigationszeile mit ◀ Seite X/Y ▶ */
|
||||
private void sendNavigation(CommandSender sender, String cmd, int page, int total) {
|
||||
// Für Konsole: einfacher Text
|
||||
if (!(sender instanceof ProxiedPlayer)) {
|
||||
String nav = " ";
|
||||
if (page > 1) nav += "&7[&e◀&7] ";
|
||||
nav += "&8Seite &e" + page + "&8/&e" + total;
|
||||
if (page < total) nav += " &7[&e▶&7]";
|
||||
send(sender, nav);
|
||||
return;
|
||||
}
|
||||
|
||||
// Für Spieler: klickbare Buttons
|
||||
TextComponent line = new TextComponent(" ");
|
||||
|
||||
// ◀ zurück
|
||||
if (page > 1) {
|
||||
TextComponent prev = new TextComponent(
|
||||
ChatColor.translateAlternateColorCodes('&', "&7[&e◀&7] "));
|
||||
prev.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND,
|
||||
"/" + cmd + " help " + (page - 1)));
|
||||
prev.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
new ComponentBuilder(ChatColor.translateAlternateColorCodes('&',
|
||||
"&7Seite &e" + (page - 1) + " &7anzeigen")).create()));
|
||||
line.addExtra(prev);
|
||||
}
|
||||
|
||||
// Seitenanzeige
|
||||
TextComponent mid = new TextComponent(
|
||||
ChatColor.translateAlternateColorCodes('&',
|
||||
"&8Seite &e" + page + "&8/&e" + total));
|
||||
line.addExtra(mid);
|
||||
|
||||
// ▶ vor
|
||||
if (page < total) {
|
||||
TextComponent next = new TextComponent(
|
||||
ChatColor.translateAlternateColorCodes('&', " &7[&e▶&7]"));
|
||||
next.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND,
|
||||
"/" + cmd + " help " + (page + 1)));
|
||||
next.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
new ComponentBuilder(ChatColor.translateAlternateColorCodes('&',
|
||||
"&7Seite &e" + (page + 1) + " &7anzeigen")).create()));
|
||||
line.addExtra(next);
|
||||
}
|
||||
|
||||
sender.sendMessage(line);
|
||||
}
|
||||
|
||||
private static void send(CommandSender s, String text) {
|
||||
s.sendMessage(new TextComponent(
|
||||
ChatColor.translateAlternateColorCodes('&', text)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package net.viper.status.modules.network;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import net.luckperms.api.LuckPermsProvider;
|
||||
import net.luckperms.api.model.user.User;
|
||||
import net.luckperms.api.node.Node;
|
||||
import net.luckperms.api.node.types.PermissionNode;
|
||||
import net.viper.status.StatusAPI;
|
||||
import net.viper.status.module.Module;
|
||||
import net.viper.status.modules.antibot.AntiBotModule;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* MultiAccountGuard
|
||||
*
|
||||
* Features:
|
||||
* - IP-Check: blockiert zweiten Account von gleicher IP
|
||||
* - Bypass NUR über LuckPerms (OP zählt nicht)
|
||||
* - Persistentes Log in multiaccountguard.log
|
||||
* - Staff-Benachrichtigung ingame (Permission: statusapi.staff.notify)
|
||||
* - Temporärer IP-Bann nach X Versuchen (Integration mit AntiBotModule)
|
||||
* - Discord-Webhook bei Konflikt
|
||||
*/
|
||||
public class MultiAccountGuard implements Module, Listener {
|
||||
|
||||
private static final String CONFIG_FILE = "network-guard.properties";
|
||||
private static final String LOG_FILE = "multiaccountguard.log";
|
||||
public static final String BYPASS_PERM = "statusapi.multiaccountguard.bypass";
|
||||
public static final String STAFF_PERM = "statusapi.staff.notify";
|
||||
|
||||
private static final DateTimeFormatter LOG_FMT =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
|
||||
|
||||
private Plugin plugin;
|
||||
private Logger log;
|
||||
private File logFile;
|
||||
|
||||
// Config
|
||||
private boolean enabled = true;
|
||||
private boolean checkIp = true;
|
||||
private boolean kickExisting = false;
|
||||
private String kickMessage = "&cDu bist bereits mit einem anderen Account online!\n&7Bitte trenne deinen anderen Account zuerst.";
|
||||
|
||||
// Staff-Benachrichtigung
|
||||
private boolean staffNotifyEnabled = true;
|
||||
private String staffNotifyFormat = "&8[&cMAG&8] &e{blocked} &7wurde blockiert &8(2. Account von &e{existing}&8) &7| IP: &f{ip}";
|
||||
|
||||
// Temporärer IP-Bann
|
||||
private boolean tempBanEnabled = true;
|
||||
private int tempBanMaxAttempts = 3;
|
||||
private int tempBanDurationSecs = 300;
|
||||
/** IP → Anzahl Konflikte seit letztem Reset */
|
||||
private final Map<String, Integer> attemptsByIp = new ConcurrentHashMap<>();
|
||||
|
||||
// Webhook
|
||||
private boolean webhookEnabled = true;
|
||||
private String webhookUrl = "";
|
||||
private String webhookUsername = "StatusAPI";
|
||||
private String webhookThumbnailUrl = "";
|
||||
|
||||
@Override public String getName() { return "MultiAccountGuard"; }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Enable / Disable
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.log = plugin.getLogger();
|
||||
this.logFile = new File(plugin.getDataFolder(), LOG_FILE);
|
||||
|
||||
loadConfig();
|
||||
|
||||
if (!enabled) {
|
||||
log.info("[MultiAccountGuard] Deaktiviert.");
|
||||
return;
|
||||
}
|
||||
|
||||
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
|
||||
|
||||
log.info("[MultiAccountGuard] Aktiv | IP-Check=" + checkIp
|
||||
+ " | kickExisting=" + kickExisting
|
||||
+ " | staffNotify=" + staffNotifyEnabled
|
||||
+ " | tempBan=" + tempBanEnabled + "(max=" + tempBanMaxAttempts + ", " + tempBanDurationSecs + "s)"
|
||||
+ " | Webhook=" + (webhookEnabled && !webhookUrl.isEmpty()));
|
||||
log.info("[MultiAccountGuard] Bypass NUR via LuckPerms: /lp user <Name> permission set " + BYPASS_PERM + " true");
|
||||
log.info("[MultiAccountGuard] Log-Datei: " + logFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Event
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
if (!enabled) return;
|
||||
|
||||
ProxiedPlayer joining = event.getPlayer();
|
||||
|
||||
if (hasBypass(joining)) {
|
||||
log.info("[MultiAccountGuard] " + joining.getName() + " hat Bypass (LuckPerms) – übersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
UUID joiningUuid = joining.getUniqueId();
|
||||
String joiningIp = extractIp(joining.getSocketAddress());
|
||||
|
||||
if (joiningIp == null) {
|
||||
log.warning("[MultiAccountGuard] Konnte IP von " + joining.getName() + " nicht lesen – übersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[MultiAccountGuard] Login-Check: " + joining.getName()
|
||||
+ " | UUID=" + joiningUuid + " | IP=" + joiningIp);
|
||||
|
||||
// Alle anderen Spieler (sich selbst per UUID ausschließen)
|
||||
List<ProxiedPlayer> others = new ArrayList<>();
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
if (p.getUniqueId().equals(joiningUuid)) continue;
|
||||
others.add(p);
|
||||
}
|
||||
|
||||
for (ProxiedPlayer online : others) {
|
||||
if (hasBypass(online)) continue;
|
||||
|
||||
String onlineIp = extractIp(online.getSocketAddress());
|
||||
if (onlineIp == null) continue;
|
||||
|
||||
if (checkIp && joiningIp.equals(onlineIp)) {
|
||||
log.warning("[MultiAccountGuard] KONFLIKT: "
|
||||
+ joining.getName() + " (" + joiningUuid + ")"
|
||||
+ " <-> " + online.getName() + " (" + online.getUniqueId() + ")"
|
||||
+ " IP=" + joiningIp);
|
||||
handleConflict(joining, online, joiningIp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[MultiAccountGuard] " + joining.getName() + " – kein Konflikt.");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Konflikt behandeln
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void handleConflict(ProxiedPlayer joining, ProxiedPlayer existing, String ip) {
|
||||
|
||||
TextComponent msg = new TextComponent(
|
||||
ChatColor.translateAlternateColorCodes('&', kickMessage));
|
||||
|
||||
final String blockedName, allowedName;
|
||||
final UUID blockedUuid, allowedUuid;
|
||||
|
||||
if (kickExisting) {
|
||||
existing.disconnect(msg);
|
||||
blockedName = existing.getName(); blockedUuid = existing.getUniqueId();
|
||||
allowedName = joining.getName(); allowedUuid = joining.getUniqueId();
|
||||
log.warning("[MultiAccountGuard] Bestehender Account " + existing.getName() + " getrennt.");
|
||||
} else {
|
||||
joining.disconnect(msg);
|
||||
blockedName = joining.getName(); blockedUuid = joining.getUniqueId();
|
||||
allowedName = existing.getName(); allowedUuid = existing.getUniqueId();
|
||||
log.warning("[MultiAccountGuard] Neuer Account " + joining.getName() + " blockiert.");
|
||||
}
|
||||
|
||||
// 1. Persistentes Log
|
||||
writeLog(blockedName, blockedUuid, allowedName, allowedUuid, ip);
|
||||
|
||||
// 2. Staff-Benachrichtigung
|
||||
if (staffNotifyEnabled) {
|
||||
notifyStaff(blockedName, allowedName, ip);
|
||||
}
|
||||
|
||||
// 3. Temporärer IP-Bann
|
||||
if (tempBanEnabled) {
|
||||
int attempts = attemptsByIp.merge(ip, 1, Integer::sum);
|
||||
log.info("[MultiAccountGuard] IP " + ip + " hat " + attempts + "/" + tempBanMaxAttempts + " Versuche.");
|
||||
if (attempts >= tempBanMaxAttempts) {
|
||||
attemptsByIp.remove(ip);
|
||||
banIp(ip);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Discord-Webhook (async)
|
||||
if (webhookEnabled && webhookUrl != null && !webhookUrl.isEmpty()) {
|
||||
final String bn = blockedName, an = allowedName;
|
||||
final UUID bu = blockedUuid, au = allowedUuid;
|
||||
final int att = attemptsByIp.getOrDefault(ip, tempBanMaxAttempts);
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin,
|
||||
() -> sendWebhook(bn, bu, an, au, ip, att));
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Persistentes Log
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void writeLog(String blockedName, UUID blockedUuid,
|
||||
String allowedName, UUID allowedUuid, String ip) {
|
||||
try {
|
||||
if (!logFile.getParentFile().exists()) logFile.getParentFile().mkdirs();
|
||||
|
||||
String line = String.format("[%s] KONFLIKT | Geblockt: %s (%s) | Online: %s (%s) | IP: %s%n",
|
||||
LOG_FMT.format(Instant.now()),
|
||||
blockedName, blockedUuid,
|
||||
allowedName, allowedUuid,
|
||||
ip);
|
||||
|
||||
Files.write(logFile.toPath(), line.getBytes(StandardCharsets.UTF_8),
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warning("[MultiAccountGuard] Log-Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Staff-Benachrichtigung
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void notifyStaff(String blockedName, String existingName, String ip) {
|
||||
String raw = staffNotifyFormat
|
||||
.replace("{blocked}", blockedName)
|
||||
.replace("{existing}", existingName)
|
||||
.replace("{ip}", ip);
|
||||
String formatted = ChatColor.translateAlternateColorCodes('&', raw);
|
||||
TextComponent msg = new TextComponent(formatted);
|
||||
|
||||
int notified = 0;
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
if (p.hasPermission(STAFF_PERM)) {
|
||||
p.sendMessage(msg);
|
||||
notified++;
|
||||
}
|
||||
}
|
||||
log.info("[MultiAccountGuard] Staff-Benachrichtigung gesendet an " + notified + " Spieler.");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Temporärer IP-Bann via AntiBotModule
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void banIp(String ip) {
|
||||
try {
|
||||
StatusAPI statusApi = (StatusAPI) ProxyServer.getInstance()
|
||||
.getPluginManager().getPlugin("StatusAPI");
|
||||
if (statusApi == null) {
|
||||
log.warning("[MultiAccountGuard] StatusAPI nicht gefunden – IP-Bann nicht möglich.");
|
||||
return;
|
||||
}
|
||||
AntiBotModule antiBot = statusApi.getModuleManager().getModule(AntiBotModule.class);
|
||||
if (antiBot == null) {
|
||||
log.warning("[MultiAccountGuard] AntiBotModule nicht gefunden – IP-Bann nicht möglich.");
|
||||
return;
|
||||
}
|
||||
antiBot.blockIpExternal(ip, tempBanDurationSecs);
|
||||
log.warning("[MultiAccountGuard] IP " + ip + " für " + tempBanDurationSecs + "s gebannt (zu viele Multi-Account-Versuche).");
|
||||
|
||||
// Staff über den Bann informieren
|
||||
String banMsg = ChatColor.translateAlternateColorCodes('&',
|
||||
"&8[&cMAG&8] &7IP &f" + ip + " &7wurde für &c" + tempBanDurationSecs + "s &7gebannt &8(zu viele Versuche).");
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
if (p.hasPermission(STAFF_PERM)) {
|
||||
p.sendMessage(new TextComponent(banMsg));
|
||||
}
|
||||
}
|
||||
|
||||
// In Log schreiben
|
||||
try {
|
||||
String line = String.format("[%s] IP-BANN | IP: %s | Dauer: %ds%n",
|
||||
LOG_FMT.format(Instant.now()), ip, tempBanDurationSecs);
|
||||
Files.write(logFile.toPath(), line.getBytes(StandardCharsets.UTF_8),
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warning("[MultiAccountGuard] IP-Bann Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Discord-Webhook
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void sendWebhook(String blockedName, UUID blockedUuid,
|
||||
String allowedName, UUID allowedUuid,
|
||||
String ip, int attempts) {
|
||||
StringBuilder fields = new StringBuilder();
|
||||
appendField(fields, "\uD83D\uDEAB Geblockter Account",
|
||||
blockedName + "\n`" + blockedUuid + "`", false);
|
||||
appendField(fields, "\u2705 Verbundener Account",
|
||||
allowedName + "\n`" + allowedUuid + "`", false);
|
||||
appendField(fields, "\uD83C\uDF10 IP", "`" + ip + "`", true);
|
||||
appendField(fields, "Aktion",
|
||||
kickExisting ? "Alter Account getrennt" : "Neuer Account blockiert", true);
|
||||
if (tempBanEnabled) {
|
||||
appendField(fields, "\u26A0\uFE0F Versuche",
|
||||
attempts + " / " + tempBanMaxAttempts
|
||||
+ (attempts >= tempBanMaxAttempts ? " \u2192 IP gebannt!" : ""), true);
|
||||
}
|
||||
|
||||
String body = "{\"username\":\"" + esc(webhookUsername) + "\","
|
||||
+ "\"embeds\":[{"
|
||||
+ "\"title\":\"\uD83D\uDD12 Multi-Account erkannt\","
|
||||
+ "\"description\":\"Ein Spieler hat versucht mit einem zweiten Account beizutreten.\","
|
||||
+ "\"color\":15158332,"
|
||||
+ "\"fields\":[" + fields + "],"
|
||||
+ "\"footer\":{\"text\":\"StatusAPI \u2022 MultiAccountGuard\"},"
|
||||
+ "\"timestamp\":\"" + Instant.now() + "\""
|
||||
+ (webhookThumbnailUrl != null && !webhookThumbnailUrl.isEmpty()
|
||||
? ",\"thumbnail\":{\"url\":\"" + esc(webhookThumbnailUrl) + "\"}" : "")
|
||||
+ "}]}";
|
||||
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
conn = (HttpURLConnection) new URL(webhookUrl).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(8000);
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
conn.setRequestProperty("Content-Length", String.valueOf(bytes.length));
|
||||
try (OutputStream os = conn.getOutputStream()) { os.write(bytes); }
|
||||
int code = conn.getResponseCode();
|
||||
if (code < 200 || code >= 300) log.warning("[MultiAccountGuard] Webhook HTTP " + code);
|
||||
else log.info("[MultiAccountGuard] Webhook gesendet (HTTP " + code + ")");
|
||||
} catch (Exception e) {
|
||||
log.warning("[MultiAccountGuard] Webhook-Fehler: " + e.getMessage());
|
||||
} finally {
|
||||
if (conn != null) conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void appendField(StringBuilder sb, String name, String value, boolean inline) {
|
||||
if (sb.length() > 0) sb.append(",");
|
||||
sb.append("{\"name\":\"").append(esc(name))
|
||||
.append("\",\"value\":\"").append(esc(value))
|
||||
.append("\",\"inline\":").append(inline).append("}");
|
||||
}
|
||||
|
||||
private String esc(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\","\\\\").replace("\"","\\\"")
|
||||
.replace("\n","\\n").replace("\r","\\r");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Bypass – NUR LuckPerms, kein OP-Fallback
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private boolean hasBypass(ProxiedPlayer player) {
|
||||
try {
|
||||
User user = LuckPermsProvider.get().getUserManager().getUser(player.getUniqueId());
|
||||
if (user == null) return false;
|
||||
for (Node node : user.getNodes()) {
|
||||
if (node instanceof PermissionNode) {
|
||||
PermissionNode pn = (PermissionNode) node;
|
||||
if (pn.getPermission().equalsIgnoreCase(BYPASS_PERM) && pn.getValue()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.warning("[MultiAccountGuard] LuckPerms-Check fehlgeschlagen für " + player.getName() + ": " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Config
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void loadConfig() {
|
||||
File file = new File(plugin.getDataFolder(), CONFIG_FILE);
|
||||
if (!file.exists()) {
|
||||
log.info("[MultiAccountGuard] Config nicht gefunden – Defaults werden verwendet.");
|
||||
return;
|
||||
}
|
||||
Properties p = new Properties();
|
||||
try (FileInputStream fis = new FileInputStream(file);
|
||||
InputStreamReader r = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
|
||||
p.load(r);
|
||||
|
||||
enabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.enabled", "true"));
|
||||
checkIp = Boolean.parseBoolean(p.getProperty("multiaccountguard.check_ip", "true"));
|
||||
kickExisting = Boolean.parseBoolean(p.getProperty("multiaccountguard.kick_existing", "false"));
|
||||
kickMessage = p.getProperty("multiaccountguard.kick_message", kickMessage);
|
||||
|
||||
staffNotifyEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.staff_notify.enabled", "true"));
|
||||
staffNotifyFormat = p.getProperty("multiaccountguard.staff_notify.format", staffNotifyFormat);
|
||||
|
||||
tempBanEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.tempban.enabled", "true"));
|
||||
tempBanMaxAttempts = parseInt(p.getProperty("multiaccountguard.tempban.max_attempts", "3"), 3);
|
||||
tempBanDurationSecs = parseInt(p.getProperty("multiaccountguard.tempban.duration_secs", "300"), 300);
|
||||
|
||||
webhookEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.webhook.enabled", "true"));
|
||||
webhookUrl = p.getProperty("networkinfo.webhook.url", "").trim();
|
||||
webhookUsername = p.getProperty("networkinfo.webhook.username", "StatusAPI").trim();
|
||||
webhookThumbnailUrl = p.getProperty("networkinfo.webhook.thumbnail_url", "").trim();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warning("[MultiAccountGuard] Config-Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private int parseInt(String s, int fallback) {
|
||||
try { return Integer.parseInt(s == null ? "" : s.trim()); }
|
||||
catch (Exception ignored) { return fallback; }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private String extractIp(SocketAddress addr) {
|
||||
if (addr instanceof InetSocketAddress)
|
||||
return ((InetSocketAddress) addr).getAddress().getHostAddress();
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public boolean isCheckIp() { return checkIp; }
|
||||
public boolean isKickExisting() { return kickExisting; }
|
||||
}
|
||||
@@ -7,9 +7,9 @@ networkinfo.command.enabled=true
|
||||
networkinfo.include_player_names=false
|
||||
|
||||
# Discord Webhook fuer Status-, Warn- und Attack-Meldungen
|
||||
networkinfo.webhook.enabled=true
|
||||
networkinfo.webhook.enabled=false
|
||||
networkinfo.webhook.url=
|
||||
networkinfo.webhook.username=StatusAPI
|
||||
networkinfo.webhook.username=
|
||||
networkinfo.webhook.thumbnail_url=
|
||||
networkinfo.webhook.notify_start_stop=true
|
||||
# compact = kurze Texte | detailed = strukturierte Embeds mit Feldern
|
||||
@@ -27,9 +27,9 @@ networkinfo.alert.tps_threshold=18.0
|
||||
# Attack Meldungen (Detected/Stopped)
|
||||
networkinfo.attack.enabled=true
|
||||
# Nutzt automatisch networkinfo.webhook.url
|
||||
networkinfo.attack.source=Viper-Network
|
||||
networkinfo.attack.source=
|
||||
# API-Key fuer POST /network/attack
|
||||
networkinfo.attack.api_key=2jN8xQ4mL9vK3sT7pR1yW6dH5cF0bZ
|
||||
networkinfo.attack.api_key=
|
||||
|
||||
# ===========================
|
||||
# ANTIBOT / ATTACK GUARD
|
||||
@@ -99,4 +99,37 @@ backendguard.allowed_proxy_cidrs=10.0.0.0/24
|
||||
|
||||
# Optionaler API-Key fuer GET /network/backendguard/config
|
||||
# Leer = kein API-Key erforderlich (nur im internen Netzwerk empfohlen)
|
||||
backendguard.sync.api_key=bgSync_7Rk9pQ2nLm5xV8cH4tW1yZ6
|
||||
backendguard.sync.api_key=
|
||||
|
||||
# ===========================
|
||||
# MULTI ACCOUNT GUARD
|
||||
# ===========================
|
||||
# Verhindert, dass ein Spieler mit zwei Accounts gleichzeitig online ist.
|
||||
multiaccountguard.enabled=true
|
||||
|
||||
# IP-Check: Gleiche IP mit unterschiedlichem Namen -> blockieren
|
||||
multiaccountguard.check_ip=true
|
||||
|
||||
# UUID-Check: Gleiche UUID mit unterschiedlichem Namen -> blockieren (Bedrock-Edge-Cases)
|
||||
multiaccountguard.check_uuid=true
|
||||
|
||||
# true = bestehenden (alten) Account rauswerfen, neuen reinlassen
|
||||
# false = neuen Account blockieren (Standard)
|
||||
multiaccountguard.kick_existing=false
|
||||
|
||||
# Kick-Nachricht (& fuer Farbcodes, \n fuer Zeilenumbruch)
|
||||
multiaccountguard.kick_message=&cDu bist bereits mit einem anderen Account online!\n&7Bitte trenne deinen anderen Account zuerst.
|
||||
|
||||
# Staff-Benachrichtigung bei Konflikt (Permission: statusapi.staff.notify)
|
||||
multiaccountguard.staff_notify.enabled=true
|
||||
multiaccountguard.staff_notify.format=&8[&cMAG&8] &e{blocked} &7wurde blockiert &8(2. Account von &e{existing}&8) &7| IP: &f{ip}
|
||||
|
||||
# Temporaerer IP-Bann nach X Versuchen (Integration mit AntiBotModule)
|
||||
# max_attempts: Anzahl Konflikte bevor die IP gebannt wird
|
||||
# duration_secs: Bann-Dauer in Sekunden
|
||||
multiaccountguard.tempban.enabled=true
|
||||
multiaccountguard.tempban.max_attempts=3
|
||||
multiaccountguard.tempban.duration_secs=300
|
||||
|
||||
# Discord-Meldung bei jedem Konflikt (nutzt networkinfo.webhook.url automatisch)
|
||||
multiaccountguard.webhook.enabled=true
|
||||
@@ -1,6 +1,6 @@
|
||||
name: StatusAPI
|
||||
main: net.viper.status.StatusAPI
|
||||
version: 4.1.2
|
||||
version: 4.1.3
|
||||
author: M_Viper
|
||||
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
|
||||
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
|
||||
@@ -10,6 +10,13 @@ softdepend:
|
||||
- Geyser-BungeeCord
|
||||
|
||||
commands:
|
||||
# ── HelpModule ────────────────────────────────────────────
|
||||
help:
|
||||
description: Zeigt alle verfügbaren Befehle (Admin-Befehle nur mit Berechtigung)
|
||||
usage: /<command> help
|
||||
# Hinweis: Der Befehlsname ist in verify.properties unter statusapi.help konfigurierbar
|
||||
# Beispiel: statusapi.help=vn → /vn help
|
||||
|
||||
# ── ScoreboardModule ──────────────────────────────────────
|
||||
scoreboard:
|
||||
description: Scoreboard ein-/ausblenden oder zwischen Player/Admin wechseln
|
||||
@@ -219,6 +226,16 @@ permissions:
|
||||
description: Zugriff auf /automessage reload
|
||||
default: op
|
||||
|
||||
# ── MultiAccountGuard ─────────────────────────────────────
|
||||
# KEIN default – Permission muss manuell vergeben werden!
|
||||
# lp user <Name> permission set statusapi.multiaccountguard.bypass true
|
||||
statusapi.multiaccountguard.bypass:
|
||||
description: Erlaubt mehrere gleichzeitige Accounts (nur manuell vergeben)
|
||||
|
||||
statusapi.staff.notify:
|
||||
description: Empfaengt Ingame-Benachrichtigungen vom MultiAccountGuard
|
||||
default: false
|
||||
|
||||
# ── ChatModule – Kanaele ──────────────────────────────────
|
||||
chat.channel.local:
|
||||
description: Zugang zum Local-Kanal
|
||||
@@ -292,4 +309,4 @@ permissions:
|
||||
# ── ServerSwitcherModule ──────────────────────────────────
|
||||
serverswitcher.use:
|
||||
description: Zugriff auf /go (Schneller Serverwechsel)
|
||||
default: false
|
||||
default: false
|
||||
@@ -17,7 +17,16 @@ broadcast.format=%prefixColored% %messageColored%
|
||||
# ===========================
|
||||
statusapi.port=9191
|
||||
|
||||
# ===========================
|
||||
# INGAME HILFE
|
||||
# ===========================
|
||||
# Befehlsname für die Ingame-Hilfe (Standard: help)
|
||||
# Beispiel: statusapi.help=vn → Befehl wird /vn
|
||||
statusapi.help=sapi
|
||||
|
||||
# Permission, die Admin-Befehle in der Hilfe sichtbar macht
|
||||
# (OP und Spieler mit dieser Permission sehen die Admin-Sektion)
|
||||
statusapi.help.permission=statusapi.admin
|
||||
|
||||
# ===========================
|
||||
# WORDPRESS / VERIFY EINSTELLUNGEN
|
||||
@@ -93,3 +102,5 @@ economy.mysql.database=survivalplus
|
||||
economy.mysql.username=root
|
||||
economy.mysql.password=
|
||||
economy.start-balance=500.0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user