From 28d4886584a96b00f85caa922cea8ff44c28bfad Mon Sep 17 00:00:00 2001 From: Chaoscaot Date: Sun, 4 Aug 2024 21:05:46 +0200 Subject: [PATCH] Add VelocityCore Module --- VelocityCore/Persistent/build.gradle.kts | 29 + .../de/steamwar/persistent/Arenaserver.java | 37 + .../src/de/steamwar/persistent/Bauserver.java | 60 ++ .../de/steamwar/persistent/Builderserver.java | 59 ++ .../de/steamwar/persistent/Persistent.java | 203 +++++ .../de/steamwar/persistent/Reflection.java | 75 ++ .../steamwar/persistent/ReloadablePlugin.java | 28 + .../de/steamwar/persistent/Servertype.java | 26 + .../src/de/steamwar/persistent/Storage.java | 52 ++ .../src/de/steamwar/persistent/Subserver.java | 263 +++++++ VelocityCore/build.gradle.kts | 60 ++ VelocityCore/src/GDPRQueryREADME.md | 29 + .../src/de/steamwar/command/SWCommand.java | 183 +++++ .../src/de/steamwar/command/TypeMapper.java | 29 + .../src/de/steamwar/command/TypeUtils.java | 75 ++ .../de/steamwar/command/TypeValidator.java | 24 + .../steamwar/messages/BungeeCore.properties | 730 ++++++++++++++++++ .../messages/BungeeCore_de.properties | 683 ++++++++++++++++ .../src/de/steamwar/messages/Chatter.java | 276 +++++++ .../de/steamwar/messages/ChatterGroup.java | 63 ++ .../src/de/steamwar/messages/Message.java | 22 + .../de/steamwar/messages/PlayerChatter.java | 64 ++ .../messages/SteamwarResourceBundle.java | 53 ++ .../src/de/steamwar/sql/SQLConfigImpl.java | 37 + .../src/de/steamwar/sql/SQLWrapperImpl.java | 83 ++ .../de/steamwar/velocitycore/ArenaMode.java | 121 +++ .../de/steamwar/velocitycore/Broadcaster.java | 44 ++ .../src/de/steamwar/velocitycore/Config.java | 88 +++ .../de/steamwar/velocitycore/ErrorLogger.java | 83 ++ .../steamwar/velocitycore/EventStarter.java | 90 +++ .../steamwar/velocitycore/GameModeConfig.java | 92 +++ .../src/de/steamwar/velocitycore/Node.java | 218 ++++++ .../steamwar/velocitycore/ServerStarter.java | 373 +++++++++ .../steamwar/velocitycore/ServerVersion.java | 87 +++ .../velocitycore/SubserverSystem.java | 58 ++ .../steamwar/velocitycore/VelocityCore.java | 282 +++++++ .../velocitycore/commands/AlertCommand.java | 46 ++ .../velocitycore/commands/ArenaCommand.java | 72 ++ .../velocitycore/commands/BauCommand.java | 251 ++++++ .../velocitycore/commands/BugCommand.java | 39 + .../commands/BuilderCloudCommand.java | 167 ++++ .../commands/ChallengeCommand.java | 99 +++ .../velocitycore/commands/CheckCommand.java | 296 +++++++ .../velocitycore/commands/DevCommand.java | 127 +++ .../velocitycore/commands/EventCommand.java | 117 +++ .../commands/EventRescheduleCommand.java | 66 ++ .../commands/EventreloadCommand.java | 36 + .../velocitycore/commands/FightCommand.java | 141 ++++ .../velocitycore/commands/GDPRQuery.java | 284 +++++++ .../velocitycore/commands/HelpCommand.java | 117 +++ .../commands/HistoricCommand.java | 41 + .../velocitycore/commands/IgnoreCommand.java | 49 ++ .../velocitycore/commands/JoinmeCommand.java | 38 + .../velocitycore/commands/KickCommand.java | 42 + .../velocitycore/commands/ListCommand.java | 71 ++ .../velocitycore/commands/LocalCommand.java | 36 + .../velocitycore/commands/ModCommand.java | 120 +++ .../velocitycore/commands/MsgCommand.java | 63 ++ .../velocitycore/commands/PingCommand.java | 35 + .../commands/PlaytimeCommand.java | 43 ++ .../velocitycore/commands/PollCommand.java | 64 ++ .../commands/PollresultCommand.java | 50 ++ .../commands/PunishmentCommand.java | 274 +++++++ .../velocitycore/commands/RCommand.java | 37 + .../velocitycore/commands/RankCommand.java | 64 ++ .../velocitycore/commands/ReplayCommand.java | 88 +++ .../velocitycore/commands/RulesCommand.java | 41 + .../commands/ServerSwitchCommand.java | 40 + .../commands/ServerTeamchatCommand.java | 37 + .../commands/SetLocaleCommand.java | 41 + .../velocitycore/commands/StatCommand.java | 57 ++ .../velocitycore/commands/TeamCommand.java | 601 ++++++++++++++ .../commands/TeamchatCommand.java | 45 ++ .../velocitycore/commands/TpCommand.java | 169 ++++ .../commands/TutorialCommand.java | 163 ++++ .../velocitycore/commands/TypeMappers.java | 79 ++ .../commands/UnIgnoreCommand.java | 45 ++ .../velocitycore/commands/VerifyCommand.java | 60 ++ .../commands/WebpasswordCommand.java | 72 ++ .../velocitycore/commands/WhoisCommand.java | 176 +++++ .../velocitycore/discord/DiscordBot.java | 218 ++++++ .../velocitycore/discord/DiscordConfig.java | 70 ++ .../discord/DiscordTicketType.java | 46 ++ .../discord/channels/ChecklistChannel.java | 48 ++ .../discord/channels/DiscordChannel.java | 99 +++ .../discord/channels/DiscordChatRoom.java | 52 ++ .../discord/channels/EventChannel.java | 119 +++ .../discord/channels/InteractionReply.java | 65 ++ .../channels/StaticMessageChannel.java | 69 ++ .../discord/listeners/ChannelListener.java | 91 +++ .../discord/listeners/DiscordSchemUpload.java | 90 +++ .../discord/listeners/DiscordTeamEvent.java | 67 ++ .../listeners/DiscordTicketHandler.java | 165 ++++ .../discord/util/AuthManager.java | 72 ++ .../discord/util/DiscordAlert.java | 64 ++ .../discord/util/DiscordRanks.java | 59 ++ .../velocitycore/inventory/InvCallback.java | 61 ++ .../velocitycore/inventory/SWInventory.java | 105 +++ .../velocitycore/inventory/SWItem.java | 90 +++ .../velocitycore/inventory/SWListInv.java | 88 +++ .../velocitycore/inventory/SWStreamInv.java | 71 ++ .../velocitycore/listeners/BanListener.java | 85 ++ .../velocitycore/listeners/BasicListener.java | 29 + .../velocitycore/listeners/ChatListener.java | 265 +++++++ .../velocitycore/listeners/CheckListener.java | 74 ++ .../listeners/ConnectionListener.java | 100 +++ .../listeners/EventModeListener.java | 49 ++ .../velocitycore/listeners/IPSanitizer.java | 52 ++ .../velocitycore/listeners/PluginMessage.java | 507 ++++++++++++ .../velocitycore/listeners/PollSystem.java | 91 +++ .../listeners/SessionManager.java | 48 ++ .../listeners/SettingsChangedListener.java | 41 + .../de/steamwar/velocitycore/mods/Alpine.java | 71 ++ .../steamwar/velocitycore/mods/Badlion.java | 55 ++ .../velocitycore/mods/Controlify.java | 53 ++ .../de/steamwar/velocitycore/mods/FML.java | 95 +++ .../de/steamwar/velocitycore/mods/FML2.java | 176 +++++ .../velocitycore/mods/FabricModSender.java | 144 ++++ .../steamwar/velocitycore/mods/Feather.java | 57 ++ .../steamwar/velocitycore/mods/Hostname.java | 84 ++ .../steamwar/velocitycore/mods/LabyMod.java | 87 +++ .../de/steamwar/velocitycore/mods/Lunar.java | 130 ++++ .../steamwar/velocitycore/mods/ModUtils.java | 92 +++ .../steamwar/velocitycore/mods/ReplayMod.java | 64 ++ .../velocitycore/mods/Schematica.java | 41 + .../velocitycore/mods/WorldDownloader.java | 48 ++ .../velocitycore/network/NetworkSender.java | 43 ++ .../velocitycore/network/ServerMetaInfo.java | 25 + .../network/handlers/EloPlayerHandler.java | 251 ++++++ .../network/handlers/EloSchemHandler.java | 75 ++ .../handlers/ExecuteCommandHandler.java | 36 + .../network/handlers/FightInfoHandler.java | 56 ++ .../network/handlers/ImALobbyHandler.java | 32 + .../handlers/InventoryCallbackHandler.java | 63 ++ .../network/handlers/PrepareSchemHandler.java | 41 + .../velocitycore/tablist/Tablist.java | 319 ++++++++ .../velocitycore/tablist/TablistBuild.java | 70 ++ .../velocitycore/tablist/TablistGroup.java | 45 ++ .../velocitycore/tablist/TablistManager.java | 112 +++ .../velocitycore/tablist/TablistPart.java | 65 ++ .../velocitycore/tablist/TablistServer.java | 103 +++ .../steamwar/velocitycore/util/BauLock.java | 66 ++ .../velocitycore/util/BauLockState.java | 29 + build.gradle.kts | 23 + settings.gradle.kts | 2 + 145 files changed, 15181 insertions(+) create mode 100644 VelocityCore/Persistent/build.gradle.kts create mode 100644 VelocityCore/Persistent/src/de/steamwar/persistent/Arenaserver.java create mode 100644 VelocityCore/Persistent/src/de/steamwar/persistent/Bauserver.java create mode 100644 VelocityCore/Persistent/src/de/steamwar/persistent/Builderserver.java create mode 100644 VelocityCore/Persistent/src/de/steamwar/persistent/Persistent.java create mode 100644 VelocityCore/Persistent/src/de/steamwar/persistent/Reflection.java create mode 100644 VelocityCore/Persistent/src/de/steamwar/persistent/ReloadablePlugin.java create mode 100644 VelocityCore/Persistent/src/de/steamwar/persistent/Servertype.java create mode 100644 VelocityCore/Persistent/src/de/steamwar/persistent/Storage.java create mode 100644 VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java create mode 100644 VelocityCore/build.gradle.kts create mode 100644 VelocityCore/src/GDPRQueryREADME.md create mode 100644 VelocityCore/src/de/steamwar/command/SWCommand.java create mode 100644 VelocityCore/src/de/steamwar/command/TypeMapper.java create mode 100644 VelocityCore/src/de/steamwar/command/TypeUtils.java create mode 100644 VelocityCore/src/de/steamwar/command/TypeValidator.java create mode 100644 VelocityCore/src/de/steamwar/messages/BungeeCore.properties create mode 100644 VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties create mode 100644 VelocityCore/src/de/steamwar/messages/Chatter.java create mode 100644 VelocityCore/src/de/steamwar/messages/ChatterGroup.java create mode 100644 VelocityCore/src/de/steamwar/messages/Message.java create mode 100644 VelocityCore/src/de/steamwar/messages/PlayerChatter.java create mode 100644 VelocityCore/src/de/steamwar/messages/SteamwarResourceBundle.java create mode 100644 VelocityCore/src/de/steamwar/sql/SQLConfigImpl.java create mode 100644 VelocityCore/src/de/steamwar/sql/SQLWrapperImpl.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/ArenaMode.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/Broadcaster.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/Config.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/ErrorLogger.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/EventStarter.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/GameModeConfig.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/Node.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/ServerVersion.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/SubserverSystem.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/ArenaCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/BugCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/BuilderCloudCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/ChallengeCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/DevCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/EventCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/EventRescheduleCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/EventreloadCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/FightCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/HelpCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/HistoricCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/IgnoreCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/JoinmeCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/KickCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/ListCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/ModCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/PingCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/PlaytimeCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/PollCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/PollresultCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/PunishmentCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/RankCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/RulesCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/ServerSwitchCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/SetLocaleCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/StatCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/TpCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/TutorialCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/TypeMappers.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/UnIgnoreCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/VerifyCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/commands/WhoisCommand.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/DiscordConfig.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/DiscordTicketType.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/channels/ChecklistChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/channels/EventChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/channels/InteractionReply.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/channels/StaticMessageChannel.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/listeners/ChannelListener.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordSchemUpload.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordTeamEvent.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordTicketHandler.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/util/AuthManager.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/util/DiscordAlert.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/discord/util/DiscordRanks.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/inventory/InvCallback.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/inventory/SWInventory.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/inventory/SWItem.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/inventory/SWListInv.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/inventory/SWStreamInv.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/BanListener.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/BasicListener.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/CheckListener.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/ConnectionListener.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/EventModeListener.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/IPSanitizer.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/PluginMessage.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/PollSystem.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/SessionManager.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/listeners/SettingsChangedListener.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/Alpine.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/Badlion.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/Controlify.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/FML.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/FML2.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/FabricModSender.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/Feather.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/Hostname.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/LabyMod.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/Lunar.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/ModUtils.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/ReplayMod.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/Schematica.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/mods/WorldDownloader.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/network/NetworkSender.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/network/ServerMetaInfo.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/network/handlers/EloPlayerHandler.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/network/handlers/EloSchemHandler.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/network/handlers/ExecuteCommandHandler.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/network/handlers/ImALobbyHandler.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/network/handlers/InventoryCallbackHandler.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/network/handlers/PrepareSchemHandler.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/tablist/Tablist.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/tablist/TablistBuild.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/tablist/TablistGroup.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/tablist/TablistPart.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/tablist/TablistServer.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/util/BauLock.java create mode 100644 VelocityCore/src/de/steamwar/velocitycore/util/BauLockState.java diff --git a/VelocityCore/Persistent/build.gradle.kts b/VelocityCore/Persistent/build.gradle.kts new file mode 100644 index 00000000..77f2d623 --- /dev/null +++ b/VelocityCore/Persistent/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("java") +} + +group = "de.steamwar" + +tasks.compileJava { + options.encoding = "UTF-8" +} + +sourceSets { + main { + java { + srcDirs("src/") + } + resources { + srcDirs("src/") + exclude("**/*.java", "**/*.kt") + } + } +} + +dependencies { + compileOnly("org.projectlombok:lombok:1.18.32") + annotationProcessor("org.projectlombok:lombok:1.18.32") + + compileOnly("de.steamwar:velocity:RELEASE") + annotationProcessor("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT") +} \ No newline at end of file diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Arenaserver.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Arenaserver.java new file mode 100644 index 00000000..c283090b --- /dev/null +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Arenaserver.java @@ -0,0 +1,37 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.persistent; + +import lombok.Getter; + +@Getter +public class Arenaserver extends Subserver { + + private final String mode; + private final String map; + private final boolean allowMerge; + + public Arenaserver(String serverName, String mode, String map, boolean allowMerge, int port, ProcessBuilder processBuilder, Runnable shutdownCallback) { + super(Servertype.ARENA, serverName, port, processBuilder, shutdownCallback, null); + this.mode = mode; + this.map = map; + this.allowMerge = allowMerge; + } +} diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Bauserver.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Bauserver.java new file mode 100644 index 00000000..fecd1ae1 --- /dev/null +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Bauserver.java @@ -0,0 +1,60 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.persistent; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +@Getter +public class Bauserver extends Subserver { + private static final Map servers = new HashMap<>(); + public static Bauserver get(UUID owner) { + synchronized (servers) { + return servers.get(owner); + } + } + + private final UUID owner; + + public Bauserver(String serverName, UUID owner, int port, ProcessBuilder processBuilder, Runnable shutdownCallback){ + this(serverName, owner, port, processBuilder, shutdownCallback, null); + } + + public Bauserver(String serverName, UUID owner, int port, ProcessBuilder processBuilder, Runnable shutdownCallback, Consumer failureCallback){ + super(Servertype.BAUSERVER, serverName, port, processBuilder, shutdownCallback, failureCallback); + this.owner = owner; + + synchronized (servers) { + servers.put(owner, this); + } + } + + @Override + protected void unregister() { + synchronized (servers) { + servers.remove(owner); + } + super.unregister(); + } +} diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Builderserver.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Builderserver.java new file mode 100644 index 00000000..ad90b8a2 --- /dev/null +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Builderserver.java @@ -0,0 +1,59 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.persistent; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +@Getter +public class Builderserver extends Subserver { + + private static final Map servers = new HashMap<>(); + public static Builderserver get(String map) { + synchronized (servers) { + return servers.get(map); + } + } + + private final String map; + public Builderserver(String serverName, String map, int port, ProcessBuilder processBuilder, Runnable shutdownCallback){ + this(serverName, map, port, processBuilder, shutdownCallback, null); + } + + public Builderserver(String serverName, String map, int port, ProcessBuilder processBuilder, Runnable shutdownCallback, Consumer failureCallback){ + super(Servertype.BUILDER, serverName, port, processBuilder, shutdownCallback, failureCallback); + this.map = map; + + synchronized (servers) { + servers.put(map, this); + } + } + + @Override + protected void unregister() { + synchronized (servers) { + servers.remove(map); + } + super.unregister(); + } +} diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Persistent.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Persistent.java new file mode 100644 index 00000000..bc5fd1ed --- /dev/null +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Persistent.java @@ -0,0 +1,203 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.persistent; + +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Module; +import com.google.inject.name.Names; +import com.mojang.brigadier.Command; +import com.velocitypowered.api.command.BrigadierCommand; +import com.velocitypowered.api.command.CommandManager; +import com.velocitypowered.api.command.CommandMeta; +import com.velocitypowered.api.event.EventManager; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.PluginContainer; +import com.velocitypowered.api.plugin.PluginDescription; +import com.velocitypowered.api.plugin.PluginManager; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.scheduler.ScheduledTask; +import com.velocitypowered.proxy.plugin.PluginClassLoader; +import com.velocitypowered.proxy.plugin.VelocityPluginManager; +import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer; +import com.velocitypowered.proxy.plugin.loader.java.JavaPluginLoader; +import lombok.Getter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.NoSuchElementException; +import java.util.ResourceBundle; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Plugin( + id = "persistentvelocitycore", + name = "PersistentVelocityCore" +) +public class Persistent { + + private static final Reflection.Method registerPlugin = new Reflection.Method<>(VelocityPluginManager.class, "registerPlugin", PluginContainer.class); + + @Getter + private static Persistent instance; + + @Getter + private final ProxyServer proxy; + @Getter + private final Logger logger; + private final Path directory; + + @Inject + public Persistent(ProxyServer proxy, Logger logger, @DataDirectory Path dataDirectory) { + instance = this; + this.proxy = proxy; + this.logger = logger; + this.directory = dataDirectory; + } + + @Subscribe + public void onEnable(ProxyInitializeEvent event) { + proxy.getCommandManager().register( + new BrigadierCommand( + BrigadierCommand.literalArgumentBuilder("softreload") + .requires(commandSource -> commandSource.hasPermission("bungeecore.softreload")) + .executes(commandContext -> softreload()) + .build() + ) + ); + } + + @Subscribe + public void onDisable(ProxyShutdownEvent event) { + Subserver.shutdown(); + } + + public int softreload() { + PluginContainer container = null; + ReloadablePlugin plugin = null; + try { + container = proxy.getPluginManager().getPlugin("velocitycore").orElseThrow(); + plugin = (ReloadablePlugin) container.getInstance().orElseThrow(); + } catch (NoSuchElementException e) { + logger.log(Level.WARNING, "Could not find loaded VelocityCore, continuing without unloading."); + } + + PluginContainer newContainer; + try { + newContainer = prepareLoad(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Could not instantiate new VelocityCore, aborting softreload.", e); + return Command.SINGLE_SUCCESS; + } + + broadcast("§eNetwork update is starting§8."); + try { + if(container != null && plugin != null) { + plugin.onProxyShutdown(new ProxyShutdownEvent()); + unload(container, plugin); + } + + registerPlugin.invoke((VelocityPluginManager) proxy.getPluginManager(), newContainer); + ((ReloadablePlugin) newContainer.getInstance().orElseThrow()).onProxyInitialization(new ProxyInitializeEvent()); + } catch (Throwable t) { + logger.log(Level.SEVERE, "Error during softreload", t); + broadcast("§cNetwork update failed§8, §cexpect network restart soon§8."); + return Command.SINGLE_SUCCESS; + } + + broadcast("§eNetwork update complete§8."); + return Command.SINGLE_SUCCESS; + } + + private void broadcast(String message) { + Component component = LegacyComponentSerializer.legacySection().deserialize("§eSteam§8War» " + message); + proxy.getAllPlayers().forEach(player -> player.sendMessage(component)); + proxy.getConsoleCommandSource().sendMessage(component); + } + + private PluginContainer prepareLoad() throws Exception { + Path plugins = directory.getParent(); + JavaPluginLoader loader = new JavaPluginLoader(proxy, plugins); + PluginDescription description = loader.createPluginFromCandidate(loader.loadCandidate(plugins.resolve("VelocityCore.jar"))); + PluginContainer container = new VelocityPluginContainer(description); + + AbstractModule commonModule = new AbstractModule() { + @Override + protected void configure() { + this.bind(ProxyServer.class).toInstance(proxy); + this.bind(PluginManager.class).toInstance(proxy.getPluginManager()); + this.bind(EventManager.class).toInstance(proxy.getEventManager()); + this.bind(CommandManager.class).toInstance(proxy.getCommandManager()); + this.bind(PluginContainer.class).annotatedWith(Names.named(container.getDescription().getId())).toInstance(container); + } + }; + + Module module = loader.createModule(container); + loader.createPlugin(container, module, commonModule); + + return container; + } + + private void unload(PluginContainer container, Object plugin) throws InterruptedException, IOException { + PluginClassLoader classLoader = ((PluginClassLoader) plugin.getClass().getClassLoader()); + + CommandManager commandManager = proxy.getCommandManager(); + for(String alias : commandManager.getAliases()) { + CommandMeta meta = commandManager.getCommandMeta(alias); + if(meta != null && meta.getPlugin() == plugin) + commandManager.unregister(meta); + } + + proxy.getEventManager().unregisterListeners(plugin); + proxy.getScheduler().tasksByPlugin(plugin).forEach(ScheduledTask::cancel); + + container.getExecutorService().shutdown(); + if(!container.getExecutorService().awaitTermination(100, TimeUnit.MILLISECONDS)) + logger.log(Level.WARNING, "ExecutorService termination took longer than 100ms, continuing."); + + for(Thread thread : Thread.getAllStackTraces().keySet()) { + if(thread.getClass().getClassLoader() != classLoader) + continue; + + thread.interrupt(); + thread.join(100); + + if (thread.isAlive()) + logger.log(Level.WARNING, "Could not stop thread %s of plugin %s. Still running".formatted(thread.getName(), container.getDescription().getId())); + } + + //TODO close all log handlers + /*for (Handler handler : plugin.getLogger().getHandlers()) { + handler.close(); + }*/ + + + //Clear resource bundle cache + ResourceBundle.clearCache(classLoader); + classLoader.close(); + } +} diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Reflection.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Reflection.java new file mode 100644 index 00000000..416a3694 --- /dev/null +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Reflection.java @@ -0,0 +1,75 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.persistent; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class Reflection { + public static class Field { + private final java.lang.reflect.Field f; + + public Field(Class target, String name) { + try { + f = target.getDeclaredField(name); + f.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new IllegalArgumentException("Cannot find field with name " + name, e); + } + } + + public T get(C target) { + try { + return (T) f.get(target); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot access reflection.", e); + } + } + + public void set(C target, T value) { + try { + f.set(target, value); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot access reflection.", e); + } + } + } + + public static class Method { + private final java.lang.reflect.Method m; + + public Method(Class clazz, String methodName, Class... params) { + try { + m = clazz.getDeclaredMethod(methodName, params); + m.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Cannot find method with name " + methodName, e); + } + } + + public Object invoke(C target, Object... arguments) { + try { + return m.invoke(target, arguments); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot invoke method " + m, e); + } + } + } +} diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/ReloadablePlugin.java b/VelocityCore/Persistent/src/de/steamwar/persistent/ReloadablePlugin.java new file mode 100644 index 00000000..95bfc4b9 --- /dev/null +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/ReloadablePlugin.java @@ -0,0 +1,28 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.persistent; + +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; + +public interface ReloadablePlugin { + void onProxyInitialization(ProxyInitializeEvent event); + default void onProxyShutdown(ProxyShutdownEvent event) {} +} diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Servertype.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Servertype.java new file mode 100644 index 00000000..4731a2eb --- /dev/null +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Servertype.java @@ -0,0 +1,26 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.persistent; + +public enum Servertype { + BAUSERVER, + ARENA, + BUILDER +} diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Storage.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Storage.java new file mode 100644 index 00000000..a32a577d --- /dev/null +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Storage.java @@ -0,0 +1,52 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.persistent; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; +import lombok.experimental.UtilityClass; + +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@UtilityClass +public class Storage { + public static final Map> challenges = new HashMap<>(); + + public static final Map lastChats = new HashMap<>(); + + public static final Map> teamInvitations = new HashMap<>(); // UserID -> List + + public static final Map sessions = new HashMap<>(); // Contains session start timestamp + + public static final Map eventServer = new HashMap<>(); // TeamID -> Subserver map + + public static final Map fabricCheckedPlayers = new HashMap<>(); + + public static final Map fabricExpectPluginMessage = new HashMap<>(); + + public static final Map teamServers = new HashMap<>(); // TeamID -> ServerInfo map + + public static final Map> directTabItems = new HashMap<>(); +} diff --git a/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java b/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java new file mode 100644 index 00000000..5d262ccf --- /dev/null +++ b/VelocityCore/Persistent/src/de/steamwar/persistent/Subserver.java @@ -0,0 +1,263 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.persistent; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; +import lombok.Getter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.io.*; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; + +@SuppressWarnings("unused") +public class Subserver { + + private static final Component PREFIX = Component + .text("Steam").color(NamedTextColor.YELLOW) + .append(Component.text("War» ").color(NamedTextColor.DARK_GRAY)); + + private static final Logger logger = Persistent.getInstance().getLogger(); + + @Getter + private static final List serverList = new LinkedList<>(); + private static final Map infoToServer = new HashMap<>(); + + public static Subserver getSubserver(Player p) { + synchronized (serverList) { + for (int i = serverList.size() - 1; i >= 0; i--) { + if (serverList.get(i).onServer(p)) + return serverList.get(i); + } + } + return null; + } + + public static Subserver getSubserver(ServerInfo server) { + synchronized (serverList) { + return infoToServer.get(server); + } + } + + static void shutdown() { + while (!serverList.isEmpty()) { + Subserver server = serverList.get(0); + server.stop(); + } + } + + private final String serverName; + private final boolean checkpoint; + private final Runnable shutdownCallback; + private final Consumer failureCallback; + private final Process process; + private final PrintWriter writer; + @Getter + private final ServerInfo server; + @Getter + private RegisteredServer registeredServer; + @Getter + private final Servertype type; + private final Thread thread; + @Getter + private boolean started; + + private final List cachedPlayers = new LinkedList<>(); + @Getter + private final Map tablistNames = new HashMap<>(); + + protected Subserver(Servertype type, String serverName, int port, ProcessBuilder processBuilder, Runnable shutdownCallback, Consumer failureCallback) { + this.started = false; + this.serverName = serverName; + this.type = type; + this.shutdownCallback = shutdownCallback; + this.failureCallback = failureCallback == null ? this::fatalError : failureCallback; + this.checkpoint = processBuilder.command().contains("criu"); + + try { + this.process = processBuilder.start(); + } catch (IOException e) { + throw new SecurityException("Server could not be started", e); + } + + InetSocketAddress address = new InetSocketAddress("127.0.0.1", port); + this.server = new ServerInfo(serverName, address); + this.writer = new PrintWriter(process.getOutputStream(), true); + + this.thread = new Thread(this::run, "Subserver " + serverName); + this.thread.start(); + } + + public void sendPlayer(Player p) { + if (!started) { + p.sendActionBar(generateBar(0)); + cachedPlayers.add(p); + } else { + p.createConnectionRequest(registeredServer).connect(); + } + } + + public void execute(String command) { + writer.println(command); + } + + public void stop() { + try { + long pid = process.pid(); + if (checkpoint) + pid = process.children().findAny().map(ProcessHandle::pid).orElse(pid); + + Runtime.getRuntime().exec(new String[]{"kill", "-SIGUSR1", Long.toString(pid)}); + } catch (IOException e) { + logger.log(Level.SEVERE, "Failed to send SIGUSR1 to subserver.", e); + } + + try { + if (!process.waitFor(1, TimeUnit.MINUTES)) { + logger.log(Level.SEVERE, () -> serverName + " did not stop correctly, forcibly stopping!"); + process.destroyForcibly(); + } + + thread.join(); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, "Subserver stop interrupted!", e); + Thread.currentThread().interrupt(); + } + } + + private boolean onServer(Player p) { + return cachedPlayers.contains(p) || (registeredServer != null && registeredServer.getPlayersConnected().contains(p)); + } + + private void fatalError(Exception e) { + logger.log(Level.SEVERE, e, () -> serverName + " did not run correctly!"); + + for (Player cached : cachedPlayers) + cached.sendMessage(PREFIX.append(Component.text("Unexpected error during server startup.").color(NamedTextColor.RED))); + if (registeredServer != null) { + for (Player player : registeredServer.getPlayersConnected()) + player.sendMessage(PREFIX.append(Component.text("Lost connection to server.").color(NamedTextColor.RED))); + } + } + + private void start(InputStream stream, Predicate test) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + String line = ""; + while (!started && (line = reader.readLine()) != null) { + started = test.test(line); + } + + if (line == null) + throw new IOException(serverName + " did not start correctly!"); + } + } + + protected void register() { + if (Persistent.getInstance().getProxy().getServer(serverName).isPresent()) { + SecurityException e = new SecurityException("Server already registered: " + serverName); + stop(); + failureCallback.accept(e); + throw e; + } + + synchronized (serverList) { + registeredServer = Persistent.getInstance().getProxy().registerServer(server); + serverList.add(this); + infoToServer.put(server, this); + } + } + + protected void unregister() { + synchronized (serverList) { + infoToServer.remove(server); + serverList.remove(this); + Persistent.getInstance().getProxy().unregisterServer(server); + registeredServer = null; + } + } + + private void run() { + register(); + + Exception ex = null; + try { + if (checkpoint) { + start(process.getErrorStream(), line -> line.contains("Restore finished successfully.")); + } else { + start(process.getInputStream(), line -> { + if (line.contains("Loading libraries, please wait")) + sendProgress(2); + else if (line.contains("Starting Minecraft server on")) + sendProgress(4); + else if (line.contains("Preparing start region")) + sendProgress(6); + return line.contains("Finished mapping loading"); + }); + } + + if (!started) + return; + + sendProgress(8); + + Thread.sleep(300); + + sendProgress(10); + for (Player cachedPlayer : cachedPlayers) { + sendPlayer(cachedPlayer); + } + cachedPlayers.clear(); + + process.waitFor(); + } catch (IOException e) { + ex = e; + } catch (InterruptedException e) { + ex = e; + Thread.currentThread().interrupt(); + } finally { + unregister(); + shutdownCallback.run(); + if (ex != null) + failureCallback.accept(ex); + } + } + + private Component generateBar(int progress) { + return Component.text("⬛".repeat(Math.max(0, progress))).color(NamedTextColor.YELLOW) + .append(Component.text("⬛".repeat(Math.max(0, 10 - progress))).color(NamedTextColor.DARK_GRAY)); + } + + private void sendProgress(int progress) { + Component tc = generateBar(progress); + for (Player cached : cachedPlayers) + cached.sendActionBar(tc); + } +} diff --git a/VelocityCore/build.gradle.kts b/VelocityCore/build.gradle.kts new file mode 100644 index 00000000..2e8d5a78 --- /dev/null +++ b/VelocityCore/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + id("java") + id("com.github.johnrengelman.shadow") +} + +group = "de.steamwar" + +tasks.shadowJar { + exclude("META-INF/*") + exclude("org/sqlite/native/FreeBSD/**', 'org/sqlite/native/Mac/**', 'org/sqlite/native/Windows/**', 'org/sqlite/native/Linux-Android/**', 'org/sqlite/native/Linux-Musl/**") + exclude("org/sqlite/native/Linux/aarch64/**', 'org/sqlite/native/Linux/arm/**', 'org/sqlite/native/Linux/armv6/**', 'org/sqlite/native/Linux/armv7/**', 'org/sqlite/native/Linux/ppc64/**', 'org/sqlite/native/Linux/x86/**") + exclude("org/slf4j/**") + //https://imperceptiblethoughts.com/shadow/configuration/minimizing/ + minimize { + exclude(project(":VelocityCore")) + exclude(dependency("mysql:mysql-connector-java:.*")) + } + duplicatesStrategy = DuplicatesStrategy.INCLUDE +} + +tasks.compileJava { + options.encoding = "UTF-8" +} + +sourceSets { + main { + java { + srcDirs("src/") + } + resources { + srcDirs("src/") + exclude("**/*.java", "**/*.kt") + } + } +} + +dependencies { + compileOnly("org.projectlombok:lombok:1.18.32") + annotationProcessor("org.projectlombok:lombok:1.18.32") + + annotationProcessor("com.velocitypowered:velocity-api:3.3.0-SNAPSHOT") + compileOnly("de.steamwar:velocity:RELEASE") + + implementation(project(":CommonCore")) + implementation(project(":CommandFramework")) + + compileOnly(project(":VelocityCore:Persistent")) + + implementation("org.xerial:sqlite-jdbc:3.46.0.0") + implementation("mysql:mysql-connector-java:8.0.33") + + implementation("net.dv8tion:JDA:4.4.0_352") { + exclude(module = "opus-java") + } + + implementation("org.msgpack:msgpack-core:0.9.8") + + implementation("com.lunarclient:apollo-api:1.1.0") + implementation("com.lunarclient:apollo-common:1.1.0") +} \ No newline at end of file diff --git a/VelocityCore/src/GDPRQueryREADME.md b/VelocityCore/src/GDPRQueryREADME.md new file mode 100644 index 00000000..bbe4fcbd --- /dev/null +++ b/VelocityCore/src/GDPRQueryREADME.md @@ -0,0 +1,29 @@ +# SteamWar GDPR report + +## Copyright notice +The provided build worlds contain the world design of SteamWar contributors and is subject to their copyright. +The build worlds are therefore provided for personal use only. + +## Categories of personal data processed +- IP address +- Minecraft account +- E-Mail address (if using a website account) + +## Processing purposes of personal data and person-related data +- Provision of SteamWar user functionality (Minecraft account, IP address, BuildWorlds, BuildInventories, BuildMembers, Elo, IgnoredPlayers, SchematicMembers, SchematicChecksessions, Schematics, PersonalKits, UserData, UserConfigs) +- Manual analysis and punishment of player misbehaviour (BannedIPs, log files, Punishments) +- Statistical analysis (Sessions, Fights, SchematicChecksessions) +- Historical data storage (Fights) +- Technical error analysis (log files) +- Provision of the SteamWar website functionality (IP address, E-Mail address) + +## Data Accessors +- SteamWar software +- SteamWar Administration +- SteamWar Development +- SteamWar Moderation (limited to Punishments and SchematicChecksessions) + +## Storage duration +- Data for provision of functionality is stored until user triggered deletion +- Log file storage duration is targeted at one month, but might differ due to technical reasons +- Historical and statistical data deletion is not intended diff --git a/VelocityCore/src/de/steamwar/command/SWCommand.java b/VelocityCore/src/de/steamwar/command/SWCommand.java new file mode 100644 index 00000000..52845d2e --- /dev/null +++ b/VelocityCore/src/de/steamwar/command/SWCommand.java @@ -0,0 +1,183 @@ +/* + * 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.command; + +import com.velocitypowered.api.command.SimpleCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.discord.DiscordBot; +import lombok.Getter; +import net.kyori.adventure.text.event.ClickEvent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import java.util.logging.Level; + +public class SWCommand extends AbstractSWCommand { + + static { + TypeUtils.init(); + } + + private final String name; + @Getter + private final UserPerm permission; + private final String[] aliases; + + private SimpleCommand command; + + private final List defaultHelpMessages = new ArrayList<>(); + + protected SWCommand(String command, String... aliases) { + this(command, null, aliases); + } + + protected SWCommand(String command, UserPerm permission, String... aliases) { + super(Chatter.class, command, aliases); + this.name = command; + this.permission = permission; + this.aliases = aliases; + create = true; + createAndSafeCommand(command, aliases); + unregister(); + register(); + } + + public void execute(Chatter sender, String[] args) { + execute(sender, null, args); + } + + private boolean create = false; + + @Override + protected void createAndSafeCommand(String command, String[] aliases) { + if (!create) return; + this.command = new SimpleCommand() { + + @Override + public void execute(Invocation invocation) { + SWCommand.this.execute(Chatter.of(invocation.source()), invocation.alias(), invocation.arguments()); + } + + @Override + public List suggest(Invocation invocation) { + String[] args = invocation.arguments(); + if(args.length == 0) + args = new String[]{""}; + + return SWCommand.this.tabComplete(Chatter.of(invocation.source()), invocation.alias(), args); + } + + @Override + public boolean hasPermission(Invocation invocation) { + return permission == null || Chatter.of(invocation.source()).user().perms().contains(permission); + } + }; + } + + @Override + public void unregister() { + if (command == null) + return; + + VelocityCore.getProxy().getCommandManager().unregister(name); + DiscordBot.getCommands().remove(name); + } + + @Override + public void register() { + if (command == null) + return; + + VelocityCore.getProxy().getCommandManager().register(VelocityCore.getProxy().getCommandManager().metaBuilder(name).aliases(aliases).plugin(VelocityCore.get()).build(), command); + DiscordBot.getCommands().put(name, this); + } + + @Override + protected void commandSystemError(Chatter sender, CommandFrameworkException e) { + VelocityCore.getLogger().log(Level.SEVERE, e.getMessage(), e); + sender.prefixless("COMMAND_SYSTEM_ERROR"); + } + + @Override + protected void commandSystemWarning(Supplier message) { + VelocityCore.getLogger().log(Level.WARNING, message); + } + + public void addDefaultHelpMessage(String message) { + defaultHelpMessages.add(message); + } + + @Override + protected void sendMessage(Chatter sender, String message, Object[] args) { + sender.system(message, args); + } + + @Register(noTabComplete = true) + public void internalHelp(Chatter sender, String... args) { + try { + sender.prefixless("COMMAND_HELP_HEAD", name); + defaultHelpMessages.forEach(sender::prefixless); + } catch (Exception e) { + VelocityCore.getLogger().log(Level.WARNING, "Failed to send help message", e); + return; + } + AtomicInteger atomicInteger = new AtomicInteger(); + if (args.length != 0) { + commandList.forEach(subCommand -> { + List tabCompletes = subCommand.tabComplete(sender, args); + if (tabCompletes == null || tabCompletes.isEmpty()) { + atomicInteger.incrementAndGet(); + return; + } + boolean hasTabCompletes = tabCompletes.stream() + .anyMatch(s -> s.toLowerCase().startsWith(args[args.length - 1].toLowerCase())); + if (hasTabCompletes) { + send(sender, subCommand); + } else { + atomicInteger.incrementAndGet(); + } + }); + } + if (args.length == 0 || atomicInteger.get() == commandList.size()) { + commandList.forEach(subCommand -> { + if (subCommand.validator == null || subCommand.validator.validate(sender, sender, (s, args1) -> {})) { + send(sender, subCommand); + } + }); + } + } + + private void send(Chatter chatter, SubCommand subCommand) { + try { + for (String s : subCommand.description) { + String hover = "§8/§e" + name + " " + String.join(" ", subCommand.subCommand); + chatter.prefixless(s, new Message("PLAIN_STRING", hover), ClickEvent.suggestCommand("/" + name + " " + String.join(" ", subCommand.subCommand))); + } + } catch (Exception e) { + VelocityCore.getLogger().log(Level.WARNING, "Failed to send description of registered method '%s' with description '%s'".formatted(subCommand.method, Arrays.toString(subCommand.description)), e); + } + } +} diff --git a/VelocityCore/src/de/steamwar/command/TypeMapper.java b/VelocityCore/src/de/steamwar/command/TypeMapper.java new file mode 100644 index 00000000..5b592260 --- /dev/null +++ b/VelocityCore/src/de/steamwar/command/TypeMapper.java @@ -0,0 +1,29 @@ +/* + * 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.command; + +import de.steamwar.messages.Chatter; + +public interface TypeMapper extends AbstractTypeMapper { + /** + * The CommandSender can be null! + */ + T map(Chatter sender, PreviousArguments previousArguments, String s); +} diff --git a/VelocityCore/src/de/steamwar/command/TypeUtils.java b/VelocityCore/src/de/steamwar/command/TypeUtils.java new file mode 100644 index 00000000..a273cda2 --- /dev/null +++ b/VelocityCore/src/de/steamwar/command/TypeUtils.java @@ -0,0 +1,75 @@ +/* + * 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.command; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.ServerVersion; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.commands.TypeMappers; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.Team; +import lombok.experimental.UtilityClass; + +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@UtilityClass +public class TypeUtils { + + static void init() { + SWCommandUtils.addMapper(Player.class, SWCommandUtils.createMapper(s -> VelocityCore.getProxy().getPlayer(s).orElse(null), s -> VelocityCore.getProxy().getAllPlayers().stream().map(Player::getUsername).collect(Collectors.toList()))); + SWCommandUtils.addMapper(SteamwarUser.class, SWCommandUtils.createMapper(SteamwarUser::get, s -> VelocityCore.getProxy().getAllPlayers().stream().map(Player::getUsername).collect(Collectors.toList()))); + SWCommandUtils.addMapper(ServerVersion.class, new TypeMapper<>() { + @Override + public ServerVersion map(Chatter sender, PreviousArguments previousArguments, String s) { + Player player = sender.getPlayer(); + if (player != null && s.isEmpty()) { + ProtocolVersion version = player.getProtocolVersion(); + if (version.greaterThan(ProtocolVersion.MINECRAFT_1_19_4)) { + return ServerVersion.PAPER_20; + } else if (version.greaterThan(ProtocolVersion.MINECRAFT_1_15_2)) { + return ServerVersion.PAPER_19; + } else if (version.greaterThan(ProtocolVersion.MINECRAFT_1_12_2)) { + return ServerVersion.SPIGOT_15; + } else { + return ServerVersion.SPIGOT_12; + } + } + + return ServerVersion.get(s); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + return ServerVersion.chatVersions(); + } + }); + + AbstractTypeMapper teamMapper = SWCommandUtils.createMapper(Team::get, s -> Team.getAll().stream().flatMap(team -> Stream.of(team.getTeamKuerzel(), team.getTeamName())).collect(Collectors.toList())); + TabCompletionCache.add(teamMapper, true, 10, TimeUnit.SECONDS); + SWCommandUtils.addMapper(Team.class, teamMapper); + + TypeMappers.init(); + } +} diff --git a/VelocityCore/src/de/steamwar/command/TypeValidator.java b/VelocityCore/src/de/steamwar/command/TypeValidator.java new file mode 100644 index 00000000..2986b7da --- /dev/null +++ b/VelocityCore/src/de/steamwar/command/TypeValidator.java @@ -0,0 +1,24 @@ +/* + * 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.command; + +import de.steamwar.messages.Chatter; + +public interface TypeValidator extends AbstractValidator {} diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties new file mode 100644 index 00000000..0e45249f --- /dev/null +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore.properties @@ -0,0 +1,730 @@ +COMMAND_SYSTEM_ERROR = §cError executing the command! +COMMAND_HELP_HEAD=§7---=== (§e{0}§7) ===--- + +PREFIX=§eSteam§8War» +SPACER= +TIMEFORMAT=dd.MM.yyyy HH:mm +PLAIN_STRING={0} + +UNKNOWN_COMMAND=§cUnknown command. +UNKNOWN_PLAYER=§cUnknown player. +UNKNOWN_TEAM=§cUnknown team. +UNKNOWN_EVENT=$cUnknown event. +INVALID_TIME=§cInvalid time. + +STEAMWAR_BRAND=§eSteam§8War.de §7({0}) §r<- §e{1} §7({2})§r + +DEV_NO_SERVER=§cThe server is currently not available. +DEV_UNKNOWN_SERVER=§cPlease specify a dev server. + +DISABLED=§cCurrently disabled. + +SERVER_START_OVERLOAD=§cServer start cancelled due to overload. Please try again later. +UPDATE_INTERRUPTION=§cPlease try again. A software update has interrupted this action. + +#Help command +HELP_LOBBY=§7Return from anywhere to the lobby using §8/§el§7! +HELP_LOBBY_HOVER=§eBack to the lobby +HELP_BAU=§7Join your build server using §8/§ebuild§7! +HELP_BAU_HOVER=§eTo your build server +HELP_BAUSERVER=§7Get help regarding the build server with §8/§ehelp build§7! +HELP_BAUSERVER_HOVER=§eHelp for the build server +HELP_FIGHT=§7Start a new fight using §8/§efight§7! +HELP_FIGHT_HOVER=§eTo the fighting system +HELP_CHALLENGE=§7Challenge someone directly using §8/§echallenge§7! +HELP_CHALLENGE_HOVER=§eChallenge +HELP_HISTORIC=§7Start a historic fight using §8/§ehistoric§7! +HELP_HISTORIC_HOVER=§eHistoric fights +HELP_TEAM=§8/§eteam§7 for the team system! +HELP_TEAM_HOVER=§eTeam management +HELP_JOIN=§7Join a fight using §8/§ejoin §8[§eplayer§8]§7! +HELP_JOIN_HOVER=§eSJoin a player +HELP_LOCAL=§7Send chat messages only on your current server using §8/§elocal§7! +HELP_LOCAL_HOVER=§eLocal chat + +HELP_TNT=§8/§7tnt §8- §7(de)activates explosion damage +HELP_FIRE=§8/§7fire §8- §7(de)activates fire damage +HELP_TESTBLOCK=§8/§7testblock §8- §7Resets the dummy +HELP_RESET=§8/§7reset §8- §7Resets the current region +HELP_SPEED=§8/§7speed §8- §7Changes flight and walking speed +HELP_NV=§8/§7nv §8- §7(de)activates night vision +HELP_TRACE=§8/§7trace §8- §7Gives help regarding the tnt tracer +HELP_TPSLIMIT=§8/§7tpslimit §8- §7Gives help regarding the TPS limiter +HELP_LOADER=§8/§7loader §8- §7Use the automatic cannon loader +HELP_PROTECT=§8/§7protect §8- §7Protects the floor of the (M)WG region +HELP_FREEZE=§8/§7freeze §8- §7Stops block updates +HELP_SKULL=§8/§7skull §8- §7Gives you a player head +HELP_DEBUGSTICK=§8/§7debugstick §8- §7Gives you a debugstick +HELP_BAUINFO=§8/§7buildinfo §8- §7Information about the current build server +HELP_SCHEMSUBMIT=§7For a Tutorial about submitting your AirShip§8/§7MiniWarGear§8/§7WarGear§8/§7WarShip click §ehere§8! +HELP_SCHEMSUBMIT_HOVER=§ehttps://www.youtube.com/watch?v=9QrQ3UBWveE + +HELP_WE_POS1=§8//§71 §7» §8//§7pos1 +HELP_WE_POS2=§8//§72 §7» §8//§7pos2 +HELP_WE_COPY=§8//§7c §7» §8//§7copy +HELP_WE_PASTE=§8//§7p §7» §8//§7paste +HELP_WE_FLOPY=§8//§7flopy §7» §8//§7copy §7& §8//§7flip +HELP_WE_FLOPYP=§8//§7flopyp §7» §8//§7copy §7& §8//§7flip §7& §8//§7paste +HELP_WE_ROTATE_90=§8//§790 §7» §8//§7rotate §e90 +HELP_WE_ROTATE_180=§8//§7180 §7» §8//§7rotate §e180 +HELP_WE_ROTATE_N90=§8//§7-90 §7» §8//§7rotate §e-90 + +HELP_BAU_GROUP_ADMIN=§7Build server management commands +HELP_BAU_GROUP_ADMIN_HOVER=§eAll management commands +HELP_BAU_GROUP_ADMIN_TITLE=§7All management commands§8: +HELP_BAU_GROUP_OTHER=§7Additional management commands +HELP_BAU_GROUP_OTHER_HOVER=§eAdditional build server commands +HELP_BAU_GROUP_OTHER_TITLE=§7Additional build server commands§8: +HELP_BAU_GROUP_WE=§7WorldEdit shortcuts +HELP_BAU_GROUP_WE_HOVER=§eWorldEdit shortcuts +HELP_BAU_GROUP_WE_TITLE=§7WorldEdit shortcuts§8: +HELP_BAU_GROUP_PLAYER=§7Player commands +HELP_BAU_GROUP_PLAYER_HOVER=§ePlayer commands +HELP_BAU_GROUP_PLAYER_TITLE=§7Player commands§8: +HELP_BAU_GROUP_WORLD=§7World changing build server commands +HELP_BAU_GROUP_WORLD_HOVER=§eWorld changing build server commands +HELP_BAU_GROUP_WORLD_TITLE=§7World changing build server commands§8: + +HELP_BAU_TP=§8/§ebuild tp §8- §7Join the build server of friends! +HELP_BAU_TP_HOVER=§eto another build server +HELP_BAU_ADDMEMBER=§8/§ebuild addmember §8- §7Allows a friend on your build server +HELP_BAU_ADDMEMBER_HOVER=§eAdd a friend +HELP_BAU_DELMEMBER=§8/§ebuild delmember §8- §7Removes a player +HELP_BAU_DELMEMBER_HOVER=§eRemoves a player +HELP_BAU_SET_SPECTATOR=§8/§ebuild setspectator §8- §7Spectate build server +HELP_BAU_SET_SPECTATOR_HOVER=§eSets the Role to Spectator +HELP_BAU_SET_BUILDER=§8/§ebuild setbuilder §8- §7Building, WorldEdit, BauSystem features +HELP_BAU_SET_BUILDER_HOVER=§eSets the Role to Builder +HELP_BAU_SET_SUPERVISOR=§8/§ebuild supervisor §8- §7Starting build server. Saving schematics +HELP_BAU_SET_SUPERVISOR_HOVER=§eSets the Role to Supervisor +HELP_BAU_DELETE=§8/§ebuild delete §8- §7Reset your entire build server +HELP_BAU_DELETE_HOVER=§eReset build server +HELP_BAU_TESTARENA=§8/§ebuild testarena §8- §7Start a test arena +HELP_BAU_TESTARENA_HOVER=§eStart test arena +HELP_BAU_LOCK=§8/§ebuild lock §8- §7Locks the build server for a specified group of players +HELP_BAU_LOCK_HOVER=§eLock your build server +HELP_BAU_UNLOCK=§8/§ebuild unlock §8- §7Unlocks the buildserver for added users +HELP_BAU_UNLOCK_HOVER=§eUnlock your build server + +#Usage description of various commands +USAGE_ALERT=§8/§7alert §8[§emessage§8] +USAGE_IGNORE=§8/§7ignore §8[§eplayer§8] + +#ModListener +MOD_RED_SING=Attempted use of mod {0} +MOD_RED_PLUR=Attempted use of mods:\n{0} +MOD_YELLOW_SING=§7Deactivate the mod §e{0}§7 to continue playing on §eSteam§8War§7. +MOD_YELLOW_PLUR=§7Deactivate the mods\n§e{0}\n§7to continue playing on §eSteam§8War§7. +MODS_CHECKED=§7Your Mods have been checked.\n§aYou can join §eSteam§8War §anow§8. + +#Various commands +ALERT=§f{0} +STAT_SERVER=§7Server §e{0}§8: §7Below limit §e{1} §7Server count §e{2} + +#Ban&Mute-Command +PUNISHMENT_USAGE=§8/§7{0} §8[§eplayer§8] [§edd§8.§emm§8.§eyyyy §7or §edd§8.§emm§8.§eyyyy§8_§ehh§8:§emm §7or §enumber§8[§eh§7our|§ed§7ay|§ew§7eek|§em§7onth|§ey§7ear§8] §7or §eperma§8] [§ereason§8] +PUNISHMENT_USAGE_REASON=§cPlease enter a reason. +UNPUNISHMENT_USAGE=§8/§7{0} §8[§eplayer§8] + +PUNISHMENT_UNTIL=until {0} +PUNISHMENT_PERMA=permanent + +BAN_TEAM={0} §e{1} §7was §e§lbanned§7 by §e{2} {3}§8: §f{4} +BAN_PERMA=§7You are §e§lbanned §epermanently§8: §e{0} +BAN_UNTIL=§7You are §e§lbanned §euntil {0}§8: §e{1} +UNBAN_ERROR=§cThe player isn't banned. +UNBAN=§7You have §e§lunbanned §e{0}. + +BAN_AVOIDING_ALERT=§cPotential ban bypass by §r{0}§c: {1} +BAN_AVOIDING_LIST=§c{0} §e{1} +BAN_AVOIDING_BAN_HOVER=§cBan player because of bann bypass. + +MUTE_TEAM={0} §e{1} §7was §e§lmuted§7 by §e{2} {3}§8: §f{4} +MUTE_PERMA=§7You are §epermanently §e§lmuted§8: §e{0} +MUTE_UNTIL=§7You are §e§lmuted §euntil {0}§8: §e{1} +UNMUTE_ERROR=§cThe player isn't muted. +UNMUTE=§7You have §e§lunmuted §e{0}. + +NOSCHEMRECEIVING_TEAM={0} §e{1} §7was excluded from §e{2} {3} §7from §e§lrecieving schematics§8: §f{4} +NOSCHEMRECEIVING_PERMA=§7You are §epermanently§7 excluded from receiving §e§lschematics§8: §e{0} +NOSCHEMRECEIVING_UNTIL=§7You are excluded from receiving §e§lschematics §euntil {0}§8: §e{1} +UNNOSCHEMRECEIVING_ERROR=§cThe player is not excluded from receiving schematics. +UNNOSCHEMRECEIVING=§e{0} §7may now receive §e§lschematics§7 again§8. + +NOSCHEMSHARING_TEAM={0} §e{1} §7was excluded from §e{2} {3} §7from §e§lsharing schematics§8: §f{4} +NOSCHEMSHARING_PERMA=§7You are §epermanently§7 excluded from sharing §e§lschematics§8: §e{0} +NOSCHEMSHARING_UNTIL=§7You are excluded from sharing §e§lschematics §euntil {0}§8: §e{1} +UNNOSCHEMSHARING_ERROR=§cThe player is not excluded from sharing schematics. +UNNOSCHEMSHARING=§e{0} §7may now share §e§lschematics§7 again§8. + +NOSCHEMSUBMITTING_TEAM={0} §e{1} §7was excluded from §e{2} {3} §7from §e§lsubmitting schematics§8: §f{4} +NOSCHEMSUBMITTING_PERMA=§7You are §epermanently§7 excluded from submitting §e§lschematics§8: §e{0} +NOSCHEMSUBMITTING_UNTIL=§7You are excluded from submitting §e§lschematics §euntil {0}§8: §e{1} +UNNOSCHEMSUBMITTING_ERROR=§cThe player is not excluded from submitting schematics. +UNNOSCHEMSUBMITTING=§e{0} §7may now submit §e§lschematics§7 again§8. + +NODEVSERVER_TEAM={0} §e{1} §7has annoyed §e{2} §7with reason §f{4}§7 and therefore has received §e§ldev server prohibition§7§8, §f{3} +NODEVSERVER_PERMA=§7You are §epermanently§7 excluded from §e§ldev servers§8: §e{0} +NODEVSERVER_UNTIL=§7You are excluded from §e§ldev servers§7 §euntil {0}§8: §e{1} +UNNODEVSERVER_ERROR=§cThe player is not excluded from dev servers. +UNNODEVSERVER=§e{0} §7may now join §e§ldev servers§7 again§8. + +NOFIGHTSERVER_TEAM={0} §e{1} §7was excluded from §e{2} {3} §7from §e§lfighting§8: §f{4} +NOFIGHTSERVER_PERMA=§7You are §epermanently§7 excluded from §e§lfighting§8: §e{0} +NOFIGHTSERVER_UNTIL=§7You are excluded from §e§lfighting§7 §euntil {0}§8: §e{1} +UNNOFIGHTSERVER_ERROR=§cThe player is not excluded from fighting. +UNNOFIGHTSERVER=§e{0} §7may now join §e§lfights§7 again§8. + +NOTEAMSERVER_TEAM={0} §e{1} §7was excluded from §e{2} {3} §7from §e§lteam servers§8: §f{4} +NOTEAMSERVER_PERMA=§7You are §epermanently§7 excluded from §e§lteam servers§8: §e{0} +NOTEAMSERVER_UNTIL=§7You are excluded from §e§lteam servers§7 §euntil {0}§8: §e{1} +UNNOTEAMSERVER_ERROR=§cThe player is not excluded from team servers. +UNNOTEAMSERVER=§e{0} §7may now set §e§lteam servers§7 again§8. + +NOTE_TEAM={0} §e{1} §7received a §e§lnote§7 from §e{2} {3}: §f{4} + +#BugCommand +BUG_MESSAGE=§7Please describe the issue in a Discord ticket with the bug ID §e{0} §7further§8. + +#IgnoreCommand +IGNORE_YOURSELF=§cHow are you going to ignore yourself? +IGNORE_ALREADY=§cYou are already ignoring this player. +IGNORE_MESSAGE=§7You are now ignoring §e{0}§8. + +#PollresultCommand +POLLRESULT_NOPOLL=§cThere is currently no ongoing poll. +POLLRESULT_HEADER=§e{0} players have voted on the question: §7{1} +POLLRESULT_LIST=§e{0}§8: §7{1} + +#BauCommand +BAU_ADDMEMBER_USAGE=§8/§7build addmember §8[§eplayer§8] +BAU_ADDMEMBER_SELFADD=§cYou don't have to add yourself! +BAU_ADDMEMBER_ISADDED=§cThis player is already a member of your world. +BAU_ADDMEMBER_ADDED=§aThe player was added to your world. +BAU_ADDMEMBER_ADDED_TARGET=§aYou have been added to the world of §e{0}§a. +BAU_TP_USAGE=§8/§7build tp §8[§eplayer§8] +BAU_TP_NOALLOWED=§cYou are not allowed to teleport to this player's world. +BAU_LOCKED_NOALLOWED=§cThe build server is currently locked. +BAU_LOCK_BLOCKED=§cYour build lock has prevented §e{0} §cfrom joining. +BAU_LOCKED_OPTIONS=§7Build server lock options§8: §cnobody§8, §eserverteam§8, §eteam_and_serverteam§8, §eteam§8, §aopen +BAU_LOCKED_NOBODY=§7You have locked your build server for all players. +BAU_LOCKED_SERVERTEAM=§7You have locked your build server for all players except added server team members. +BAU_LOCKED_TEAM_AND_SERVERTEAM=§7You have locked your build server for all players except added team members and server team members. +BAU_LOCKED_TEAM=§7You have locked your build server for all players except added team members. +BAU_LOCKED_OPEN=§7You have opened your build server for all added players. +BAU_DELMEMBER_USAGE=§8/§7build delmember §8[§eplayer§8] +BAU_DELMEMBER_SELFDEL=§cYou cannot remove yourself! +BAU_DELMEMBER_DELETED=§cPlayer was removed. +BAU_DELMEMBER_DELETED_TARGET=§cYou were removed from the world of §e{0}. +BAU_DELETE_DELETED=§aYour world is being reset. +BAU_DELETE_GUI_NAME=§eDo you really want to delete the world? +BAU_DELETE_GUI_CANCEL=§cCancel +BAU_DELETE_GUI_DELETE=§aDelete +BAU_START_ALREADY=§cThis server is already starting. +BAU_MEMBER_NOMEMBER=§cThis player is no member of your world! +BAU_MEMBER_SET_USAGE=§8/§7build {0} §8[§eplayer§8] +BAU_MEMBER_SET_TARGET=§7You are now a §e{1}§7 on the world of §e{0}§7. +BAU_MEMBER_SET=§7The player is now a §e{0}§7. +BAU_MEMBER_SET_SPECTATOR = spectator +BAU_MEMBER_SET_BUILDER = builder +BAU_MEMBER_SET_SUPERVISOR = supervisor +BAU_START_NOT_ALLOWED = §cYou are not allowed to start this build server. + +#ChallengeCommand +CHALLENGE_USAGE=§8/§7challenge §8[§eplayer§8] + CHALLENGE_OFFLINE=§cThe challenged player isn\'t online. +CHALLENGE_SELF=§cSchizophrenia? +CHALLENGE_IGNORED=§cThe challenged player has blocked you. +CHALLENGE_INARENA=§cThe challenged player is already in an arena. +CHALLENGE_BROADCAST=§e{0} duel§7: §e{1} §7vs §e{2} +CHALLENGE_BROADCAST_HOVER=§aWatch +CHALLENGE_CHALLENGED=§7You have challenged §e{0} §7to a §e{1} fight§7! +CHALLENGE_CHALLENGED_TARGET=§e{0} §7 has challenged you to a §e{1} fight §7{2}! +CHALLENGE_CHALLENGED_MAP=on §e{0} §7 +CHALLENGE_ACCEPT=§7Click §ehere§7 to accept +CHALLENGE_ACCEPT_HOVER=§aAccept challenge + +#EventCommand +EVENT_TIME_FORMAT=HH:mm +EVENT_DATE_FORMAT=dd.MM. +EVENT_USAGE=§8/§7event §8[§eTeam§8] - §7To teleport to a fight +EVENT_NO_TEAM=§cThis team does not exist +EVENT_NO_FIGHT_TEAM=§cThis team has no current fight +EVENT_NO_CURRENT=§cThere is no event taking place currently +EVENT_COMING=§eUpcoming events§8: +EVENT_COMING_EVENT=§7{0}§8-§7{1}§8: §e{2} +EVENT_COMING_DEADLINE=§7 Registration deadline§8: §7{0} +EVENT_COMING_SCHEM_DEADLINE=§7 Submission deadline§8: §7{0} +EVENT_COMING_TEAMS=§7 With§8: {0} +EVENT_COMING_TEAM= §{0}{1} +EVENT_CURRENT_EVENT=§e§l{0} +EVENT_CURRENT_FIGHT=§7{0} §{1}{2}§8 vs §{3}{4} +EVENT_CURRENT_FIGHT_WIN=§8: §7Victory §{0}{1} +EVENT_CURRENT_FIGHT_DRAW=§8: §7Draw + +#EventRescheduleCommand +EVENTRESCHEDULE_USAGE=§8/§7eventreschedule §8[§eTeam1§8] [§eTeam2§8] +EVENTRESCHEDULE_UNKNOWN_TEAM=§cA team is unknown / No current event +EVENTRESCHEDULE_NO_FIGHT="§cNo fight found between the teams" +EVENTRESCHEDULE_STARTING=§aFight starts in 30s + +#FightCommand +FIGHT_UNKNOWN_GAMEMODE=§cUnknown gamemode: {0} +FIGHT_UNKNOWN_ARENA=§cThe desired arena does not exist. +FIGHT_IN_ARENA=§cYou are already in an arena. +FIGHT_BROADCAST=§7Click §ehere§7 to fight §e{0} §7against §e{1}! +FIGHT_BROADCAST_HOVER=§aFight §eagainst §7{1} + +#CheckCommand +CHECK_REMINDER=§7There are §e{0} §7schematics left for review§8! +CHECK_REMINDER_HOVER=§eSchematics to review +CHECK_NOT_CHECKING=§cYou are currently not reviewing any schematic. +CHECK_HELP_LIST=§8/§7check list §8- §7Shows the list of unreviewed schematics +CHECK_HELP_NEXT=§8/§7check next §8- §7Next review question§8/§7accept +CHECK_HELP_DECLINE=§8/§7check decline §8[§ereason§8] - §7Decline schematic +CHECK_HELP_CANCEL=§8/§7check cancel §8- §7Cancels reviewing process +CHECK_LIST_HEADER=§e§l{0} schematics are left to review +CHECK_LIST_TO_CHECK={0} §8{1} §7{2} §e{3} +CHECK_LIST_WAIT=§{0}{1}:{2} +CHECK_LIST_TO_CHECK_HOVER=§eCheck schematic +CHECK_LIST_CHECKING={0} §8{1} §7{2} §e{3} §7is being reviewed by §e{4} +CHECK_LIST_CHECKING_HOVER=§eTo the reviewer +CHECK_SCHEMATIC_ALREADY_CHECKING=§cYou are already reviewing a schematic! +CHECK_SCHEMATIC_OWN=§cYou cannot review your own schematics. +CHECK_SCHEMATIC_OWN_TEAM=§cYou cannot review your team schematics. +CHECK_SCHEMATIC_PREVIOUS=§7{0} from {1}§8: §e{2} +CHECK_INVALID_RANK=§cUnknown schematic rank. +CHECK_ABORT=§aThe test operation was canceled! +CHECK_NEXT=Next question +CHECK_ACCEPT=Accept +CHECK_DECLINE=Decline +CHECK_RANK=§aRank {0}: {1} +CHECK_RANK_HOVER=§aAccept with given rank +CHECK_ACCEPTED=§aYour §e{0} {1} §ewas accepted§8! +CHECK_ACCEPTED_TEAM=§7The schematic §e{0} §7from §e{1} §7is now approved! +CHECK_DECLINED=§cYour §e{0} {1} §cwas declined§8: §c{2} +CHECK_DECLINED_TEAM=§7The schematic §e{0} §7from §e{1} §7is now declined because §e{2}§7! + +#HistoricCommand +HISTORIC_BROADCAST=§7Historic §e{0} §7fight by §e{1}§8! +HISTORIC_BROADCAST_HOVER=§afight against §7{1} + +#JoinCommand +JOIN_PLAYER_BLOCK=§cYou currently cannot follow this player. + +#JoinmeCommand +JOINME_USAGE=§8/§7join §8[§eplayer§8]. +JOINME_BROADCAST=§7Click §ehere§8 §7to join §e{0} §7on §e{1}§8! +JOINME_BROADCAST_HOVER=§aJoin player +JOINME_PLAYER_OFFLINE=§cThis player is offline. +JOINME_PLAYER_SELF=§cBe one with yourself! + +#KickCommand +KICK_USAGE=§8/§7kick §8[§ePlayer§8] [§eMessage§8] +KICK_OFFLINE=§cThis player is currently not online! +KICK_CONFIRM=The player {0} was kicked. +KICK_NORMAL=§cYou were kicked. +KICK_CUSTOM=§c{0} + +#MsgCommand +MSG_USAGE=§8/§7msg §8[§euser§8] [§emessage§8] +MSG_OFFLINE=§cPlayer is offline! +MSG_IGNORED=§cThis player has blocked you! + +#PingCommand +PING_RESPONSE=§7Your ping is §c{0}§7 ms! + +#PollCommand +POLL_NO_POLL=§cThere is no ongoing poll. +POLL_NO_ANSWER=§cThis is not an option +POLL_ANSWER_REFRESH=§aYour answer was updated. +POLL_ANSWER_NEW=§aYour answer was registered. + +#RCommand +R_USAGE=§8/§7r §8[§eanswer§8] + +#RegelnCommand +REGELN_RULES=§7§lRules +REGELN_AS=§eAirShip §7Rules +REGELN_AS_HOVER=§7https://steamwar.de/spielmodi/airship-regelwerk/ +REGELN_AS_URL=https://steamwar.de/spielmodi/airship-regelwerk/ +REGELN_MWG=§eMiniWarGear §7Rules +REGELN_MWG_HOVER=§7https://steamwar.de/spielmodi/miniwargear-regelwerk/ +REGELN_MWG_URL=https://steamwar.de/spielmodi/miniwargear-regelwerk/ +REGELN_WG=§eWarGear §7Rules +REGELN_WG_HOVER=§7https://steamwar.de/spielmodi/wargear-regelwerk/ +REGELN_WG_URL=https://steamwar.de/spielmodi/wargear-regelwerk/ +REGELN_WS=§eWarShip §7Rules +REGELN_WS_HOVER=§7https://steamwar.de/spielmodi/warship-regelwerk/ +REGELN_WS_URL=https://steamwar.de/spielmodi/warship-regelwerk/ +REGELN_QG=§eQuickGear §7Rules +REGELN_QG_HOVER=§7https://steamwar.de/spielmodi/quickgear-regelwerk/ +REGELN_QG_URL=https://steamwar.de/spielmodi/quickgear-regelwerk/ +REGELN_CONDUCT=§eCode of conduct +REGELN_CONDUCT_HOVER=§7https://steamwar.de/verhaltensrichtlinien/ +REGELN_CONDUCT_URL=https://steamwar.de/verhaltensrichtlinien/ + +#ReplayCommand +REPLAY_TITLE=Most recent fights +REPLAY_UNAVAILABLE=§cReplay unavailable +REPLAY_SOLO_WINNER=§e§l{0} +REPLAY_WINNER=§e§l{0} §7+§e{1} +REPLAY_SOLO_LOSER=§e{0} +REPLAY_LOSER=§e{0} §7+§e{1} +REPLAY_TIME=§7{0} +REPLAY_SERVER=§7{0} + +#TutorialCommand +TUTORIAL_TITLE=Tutorials +TUTORIAL_NAME=§e{0} +TUTORIAL_BY=§8von §7{0} +TUTORIAL_STARS=§e{0} §7Stars +TUTORIAL_RATE_TITLE=Rate tutorial +TUTORIAL_RATE=§e{0} §7Star(s) +TUTORIAL_DELETE=§cDelete with Shift+Right-Click +TUTORIAL_CREATE_HELP=§8/§7tutorial create §8[§ematerial§8] §8[§ename§8] +TUTORIAL_CREATE_MISSING=§cA tutorial can only be created on a tutorial server! +TUTORIAL_CREATED=§7Tutorial created§8. +TUTORIAL_OWN_HELP=§8/§7tutorial own §8- §7List your own tutorials + +#ServerTeamchatCommand +STC_USAGE=§8/§7stc §8[§emessage to team§8] + +#TeamchatCommand +TC_USAGE=§8/§7tc §8[§eMessage to the team§8] +TC_NO_TEAM=§cYou are currently in no team. + +#TeamCommand +TEAM_IN_TEAM=§cYou are already in a team. +TEAM_NOT_IN_TEAM=§cYou are currently in no team. +TEAM_NOT_LEADER=§cYou are not a leader of your team. +TEAM_NOT_IN_EVENT=§cThis is not possible during an event. +TEAM_HELP_HEADER=§7Manage your team with §e/team. +TEAM_HELP_LIST=§8/§7team list §8- §7List all teams. +TEAM_HELP_INFO=§8/§7team info §8- §7Get information on a team. +TEAM_HELP_TP=§8/§7team tp §8(§7Team§8) §8- §7Teleport to a team server. +TEAM_HELP_CREATE=§8/§7team create §8- §7Create your own team. +TEAM_HELP_JOIN=§8/§7team join §8- §7Join a team. +TEAM_HELP_CHAT=§8/§7teamchat §8- §7Send messages to your team. +TEAM_HELP_EVENT=§8/§7team event §8- §7Take part in an event. +TEAM_HELP_LEAVE=§8/§7team leave §8- §7Leave your team. +TEAM_HELP_INVITE=§8/§7team invite §8- §7Invite someone to join your team. +TEAM_HELP_REMOVE=§8/§7team remove §8- §7Remove somebody out of your team. +TEAM_HELP_KUERZEL=§8/§7team changekuerzel §8- §7Change your team shortcut. +TEAM_HELP_NAME=§8/§7team changename §8- §7Change your team name. +TEAM_HELP_COLOR=§8/§7team changecolor §8- §7Change your team color. +TEAM_HELP_LEADER=§8/§7team promote §8- §7Promote someone to leader. +TEAM_HELP_STEP_BACK=§8/§7team stepback §8- §7Demote yourself from leader. +TEAM_HELP_SERVER=§8/§7team server §8[§eIP/address§8] §8(§7port§8) §8- §7Set the address for your teamserver. + +#Team Create +TEAM_CREATE_USAGE=§8/§7team create §8[§eteam shortcut§8] §8[§eteam name§8] +TEAM_CREATE_CREATED=§7You have created the team §e{0}§7! + +#Team Join +TEAM_JOIN_NO_INVITE=§7You have no pending invitation. +TEAM_JOIN_USAGE=§8/§7team join §8[§eteam§8] +TEAM_JOIN_INVITED=§7You were invited by the following teams§8: §e{0} +TEAM_JOIN_NOT_BY_TEAM=§cYou have no pending invitation from this team. +TEAM_JOIN_JOINED=§7You joined the team §e{0}§7! + +#Team Leave +TEAM_OTHER_LEADER_REQUIRED=§cPlease appoint another leader first! +TEAM_LEAVE_LEFT=§7You left the team! + +#Team Step Back +TEAM_STEP_BACK=§7You have stepped back from your position as leader§8! + +#Team Invite +TEAM_INVITE_USAGE=§8/§7team invite §8[§eplayer§8] +TEAM_INVITE_NO_PLAYER=§cThis player is unknown. +TEAM_INVITE_IN_TEAM=§cThis player is already in a team. +TEAM_INVITE_ALREADY_INVITED=§cAn invitation is already pending. +TEAM_INVITE_INVITED=§7You have invited §e{0} §7in your team! +TEAM_INVITE_INVITED_TARGET=§7You were invited into the team §{0}{1}§7! + +#Team Remove +TEAM_REMOVE_USAGE=§8/§7team remove §8[§eplayer§8] +TEAM_REMOVE_NOT_PLAYER=§cThis player does not exist. +TEAM_REMOVE_NOT_LEADER=§cA leader can not be kicked. +TEAM_REMOVE_INVITE=§7The invitation was retracted. +TEAM_REMOVE_NO_INVITE=§cNo pending invitation is due for this player. +TEAM_REMOVE_NOT_IN_TEAM=§cThis player is not in your team. +TEAM_REMOVE_REMOVED=§7You removed this player from the team. +TEAM_REMOVE_REMOVED_TARGET=§cYou were removed from the team. + +#Team Kuerzel +TEAM_KUERZEL_USAGE=§8/§7team changekuerzel §8[§eshortcut§8] +TEAM_KUERZEL_CHANGED=§7You changed your teams shortcut! +TEAM_KUERZEL_LENGTH=§cA team shortcut has to consist of 2 to 4 characters. +TEAM_KUERZEL_TAKEN=§cThere is already a team with that shortcut. + +#Team Name +TEAM_NAME_USAGE=§8/§7team changename §8[§eteam name§8] +TEAM_NAME_CHANGED=§7You have renamed your team! +TEAM_NAME_LENGTH=§cA team name has to consist of 4 to 15 characters. +TEAM_NAME_TAKEN=§cThere is already a team with that name. + +#Team Leader +TEAM_LEADER_USAGE=§8/§7team promote §8[§emember§8] +TEAM_LEADER_NOT_USER=§cUnknown player. +TEAM_LEADER_NOT_MEMBER=§cThis player is not in your team. +TEAM_LEADER_PROMOTED=§7You made §e{0} §7a leader! + +#Team Info +TEAM_INFO_USAGE=§8/§7team info §8[§eTeamname§8] +TEAM_INFO_TEAM=§7Team §e{0} §8[§{1}{2}§8] +TEAM_INFO_LEADER=§7Leader ({0})§8: {1} +TEAM_INFO_MEMBER=§7Member ({0})§8: {1} +TEAM_INFO_EVENTS=§7Events§8: §e{0} + +#Team List +TEAM_LIST_NOT_PAGE=§cNo page number entered +TEAM_LIST_UNKNOWN_PAGE=§cInvalid page entered +TEAM_LIST_HEADER=§7§lTeam list §7{0}§8/§7{1} +TEAM_LIST_TEAM=§{0}{1} §e{2} +TEAM_LIST_TEAM_HOVER=§7Team info +TEAM_LIST_PAGE=Page »» +TEAM_LIST_NEXT=§eNext page +TEAM_LIST_PREV=§ePrevious page + +#Team Event +TEAM_EVENT_USAGE=§8/§7team event §8[§eEvent§8] - §7to take part +TEAM_EVENT_HEADER=§7Your team takes part in the following events§8: +TEAM_EVENT_EVENT=§7{0}§8: §e{1} +TEAM_EVENT_NO_EVENT=§cThis event does not exist +TEAM_EVENT_OVER=§cThe registration period for this event is already over +TEAM_EVENT_LEFT=§7Your team no longer takes part in this event +TEAM_EVENT_JOINED=§7Your team now takes part in the event §e{0}§7! +TEAM_EVENT_HOW_TO_LEAVE=§7To cancel the participation repeat the command. + +#Team Color +TEAM_COLOR_TITLE=Choose color + +#Team Server +TEAM_SERVER_USAGE=§8/§7team server §8[§eIP/address§8] §8(§7port§8) §8- §7Sets the address of the team server. +TEAM_SERVER_SET=§7You changed the team server address§8! +TEAM_SERVER_PORT_INVALID=§cInvalid port number. +TEAM_SERVER_ADDRESS_INVALID=§cInvalid address. +TEAM_NO_ADDRESS=§cNo team server address set. +TEAM_OFFLINE=§cTeam server may be offline. +TEAM_TP_NO_TEAM=§cUnknown team. + +#TpCommand +TP_USAGE=§8/§7tp §8[§eplayer§8] +TP_USAGE_EVENT=§8/§7tp §8[§ePlayer §7or §eteam§8] + +#UnignoreCommand +UNIGNORE_USAGE=§8/§7unignore §8[§eplayer§8] +UNIGNORE_NOT_PLAYER=§cThis player does not exist! +UNIGNORE_NOT_IGNORED=§cYou are not ignoring this player. +UNIGNORE_UNIGNORED=§7You ignored §e{0}§8. + +#WebregisterCommand +WEB_USAGE=§8/§7webpassword §8[§epassword§8] +WEB_UPDATED=§7Your password was updated. +WEB_CREATED=§7Your webaccount was created. +WEB_PASSWORD_LENGTH=§cYour password is shorter than 8 characters. + +#ChatListener +CHAT_LIXFEL_ACTION_BAR=§4§lTechnical problems? +CHAT_LIXFEL_1=You called me! +CHAT_LIXFEL_2=Unfortunately I am only human and do not hear everything. +CHAT_LIXFEL_3=Therefore I ask you to deposit the problem or the error in the forum in the category §eReport error §7with a sufficient description +CHAT_LIXFEL_4=Thank you. +CHAT_LIXFEL_5=I wish you still a smooth gaming experience. +CHAT_YOYONOW_1=You called me! +CHAT_YOYONOW_2=I would like to recommend you the command "/bug ".. +CHAT_YOYONOW_3=Thank you. +CHAT_YOYONOW_4=I wish you still a smooth gaming experience. +CHAT_CHAOSCAOT_1=You called me! +CHAT_CHAOSCAOT_2=If something is broken, just say it\'s a feature. +CHAT_CHAOSCAOT_3=And if it is a feature, just then it can\'t be broken. +CHAT_CHAOSCAOT_4=Broken is just a definition. So if you define it as a feature, it\'s not broken. +CHAT_CHAOSCAOT_5=And if you define it as broken, then tell us using the command "/bug ". +CHAT_CHAOSCAOT_6=Thank you and goodbye. +CHAT_RECEIVE=§cTo be able to send chat messages, you must also receive them! +CHAT_NO_LINKS=§cYou may not send links. +CHAT_BC_USAGE=§8/§7bc §8[§emessage§8] +CHAT_NO_RECEIVER=§cNobody receives your message +CHAT_EMPTY=§cDon\'t write meaningless empty messages. + +CHAT_SERVERTEAM=§8STC §e{0}§8» §f{2} +CHAT_GLOBAL={3}{4}{5}{6}{0}§8» {7}{2} +CHAT_DISCORD_GLOBAL=§8Dc {5}{6}{0}§8» {7}{2} +CHAT_TEAM=§8TC §e{0}§8» §f{2} +CHAT_MSG=§e{0}§8»§e{1} §7{2} + +#CheckListner +CHECK_UNCHECKED=§7You still have §e{0} §7unchecked schematic§8(§7s§8)! +CHECK_CHECKING=§cYou are checking a Schematic! + +#ConnectionListener +JOIN_ARENA=§7Click §ehere§7 to join §e{0} +JOIN_ARENA_HOVER=§eJoin Arena +JOIN_FIRST=§7Please greet §e{0}§7 on the server§8! + +#EventModeListener +EVENTMODE_KICK=§cYou are not an event participant. + +#PollSystem +POLL_HEADER=§e§lPoll +POLL_HEADER2=§7Click the answer you like! +POLL_QUESTION=§e{0} +POLL_ANSWER=§7{0} +POLL_ANSWER_HOVER=§eChoose {0} + +#TablistManager +TABLIST_PHASE_WEBSITE=§8Website: https://§eSteam§8War.de +TABLIST_PHASE_DISCORD=§8Discord: https://§eSteam§8War.de/discord +TABLIST_FOOTER=§e{0} {1}§8ms §ePlayers§8: §7{2} +TABLIST_BAU=§7§lBuild +LIST_COMMAND=§e{0}§8: §7{1} + +#EventStarter +EVENT_FIGHT_BROADCAST=§eClick here §7for the fight §{0}{1} §8vs §{2}{3} +EVENT_FIGHT_BROADCAST_HOVER=§eJoin Event + +#SubserverSystem +SERVER_IGNORED=§cThis player has blocked you! +SERVER_ADD_MEMBER=§e{0} §7wants to join your Build server. +SERVER_ADD_MESSAGE=§7Click §ehere §7if you want to allow this. +SERVER_ADD_MESSAGE_HOVER=§8/§7build addmember §e{0} +SERVER_WORLD_ERROR=§cCreating the world failed. + + +#WhoisCommand +WHOIS_USAGE=§c/whois [player/ID] [-a/-m] +WHOIS_USERNAME=§7Username§8: §e{0} +WHOIS_PREFIX=§7Chat-Prefix§8: {0} +WHOIS_UUID=§7UUID§8: §e{0} +WHOIS_UUID_HOVER=§eCopy UUID +WHOIS_ID=§7ID§8: §e{0} +WHOIS_PERMS=§7Perms§8: §7{0} +WHOIS_DISCORD_ID=§7Discord-ID§8: §e{0} +WHOIS_JOINED_FIRST=§7Joined on§8: §e{0} +WHOIS_HOURS_PLAYED=§7Online Time§8: §e{0}h +WHOIS_CURRENT_PLAYED=§7Current Online Time§8: §e{0}m +WHOIS_CURRENT_SERVER=§7Current Server§8: §e{0} +WHOIS_CURRENT_PROTOCOL=§7Current Protocol§8: §e{0} +WHOIS_TEAM=§7Team§8: §e[§{0}{1}§e] {2} +WHOIS_TEAM_HOVER=§eShow {0} +WHOIS_PUNISHMENTS=§7Punishments: +WHOIS_PUNISHMENT=§7{0}§8» §f§l{1}: §e{2} - {3} §f{4} +WHOIS_NO_PUNISHMENT=§a✓ §7This player has no active punishment. +WHOIS_NO_ALL_PUNISHMENT=§a✓ §7The player has not done anything yet. +WHOIS_ACTIVE_MODS=§7Active Mods ({0}): {1} +WHOIS_NO_ACTIVE_MODS=§7This player has no active mods. +WHOIS_PLATFORM=§7Modloader: §e{0} + +#VerifyCommand +VERIFY_USAGE=§c/verify [Code] +VERIFY_INVALID=§cInvalid Code +VERIFY_SUCCESS=§7Successfully linked to the Discord account §e{0} + +#Discord +DISCORD_TICKET_HOVER=§eTo the message +DISCORD_TICKET_MESSAGE=§7Ticket §e{0}§7» §f§l{1}: §7{2} +DISCORD_TICKET_NEW=§7Ticket §e{0}§7» §aTicket was created! +DISCORD_TICKET_CLOSED=§7Ticket §e{0}§7» §cTicket was closed! + +#GDPR Query +GDPR_STATUS_WEBSITE=§7Website cannot be packed automatically and therefore must be added manually. +GDPR_STATUS_WORLD=§7Packing Build worlds... +GDPR_STATUS_INVENTORIES=§7Searching and packing inventories... +GDPR_STATUS_DATABASE=§7Packing database contents... +GDPR_STATUS_LOGS=§7Searching and packing logs... +GDPR_STATUS_FINISHED=§7Packing complete + +#Playtime Command +HOURS_PLAYED=§7Your playtime is§8: §e{0}h + +#Arena command +ARENA_NOT_FOUND=§cThe specified arena could not be found + +#Rank +RANK_PLAYER_NOT_FOUND=§cPlayer not found +RANK_PLAYER_FOUND=§eRank §7of §e{0} +RANK_HEADER=§e{0} {1} +RANK_UNPLACED=§7unranked +RANK_PLACED=§e{0}§8. §7with §e{1} §7Elo§8. +RANK_EMBLEM=§7Emblem§8: {0} + +#Fabric Mod Sender +MODIFICATION_BAN_MESSAGE=You tried to bypass / modify the FabricModSender! +MODIFICATION_BAN_LOG={0} has tried to edit / bypass the FabricModSender! Reason: {1} + +#Arena Merging +FIGHT_MERGE_TITLE=Equal fight found! +FIGHT_MERGE_DECLINE=§cLaunch new arena +FIGHT_MERGE_ACCEPT=§aJoin fight +FIGHT_MERGE_INFO_LORE_1=§8By: §e{0} +FIGHT_MERGE_OFFLINE=§7The proposed arena has been terminated in the meantime, a new arena will be started. +FIGHT_MERGE_INFO=§e{0}§8: §e{1} + +#Locale Locking +LOCK_LOCALE_CHANGED=§aLanguage saved + +#Builder Cloud +BUILDERCLOUD_USAGE=§8/§7buildercloud §8[§eversion§8] §8[§emap§8] +BUILDERCLOUD_CREATE_USAGE=§8/§7buildercloud create §8[§eversion§8] §8[§emap§8] §8<§7generator§8> +BUILDERCLOUD_RENAME_USAGE=§8/§7buildercloud rename §8[§eversion§8] §8[§emap§8] §8[§enew name§8] +BUILDERCLOUD_DEPLOY_USAGE=§8/§7deployarena §8[§egamemode§8] §8[§eversion§8] §8[§emap§8] +BUILDERCLOUD_DEPLOY_FINISHED=§7Map deployment finished. +BUILDERCLOUD_VERSION=§cUnknown version. +BUILDERCLOUD_EXISTING_MAP=§cMap already exists. +BUILDERCLOUD_UNKNOWN_MAP=§cUnknown map. +BUILDERCLOUD_RENAMED=§7Map rename successful. + +# Advent Calendar +ADVENT_CALENDAR_TITLE=§eAdvent Calendar +ADVENT_CALENDAR_DAY=§7Day§8: §e{0} +ADVENT_CALENDAR_MESSAGE=§eDid you already open your advent calendar? +ADVENT_CALENDAR_MESSAGE_HOVER=§eClick to open! +ADVENT_CALENDAR_OPEN=§7You got §e{0} §7from the advent calendar! + +#Mod Command +MOD_COMMAND_SET_USAGE=§7/mod set [mod name] [platform] [ModType 1-4] +MOD_COMMAND_GET_USAGE=§7/mod get [mod name] [platform] +MOD_CHANGED_TYPE=§7Successfully reclassified mod {0} on platform {1} to type {2}! +MOD_NO_MORE_UNCLASSIFIED_MODS=§7No more unclassified mods found in databank! +MOD_FOUND_NEXT_MOD=§7Next unclassified mod is {0} on platform {1}! +MOD_COMMAND_NOT_FOUND_IN_DATABASE=§7The Mod {0} on platform {1} was§c not §7found in the database! +MOD_COMMAND_INFO=§7The mod {0} on platform {1} is of the type {2}. +MOD_COMMAND_GUI_TITLE=Unclassified Mods +MOD_COMMAND_CLASSICIATION_GUI=Mod Type Changer +MOD_OPEN_GUI=§7Open Gui +MOD_TITLE_FILTER=Filter +MOD_UNCLASSIFIED=§7Unclassified +MOD_ALLOWED=§aAllowed +MOD_FORBIDDEN=§eForbidden +MOD_AUTOBAN=§cAutoban +MOD_YT=§5YT Only +MOD_ITEM_BACK=§7Back + +#StreamInv +INV_PAGE_BACK=§{0}Seite zurück +INV_PAGE_NEXT=§{0}Seite vor + +#Discord +DC_UNLINKED=For this action your Discord account has to be linked to your Minecraft account. To link your accounts go onto the SteamWar Discord to the `regeln-infos` Channel and click on `Minecraft Verknüpfen`. +DC_TITLE_SCHEMINFO=Schematic Info +DC_SCHEM_ACCEPT=Your schematic **{0}** has been accepted. +DC_SCHEM_DECLINE=Your schematic **{0}** has been declined.\n**Reason:**{1} +DC_AUTH_SUCCESS=:white_check_mark: You\'re Discord account has been linked with **{0}**. +DC_ROLE_ADDED=:tada: Your getting {0} now. +DC_ROLE_REMOVED=Your not getting {0} anymore. +DC_TICKET_CREATED=Your Ticket {0} has been created. +DC_TICKET_TITLE=SteamWar Ticket +DC_TICKETINTRO_REPORT=Please answer for the punishment of the misconduct the following questions as accurately as possible and attach evidence whenever possible:\n - Which player(s)?\n - On which Server?\n - At what time?\n - Type and nature of the misconduct? +DC_TICKETINTRO_IDEA=Describe your idea as detailed as possible. Hereto belongs: What, Why, How, Where? +DC_TICKETINTRO_BUG=Please describe the observed unexpected or incorrect behaviour of our software. If necessary describe steps to reproduce the error. +DC_TICKETINTRO_QUESTION=Please ask your question. A staff member will address the question soon. +DC_TICKETINTRO_APPEAL=Asking creates wonders. +DC_TICKET_CLOSE=Close + +DC_SCHEMUPLOAD_NOPERM=You\'re not allowed to upload schematics. +DC_SCHEMUPLOAD_IGNORED=Skipping `{0}`, not a schematic file. +DC_SCHEMUPLOAD_INVCHAR=`{0}` has invalid characters in its name. +DC_SCHEMUPLOAD_SUCCESS=`{0}` was uploaded successfully. +DC_SCHEMUPLOAD_ERROR=An error has occured during the upload of `{0}`. For more information ask a Developer. diff --git a/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties new file mode 100644 index 00000000..c52b6a99 --- /dev/null +++ b/VelocityCore/src/de/steamwar/messages/BungeeCore_de.properties @@ -0,0 +1,683 @@ +COMMAND_SYSTEM_ERROR = §cFehler beim Ausführen des Befehls! + +PREFIX=§eSteam§8War» +SPACER= +TIMEFORMAT=dd.MM.yyyy HH:mm + +UNKNOWN_COMMAND=§cUnbekannter Befehl. +UNKNOWN_PLAYER=§cDiesen Spieler gibt es nicht. +UNKNOWN_TEAM=§cDieses Team gibt es nicht. +UNKNOWN_EVENT=$cDieses Event gibt es nicht. +INVALID_TIME=§cUngültige Zeitangabe. + +DEV_NO_SERVER=§cDer Server ist derzeit nicht erreichbar. +DEV_UNKNOWN_SERVER=§cBitte gib einen DevServer an. + +DISABLED=§cDerzeit deaktiviert. + +SERVER_START_OVERLOAD=§cDer Serverstart wurde aufgrund von Überlastung abgebrochen. Versuche es später erneut. +UPDATE_INTERRUPTION=§cBitte erneut versuchen. Ein Softwareupdate hat die Aktion unterbrochen. + +#Help command +HELP_LOBBY=§7Kehre von überall mit §8/§el §7zur Lobby zurück! +HELP_LOBBY_HOVER=§eZurück zur Lobby +HELP_BAU=§7Komme mit §8/§ebau §7auf den Bauserver! +HELP_BAU_HOVER=§eZum Bauserver +HELP_BAUSERVER=§7Erhalte mit §8/§ehelp bau §7Hilfe zum Bauserver! +HELP_BAUSERVER_HOVER=§eHilfe zum Bauserver +HELP_FIGHT=§7Starte mit §8/§efight §7einen neuen Kampf! +HELP_FIGHT_HOVER=§eZum Kampfsystem +HELP_CHALLENGE=§7Tippe §8/§echallenge§7, um jemanden herauszufordern! +HELP_CHALLENGE_HOVER=§eHerausfordern +HELP_HISTORIC=§7Starte mit §8/§ehistoric §7einen historischen Kampf! +HELP_HISTORIC_HOVER=§eHistorische Kämpfe +HELP_TEAM=§8/§eteam§7 für das Teamsystem! +HELP_TEAM_HOVER=§eTeamverwaltung +HELP_JOIN=§7Trete mit §8/§ejoin §8[§eSpieler§8] §7einem Kampf bei! +HELP_JOIN_HOVER=§eSpieler beitreten +HELP_LOCAL=§7Schreibe mit §8/§elocal §7nur auf dem lokalen Server! +HELP_LOCAL_HOVER=§eLokaler Chat + +HELP_TNT=§8/§7tnt §8- §7(de)aktiviert Explosionsschaden +HELP_FIRE=§8/§7fire §8- §7(de)aktiviert Feuerschaden +HELP_TESTBLOCK=§8/§7testblock §8- §7Erneuert den nächsten Testblock +HELP_RESET=§8/§7reset §8- §7Setzt die derzeitige Region zurück +HELP_SPEED=§8/§7speed §8- §7Ändert deine Fluggeschwindigkeit +HELP_NV=§8/§7nv §8- §7(de)aktiviert Nachtsicht +HELP_TRACE=§8/§7trace §8- §7Gibt einen Überblick über den TNT-Tracer +HELP_TPSLIMIT=§8/§7tpslimit §8- §7Gibt einen Überblick über den TPS-Limiter +HELP_LOADER=§8/§7loader §8- §7Nutze den automatischen Kanonenlader +HELP_PROTECT=§8/§7protect §8- §7Schützt den Boden der (M)WG-Region +HELP_FREEZE=§8/§7freeze §8- §7Unterbindet Blockupdates +HELP_SKULL=§8/§7skull §8- §7Gibt dir den Kopf eines Spielers +HELP_DEBUGSTICK=§8/§7debugstick §8- §7Gibt dir einen Debugstick (1.15+) +HELP_BAUINFO=§8/§7bauinfo §8- §7Gibt dir Informationen über den Bauserver +HELP_SCHEMSUBMIT=§7Für ein Tutorial über das Freischalten deines AirShips§8/§7MiniWarGear§8/§7WarGear§8/§7WarShips klicke §ehier§8! + +HELP_BAU_GROUP_ADMIN=§7Bauserver-Verwaltungsbefehle +HELP_BAU_GROUP_ADMIN_HOVER=§eAlle Verwaltungsbefehle +HELP_BAU_GROUP_ADMIN_TITLE=§7Alle Verwaltungsbefehle§8: +HELP_BAU_GROUP_OTHER=§7Weitere Bauserverbefehle +HELP_BAU_GROUP_OTHER_HOVER=§eWeitere Bauserverbefehle +HELP_BAU_GROUP_OTHER_TITLE=§7Weitere Bauserverbefehle§8: +HELP_BAU_GROUP_WE=§7WorldEdit-Abkürzungen +HELP_BAU_GROUP_WE_HOVER=§eWorldEdit-Abkürzungen +HELP_BAU_GROUP_WE_TITLE=§7WorldEdit-Abkürzungen§8: +HELP_BAU_GROUP_PLAYER=§7Spielerbefehle +HELP_BAU_GROUP_PLAYER_HOVER=§eSpielerbefehle +HELP_BAU_GROUP_PLAYER_TITLE=§7Spielerbefehle§8: +HELP_BAU_GROUP_WORLD=§7Weltverändernde Bauserverbefehle +HELP_BAU_GROUP_WORLD_HOVER=§eWeltverändernde Bauserverbefehle +HELP_BAU_GROUP_WORLD_TITLE=§7Weltverändernde Bauserverbefehle§8: + +HELP_BAU_TP=§8/§ebau tp §8- §7Gehe auf Bauserver von Freunden! +HELP_BAU_TP_HOVER=§eZu einem anderen Bauserver +HELP_BAU_ADDMEMBER=§8/§ebau addmember §8- §7Fügt einen Freund hinzu +HELP_BAU_ADDMEMBER_HOVER=§eFüge einen Freund hinzu +HELP_BAU_DELMEMBER=§8/§ebau delmember §8- §7Entfernt einen Spieler +HELP_BAU_DELMEMBER_HOVER=§eEntfernt einen Spieler +HELP_BAU_SET_SPECTATOR=§8/§ebau setspectator §8- §7Zuschauen auf dem Bauserver +HELP_BAU_SET_SPECTATOR_HOVER=§eSetzt die Rolle Spectator +HELP_BAU_SET_BUILDER=§8/§ebau setbuilder §8- §7Bauen, WorldEdit, BauSystem Features +HELP_BAU_SET_BUILDER_HOVER=§eSetzt die Rolle Builder +HELP_BAU_SET_SUPERVISOR=§8/§ebuild setsupervisor §8- §7Starten des Baus. Schematics erstellen +HELP_BAU_SET_SUPERVISOR_HOVER=§eSetzt die Rolle Supervisor +HELP_BAU_DELETE=§8/§ebau delete §8- §7Setzt deine Bauwelt zurück +HELP_BAU_DELETE_HOVER=§eBauwelt zurücksetzen +HELP_BAU_TESTARENA=§8/§ebau testarena §8- §7Starte eine Testarena +HELP_BAU_TESTARENA_HOVER=§eTestarena starten +HELP_BAU_LOCK=§8/§ebau lock §8- §7Sperre deinen Bauserver für bestimmte Spielergruppen +HELP_BAU_LOCK_HOVER=§eSperre deinen Bau +HELP_BAU_UNLOCK=§8/§ebau unlock §8- §7Öffne deinen Bauserver für alle hinzugefügten Spieler +HELP_BAU_UNLOCK_HOVER=§eÖffne deinen Bau + +#Usage description of various commands +USAGE_ALERT=§8/§7alert §8[§eNachricht§8] +USAGE_IGNORE=§8/§7ignore §8[§eSpieler§8] + +#ModListener +MOD_RED_SING=Versuchte Benutzung des Mods {0} +MOD_RED_PLUR=Versuchte Benutzung der Mods:\n{0} +MOD_YELLOW_SING=§7Deaktiviere den Mod §e{0}§7, um weiter auf §eSteam§8War §7spielen zu können. +MOD_YELLOW_PLUR=§7Deaktiviere die Mods\n§e{0}\n§7um weiter auf §eSteam§8War §7spielen zu können. +MODS_CHECKED=§7Deine installierten Mods wurden überprüft.\n§aDu kannst nun §eSteam§8War §abetreten§8. + +#Various commands +STAT_SERVER=§7Server §e{0}§8: §7Startfähig §e{1} §7Serveranzahl §e{2} + +#Ban&Mute-Command +PUNISHMENT_USAGE=§8/§7{0} §8[§eSpieler§8] [§edd§8.§emm§8.§eyyyy §7oder §edd§8.§emm§8.§eyyyy§8_§ehh§8:§emm §7oder §ezahl§8[§eh§7our|§ed§7ay|§ew§7eek|§em§7onth|§ey§7ear§8] §7oder §eperma§8] [§eGrund§8] +PUNISHMENT_USAGE_REASON=§cBitte gib einen Grund an. +UNPUNISHMENT_USAGE=§8/§7{0} §8[§eSpieler§8] + +PUNISHMENT_UNTIL=bis zum {0} +PUNISHMENT_PERMA=permanent + +BAN_TEAM={0} §e{1} §7wurde von §e{2} {3} §e§lgebannt§8. §7Grund§8: §f{4} +BAN_PERMA=§7Du bist §epermanent §e§lgebannt§8. §7Grund§8: §e{0} +BAN_UNTIL=§7Du bist §ebis zum {0} §e§lgebannt§8. §7Grund§8: §e{1} +UNBAN_ERROR=§cDer Spieler ist nicht gebannt. +UNBAN=§7Du hast §e{0} §e§lentbannt. + +BAN_AVOIDING_ALERT=§cMögliche Bannumgehung durch §r{0}§c: {1} +BAN_AVOIDING_LIST=§c{0} §e{1} +BAN_AVOIDING_BAN_HOVER=§cBanne Spieler wegen Bannumgehung + +MUTE_TEAM={0} §e{1} §7wurde von §e{2} {3} §e§lgemuted§8. §7Grund§8: §f{4} +MUTE_PERMA=§7Du bist §epermanent §e§lgemuted§8. §7Grund§8: §e{0} +MUTE_UNTIL=§7Du bist §ebis zum {0} §e§lgemuted§8. §7Grund§8: §e{1} +UNMUTE_ERROR=§cDer Spieler ist nicht gemuted. +UNMUTE=§7Du hast §e{0} §e§lentmuted. + +NOSCHEMRECEIVING_TEAM={0} §e{1} §7wurde von §e{2} {3} §7vom §e§lSchematicerhalten§7 ausgeschlossen§8: §f{4} +NOSCHEMRECEIVING_PERMA=§7Du bist §epermanent §7vom Erhalten von §e§lSchematics§7 ausgeschlossen§8: §e{0} +NOSCHEMRECEIVING_UNTIL=§7Du bist §ebis zum {0} §7vom Erhalten von §e§lSchematics§7 ausgeschlossen§8: §e{1} +UNNOSCHEMRECEIVING_ERROR=§cDer Spieler ist nicht vom Erhalten von Schematics ausgeschlossen. +UNNOSCHEMRECEIVING=§e{0} §7darf nun wieder §e§lSchematics§7 erhalten§8. + +NOSCHEMSHARING_TEAM={0} §e{1} §7wurde von §e{2} {3} §7vom §e§lSchematicverteilen§7 ausgeschlossen§8: §f{4} +NOSCHEMSHARING_PERMA=§7Du bist §epermanent §7vom §e§lVerteilen von Schematics§7 ausgeschlossen§8: §e{0} +NOSCHEMSHARING_UNTIL=§7Du bist §ebis zum {0} §7vom §e§lVerteilen von Schematics§7 ausgeschlossen§8: §e{1} +UNNOSCHEMSHARING_ERROR=§cDer Spieler ist nicht vom Verteilen von Schematics ausgeschlossen. +UNNOSCHEMSHARING=§e{0} §7darf nun wieder §e§lSchematics§7 verteilen§8. + +NOSCHEMSUBMITTING_TEAM={0} §e{1} §7wurde von §e{2} {3} §7vom §e§lSchematiceinsenden§7 ausgeschlossen§8: §f{4} +NOSCHEMSUBMITTING_PERMA=§7Du bist §epermanent §7vom §e§lEinsenden von Schematics§7 ausgeschlossen§8: §e{0} +NOSCHEMSUBMITTING_UNTIL=§7Du bist §ebis zum {0} §7vom §e§lEinsenden von Schematics§7 ausgeschlossen§8: §e{1} +UNNOSCHEMSUBMITTING_ERROR=§cDer Spieler ist nicht vom Einsenden von Schematics ausgeschlossen. +UNNOSCHEMSUBMITTING=§e{0} §7darf nun wieder §e§lSchematis§7 einsenden§8. + +NODEVSERVER_TEAM={0} §e{1} §7hat §e{2} §7mit Grund §f{4}§7 zu genervt und hat daher §e§lDevserververbot§7 erhalten§8, §f{3} +NODEVSERVER_PERMA=§7Du bist §epermanent §7vom §e§lDevserver§7 ausgeschlossen§8: §e{0} +NODEVSERVER_UNTIL=§7Du bist §ebis zum {0} §7vom §e§lDevserver§7 ausgeschlossen§8: §e{1} +UNNODEVSERVER_ERROR=§cDer Spieler ist nicht vom Devserver ausgeschlossen. +UNNODEVSERVER=§e{0} §7darf nun wieder dem §e§lDevserver§7 beitreten§8. + +NOFIGHTSERVER_TEAM={0} §e{1} §7wurde von §e{2} {3} §7vom §e§lKämpfen§7 ausgeschlossen§8: §f{4} +NOFIGHTSERVER_PERMA=§7Du bist §epermanent §7vom §e§lKämpfen§7 ausgeschlossen§8: §e{0} +NOFIGHTSERVER_UNTIL=§7Du bist §ebis zum {0} §7vom §e§lKämpfen§7 ausgeschlossen§8: §e{1} +UNNOFIGHTSERVER_ERROR=§cDer Spieler ist nicht vom Kämpfen ausgeschlossen. +UNNOFIGHTSERVER=§e{0} §7darf nun wieder §e§lKämpfen§7 beitreten§8. + +NOTEAMSERVER_TEAM={0} §e{1} §7wurde von §e{2} {3} §7vom §e§lTeamserver§7 setzen ausgeschlossen§8: §f{4} +NOTEAMSERVER_PERMA=§7Du bist §epermanent §7vom §e§lTeamserver§7 setzen ausgeschlossen§8: §e{0} +NOTEAMSERVER_UNTIL=§7Du bist §ebis zum {0} §7vom §e§lTeamserver§7 setzen ausgeschlossen§8: §e{1} +UNNOTEAMSERVER_ERROR=§cDer Spieler ist nicht vom Teamserver setzten ausgeschlossen. +UNNOTEAMSERVER=§e{0} §7darf nun wieder §e§lTeamserver§7 setzen§8. + +NOTE_TEAM={0} §e{1} §7erhielt von §e{2} {3} §7die §e§lNotiz§7§8: §f{4} + +#BugCommand +BUG_MESSAGE=§7Bitte beschreibe das Problem in einem Discordticket genauer und gebe dabei die Bug-ID §e{0} §7an§8. + +#IgnoreCommand +IGNORE_YOURSELF=§cWie willst du dich selber ignorieren? +IGNORE_ALREADY=§cDu ignorierst diesen Spieler bereits. +IGNORE_MESSAGE=§7Du ignorierst nun §e{0}§8. + +#PollresultCommand +POLLRESULT_NOPOLL=§cDerzeit läuft keine Umfrage. +POLLRESULT_HEADER=§eEs haben {0} abgestimmt auf die Frage: §7{1} + +#BauCommand +BAU_ADDMEMBER_USAGE=§8/§7bau addmember §8[§eSpieler§8] +BAU_ADDMEMBER_SELFADD=§cDu brauchst dich nicht selbst hinzufügen! +BAU_ADDMEMBER_ISADDED=§cDieser Spieler ist bereits Mitglied auf deiner Welt. +BAU_ADDMEMBER_ADDED=§aDer Spieler wurde zu deiner Welt hinzugefügt. +BAU_ADDMEMBER_ADDED_TARGET=§aDu wurdest zu der Welt von §e{0} §ahinzugefügt. +BAU_TP_USAGE=§8/§7bau tp §8[§eSpieler§8] +BAU_TP_NOALLOWED=§cDu darfst dich nicht auf diese Welt teleportieren. +BAU_LOCKED_NOALLOWED=§cDer Bauserver ist momentan gesperrt. +BAU_LOCK_BLOCKED=§cDeine Bausperre hat den Beitritt von §e{0} §cverhindert. +BAU_LOCKED_OPTIONS=§7Bauserver-Sperroptionen§8: §cnobody§8, §eserverteam§8, §eteam_and_serverteam§8, §eteam§8, §aopen +BAU_LOCKED_NOBODY=§7Du hast deinen Bau für alle Spieler geschlossen. +BAU_LOCKED_SERVERTEAM=§7Du hast deinen Bau für alle außer hinzugefügte Serverteammitglieder gesperrt. +BAU_LOCKED_TEAM_AND_SERVERTEAM=§7Du hast deinen Bau für alle außer hinzugefügte Teammitglieder und Serverteammitglieder gesperrt. +BAU_LOCKED_TEAM=§7Du hast deinen Bau für alle außer hinzugefügte Teammitglieder gesperrt. +BAU_LOCKED_OPEN=§7Du hast deinen Bau für alle hinzugefügten Spieler geöffnet. +BAU_DELMEMBER_USAGE=§8/§7bau delmember §8[§eSpieler§8] +BAU_DELMEMBER_SELFDEL=§cDu kannst dich nicht selbst entfernen! +BAU_DELMEMBER_DELETED=§cDer Spieler wurde entfernt. +BAU_DELMEMBER_DELETED_TARGET=§cDu wurdest von der Welt von §e{0} §centfernt. +BAU_DELETE_DELETED=§aDeine Welt wird zurückgesetzt. +BAU_DELETE_GUI_NAME=§eWirklich Welt löschen? +BAU_DELETE_GUI_CANCEL=§cAbbrechen +BAU_DELETE_GUI_DELETE=§aLöschen +BAU_START_ALREADY=§cDer Server startet bereits. +BAU_MEMBER_NOMEMBER=§cDer Spieler ist kein Mitglied deiner Welt! +BAU_MEMBER_SET_USAGE=§8/§7bau {0} §8[§eSpieler§8] +BAU_MEMBER_SET_TARGET=§7Du bist nun ein §e{1}§7 auf der Welt von §e{0}§7. +BAU_MEMBER_SET=§7Der Spieler ist nun §e{0}§7. +BAU_MEMBER_SET_SPECTATOR = Zuschauer +BAU_MEMBER_SET_BUILDER = Builder +BAU_MEMBER_SET_SUPERVISOR = Supervisor +BAU_START_NOT_ALLOWED = §cDu darfst diesen Bauserver nicht starten. + +#ChallengeCommand +CHALLENGE_USAGE=§8/§7challenge §8[§eSpieler§8] +CHALLENGE_OFFLINE=§cDer Herausgeforderte ist nicht online. +CHALLENGE_SELF=§cSchizophren? +CHALLENGE_IGNORED=§cDer Herausgeforderte hat dich geblockt. +CHALLENGE_INARENA=§cDer Herausgeforderte ist bereits in einer Arena. +CHALLENGE_BROADCAST=§e{0}§7-§eDuell§7: §e{1} §7vs §e{2} +CHALLENGE_BROADCAST_HOVER=§aZuschauen +CHALLENGE_CHALLENGED=§7Du hast §e{0} §7zu einem §e{1}-Kampf §7herausgefordert! +CHALLENGE_CHALLENGED_TARGET=§e{0} §7 hat dich zu einem §e{1}-Kampf §7{2}herausgefordert! +CHALLENGE_CHALLENGED_MAP=auf §e{0} §7 +CHALLENGE_ACCEPT=§7Klicke §ehier§7, um die Herausforderung anzunehmen +CHALLENGE_ACCEPT_HOVER=§aHerausforderung annehmen + +#EventCommand +EVENT_TIME_FORMAT=HH:mm +EVENT_DATE_FORMAT=dd.MM. +EVENT_USAGE=§8/§7event §8[§eTeam§8] - §7Um dich zum Kampf zu teleportieren +EVENT_NO_TEAM=§cDieses Team gibt es nicht +EVENT_NO_FIGHT_TEAM=§cDas Team kämpft derzeit nicht +EVENT_NO_CURRENT=§cDerzeit findet kein Event statt +EVENT_COMING=§eKommende Events§8: +EVENT_COMING_EVENT=§7{0}§8-§7{1}§8: §e{2} +EVENT_COMING_DEADLINE=§7 Anmeldeschluss§8: §7{0} +EVENT_COMING_SCHEM_DEADLINE=§7 Einsendeschluss§8: §7{0} +EVENT_COMING_TEAMS=§7 Mit§8: {0} +EVENT_COMING_TEAM= §{0}{1} +EVENT_CURRENT_EVENT=§e§l{0} +EVENT_CURRENT_FIGHT=§7{0} §{1}{2}§8 vs §{3}{4} +EVENT_CURRENT_FIGHT_WIN=§8: §7Sieg §{0}{1} +EVENT_CURRENT_FIGHT_DRAW=§8: §7Unentschieden + +#EventRescheduleCommand +EVENTRESCHEDULE_USAGE=§8/§7eventreschedule §8[§eTeam1§8] [§eTeam2§8] +EVENTRESCHEDULE_UNKNOWN_TEAM=§cEin Team ist unbekannt / Derzeit kein Event +EVENTRESCHEDULE_NO_FIGHT="§cKein Kampf zwischen den Teams gefunden" +EVENTRESCHEDULE_STARTING=§aKampf startet in 30s + +#FightCommand +FIGHT_UNKNOWN_GAMEMODE=§cUnbekannter Spielmodus: {0} +FIGHT_UNKNOWN_ARENA=§cDie gewünschte Arena gibt es nicht. +FIGHT_IN_ARENA=§cDu befindest dich bereits in einer Arena. +FIGHT_BROADCAST=§7Klicke §ehier§7, um §e{0} §7gegen §e{1} §7zu §7kämpfen! +FIGHT_BROADCAST_HOVER=§aGegen §7{1} §ekämpfen + +#CheckCommand +CHECK_REMINDER=§7Es sind §e{0} §7Schematics zu prüfen§8! +CHECK_REMINDER_HOVER=§eZu prüfende Schematics +CHECK_NOT_CHECKING=§cDu prüfst derzeit nicht. +CHECK_HELP_LIST=§8/§7check list §8- §7Zeigt die Liste der ungeprüften Schematics +CHECK_HELP_NEXT=§8/§7check next §8- §7Nächste Prüffrage§8/§7freigeben +CHECK_HELP_DECLINE=§8/§7check decline §8[§eGrund§8] - §7Schematic ablehnen +CHECK_HELP_CANCEL=§8/§7check cancel §8- §7Bricht den Prüfvorgang ab +CHECK_LIST_HEADER=§e§lZu prüfen sind {0} Schematics +CHECK_LIST_TO_CHECK={0} §8{1} §7{2} §e{3} +CHECK_LIST_TO_CHECK_HOVER=§eSchematic prüfen +CHECK_LIST_CHECKING={0} §8{1} §7{2} §e{3} §7wird geprüft von §e{4} +CHECK_LIST_CHECKING_HOVER=§eZum Prüfer +CHECK_SCHEMATIC_ALREADY_CHECKING=§cDu prüfst schon eine Schematic! +CHECK_SCHEMATIC_OWN=§cDu kannst nicht deine eigenen Schematics prüfen. +CHECK_SCHEMATIC_OWN_TEAM=§cDu kannst nicht Schematics deines Teams prüfen. +CHECK_SCHEMATIC_PREVIOUS=§7{0} von {1}§8: §e{2} +CHECK_INVALID_RANK=§cUnbekannter Schematicrang. +CHECK_ABORT=§aDer Prüfvorgang wurde abgebrochen! +CHECK_NEXT=Nächste Frage +CHECK_ACCEPT=Annehmen +CHECK_DECLINE=Ablehnen +CHECK_RANK=§aRang {0}: {1} +CHECK_RANK_HOVER=§aMit diesem Rang freigeben +CHECK_ACCEPTED=§aDein §e{0} {1} §ewurde freigegeben§8! +CHECK_ACCEPTED_TEAM=§7Die Schematic §e{0} §7von §e{1} §7ist nun freigegeben! +CHECK_DECLINED=§cDein §e{0} {1} §cwurde abgelehnt§8: §c{2} +CHECK_DECLINED_TEAM=§7Die Schematic §e{0} §7von §e{1} §7wurde aufgrund von §e{2} §7abgelehnt! + +#HistoricCommand +HISTORIC_BROADCAST=§7Historischer §e{0}§8-§7Kampf von §e{1}§8! +HISTORIC_BROADCAST_HOVER=§aGegen §7{1} §ekämpfen + +#JoinCommand +JOIN_PLAYER_BLOCK=§cDu kannst diesem Spieler derzeit nicht folgen. + +#JoinmeCommand +JOINME_USAGE=§8/§7join §8[§eSpieler§8]. +JOINME_BROADCAST=§7Klicke §ehier§8, §7um zu §e{0} §7auf §e{1} §7zu kommen§8! +JOINME_BROADCAST_HOVER=§aSpieler folgen +JOINME_PLAYER_OFFLINE=§cDieser Spieler ist offline. +JOINME_PLAYER_SELF=§cSei eins mit dir selbst! + +#KickCommand +KICK_USAGE=§8/§7kick §8[§eSpieler§8] [§eNachricht§8] +KICK_OFFLINE=§cDieser Spieler ist derzeit nicht online! +KICK_CONFIRM=Der Spieler {0} wurde gekickt. +KICK_NORMAL=§cDu wurdest gekickt. + +#MsgCommand +MSG_USAGE=§8/§7msg §8[§eBenutzer§8] [§eNachricht§8] +MSG_OFFLINE=§cKein Gesprächspartner verfügbar! +MSG_IGNORED=§cDieser Spieler hat dich geblockt! + +#PingCommand +PING_RESPONSE=§7Dein Ping beträgt §c{0}§7 ms! + +#PollCommand +POLL_NO_POLL=§cDerzeit läuft keine Umfrage. +POLL_NO_ANSWER=§cDas ist keine Antwortmöglichkeit! +POLL_ANSWER_REFRESH=§aDeine Antwort wurde aktualisiert. +POLL_ANSWER_NEW=§aDeine Antwort wurde registriert. + +#RCommand +R_USAGE=§8/§7r §8[§eAntwort§8] + +#RegelnCommand +REGELN_RULES=§7§lRegelwerke +REGELN_AS=§eAirShip§8-§7Regelwerk +REGELN_AS_HOVER=§7https://steamwar.de/spielmodi/airship-regelwerk/ +REGELN_AS_URL=https://steamwar.de/spielmodi/airship-regelwerk/ +REGELN_MWG=§eMiniWarGear§8-§7Regelwerk +REGELN_MWG_HOVER=§7https://steamwar.de/spielmodi/miniwargear-regelwerk/ +REGELN_MWG_URL=https://steamwar.de/spielmodi/miniwargear-regelwerk/ +REGELN_WG=§eWarGear§8-§7Regelwerk +REGELN_WG_HOVER=§7https://steamwar.de/spielmodi/wargear-regelwerk/ +REGELN_WG_URL=https://steamwar.de/spielmodi/wargear-regelwerk/ +REGELN_WS=§eWarShip§8-§7Regelwerk +REGELN_WS_HOVER=§7https://steamwar.de/spielmodi/warship-regelwerk/ +REGELN_WS_URL=https://steamwar.de/spielmodi/warship-regelwerk/ +REGELN_QG=§eQuickGear§8-§7Regelwerk +REGELN_QG_HOVER=§7https://steamwar.de/spielmodi/quickgear-regelwerk/ +REGELN_QG_URL=https://steamwar.de/spielmodi/quickgear-regelwerk/ +REGELN_CONDUCT=§eVerhaltensrichtlinien +REGELN_CONDUCT_HOVER=§7https://steamwar.de/verhaltensrichtlinien/ +REGELN_CONDUCT_URL=https://steamwar.de/verhaltensrichtlinien/ + +#ReplayCommand +REPLAY_TITLE=Letzte Kämpfe +REPLAY_UNAVAILABLE=§cReplay nicht möglich + +#TutorialCommand +TUTORIAL_TITLE=Tutorials +TUTORIAL_NAME=§e{0} +TUTORIAL_BY=§8von §7{0} +TUTORIAL_STARS=§e{0} §7Sterne +TUTORIAL_RATE_TITLE=Tutorial bewerten +TUTORIAL_RATE=§e{0} §7Stern(e) +TUTORIAL_DELETE=§cMit Shift+Rechtsklick löschen +TUTORIAL_CREATE_HELP=§8/§7tutorial create §8[§eMaterial§8] §8[§eName§8] +TUTORIAL_CREATE_MISSING=§cEin Tutorial kann nur von einem Tutorialserver aus erstellt werden! +TUTORIAL_CREATED=§7Das Tutorial wurde erstellt§8. +TUTORIAL_OWN_HELP=§8/§7tutorial own §8- §7Liste der eigenen Tutorials + +#ServerTeamchatCommand +STC_USAGE=§8/§7stc §8[§eNachricht an das Team§8] + +#TeamchatCommand +TC_USAGE=§8/§7tc §8[§eNachricht an das Team§8] +TC_NO_TEAM=§cDu bist in keinem Team. + +#TeamCommand +TEAM_IN_TEAM=§cDu bist bereits in einem Team. +TEAM_NOT_IN_TEAM=§cDu bist in keinem Team. +TEAM_NOT_LEADER=§cDu bist nicht der Teamleader. +TEAM_NOT_IN_EVENT=§cDies ist während eines Events nicht möglich. +TEAM_HELP_HEADER=§7Mit §e/team §7verwaltest du dein Team. +TEAM_HELP_LIST=§8/§7team list §8- §7Liste alle Teams auf. +TEAM_HELP_INFO=§8/§7team info §8- §7Informiere dich über ein Team. +TEAM_HELP_TP=§8/§7team tp §8(§7Team§8) §8- §7Teleportiert zum Teamserver. +TEAM_HELP_CREATE=§8/§7team create §8- §7Erstelle dein eigenes Team. +TEAM_HELP_JOIN=§8/§7team join §8- §7Trete einem Team bei. +TEAM_HELP_CHAT=§8/§7teamchat §8- §7Sende Nachrichten an dein Team. +TEAM_HELP_EVENT=§8/§7team event §8- §7Nehme an Events teil. +TEAM_HELP_LEAVE=§8/§7team leave §8- §7Verlasse dein Team. +TEAM_HELP_INVITE=§8/§7team invite §8- §7Lade jemanden in dein Team ein. +TEAM_HELP_REMOVE=§8/§7team remove §8- §7Entferne jemanden aus deinem Team. +TEAM_HELP_KUERZEL=§8/§7team changekuerzel §8- §7Ändere dein Teamkürzel. +TEAM_HELP_NAME=§8/§7team changename §8- §7Ändere deinen Teamnamen. +TEAM_HELP_COLOR=§8/§7team changecolor §8- §7Ändere deine Teamfarbe. +TEAM_HELP_LEADER=§8/§7team promote §8- §7Ernenne jemanden zum Teamleader. +TEAM_HELP_STEP_BACK=§8/§7team stepback §8- §7Tritt als Leader zurück. +TEAM_HELP_SERVER=§8/§7team server §8[§eIP/Adresse§8] §8(§7Port§8) §8- §7Setzt Adresse des Teamservers. + +#Team Create +TEAM_CREATE_USAGE=§8/§7team create §8[§eTeamkürzel§8] §8[§eTeamname§8] +TEAM_CREATE_CREATED=§7Du hast das Team §e{0} §7gegründet! + +#Team Join +TEAM_JOIN_NO_INVITE=§7Du hast keine Einladungen erhalten. +TEAM_JOIN_USAGE=§8/§7team join §8[§eTeam§8] +TEAM_JOIN_INVITED=§7Du wurdest von diesen Teams eingeladen§8: §e{0} +TEAM_JOIN_NOT_BY_TEAM=§cVon diesem Team wurdest du nicht eingeladen. +TEAM_JOIN_JOINED=§7Du bist dem Team §e{0} §7beigetreten! + +#Team Leave +TEAM_OTHER_LEADER_REQUIRED=§cBitte ernenne zunächst ein anderes Teammitglied zum Leader! +TEAM_LEAVE_LEFT=§7Du hast dein Team verlassen! + +#Team Step Back +TEAM_STEP_BACK=§7Du hast deinen Posten als Teamleader abgegeben§8! + +#Team Invite +TEAM_INVITE_USAGE=§8/§7team invite §8[§eSpieler§8] +TEAM_INVITE_NO_PLAYER=§cDiesen Spieler gibt es nicht. +TEAM_INVITE_IN_TEAM=§cDieser Spieler ist bereits in einem Team. +TEAM_INVITE_ALREADY_INVITED=§cDu hast diesen Spieler bereits eingeladen. +TEAM_INVITE_INVITED=§7Du hast §e{0} §7in das Team eingeladen! +TEAM_INVITE_INVITED_TARGET=§7Du wurdest in das Team §{0}{1} §7eingeladen! + +#Team Remove +TEAM_REMOVE_USAGE=§8/§7team remove §8[§eSpieler§8] +TEAM_REMOVE_NOT_PLAYER=§cDiesen Spieler gibt es nicht. +TEAM_REMOVE_NOT_LEADER=§cLeader können nicht rausgeworfen werden. +TEAM_REMOVE_INVITE=§7Die Einladung wurde zurückgezogen. +TEAM_REMOVE_NO_INVITE=§cDieser Spieler hat keine Einladung erhalten. +TEAM_REMOVE_NOT_IN_TEAM=§cDieser Spieler ist nicht in deinem Team. +TEAM_REMOVE_REMOVED=§7Der Spieler wurde aus dem Team entfernt. +TEAM_REMOVE_REMOVED_TARGET=§cDu wurdest aus dem Team entfernt. + +#Team Kuerzel +TEAM_KUERZEL_USAGE=§8/§7team changekuerzel §8[§eTeamkürzel§8] +TEAM_KUERZEL_CHANGED=§7Du hast das Kürzel des Teams geändert! +TEAM_KUERZEL_LENGTH=§cEin Teamkürzel muss aus 2 bis 4 Buchstaben bestehen. +TEAM_KUERZEL_TAKEN=§cEs gibt bereits ein Team mit diesem Namen. + +#Team Name +TEAM_NAME_USAGE=§8/§7team changename §8[§eTeamname§8] +TEAM_NAME_CHANGED=§7Du hast das Team umbenannt! +TEAM_NAME_LENGTH=§cEin Teamname muss aus 4 bis 15 Buchstaben bestehen. +TEAM_NAME_TAKEN=§cEs gibt bereits ein Team mit diesem Namen. + +#Team Leader +TEAM_LEADER_USAGE=§8/§7team promote §8[§eMember§8] +TEAM_LEADER_NOT_USER=§cUnbekannter Spieler. +TEAM_LEADER_NOT_MEMBER=§cDer Spieler ist nicht in deinem Team. +TEAM_LEADER_PROMOTED=§7Du hast den Spieler §e{0} §7zum Leader gemacht! + +#Team Info +TEAM_INFO_USAGE=§8/§7team info §8[§eTeamname§8] +TEAM_INFO_TEAM=§7Team §e{0} §8[§{1}{2}§8] +TEAM_INFO_LEADER=§7Leader ({0})§8: {1} +TEAM_INFO_MEMBER=§7Member ({0})§8: {1} +TEAM_INFO_EVENTS=§7Events§8: §e{0} + +#Team List +TEAM_LIST_NOT_PAGE=§cKeine Seitenzahl angegeben +TEAM_LIST_UNKNOWN_PAGE=§cUngültige Seitenzahl angegeben +TEAM_LIST_HEADER=§7§lTeamliste §7{0}§8/§7{1} +TEAM_LIST_TEAM=§{0}{1} §e{2} +TEAM_LIST_TEAM_HOVER=§7Teaminfo +TEAM_LIST_PAGE=Seite »» +TEAM_LIST_NEXT=§eNächste Seite +TEAM_LIST_PREV=§eVorherige Seite + +#Team Event +TEAM_EVENT_USAGE=§8/§7team event §8[§eEvent§8] - §7um daran teilzunehmen +TEAM_EVENT_HEADER=§7Dein Team nimmt an folgenden Events teil§8: +TEAM_EVENT_EVENT=§7{0}§8: §e{1} +TEAM_EVENT_NO_EVENT=§cDieses Event gibt es nicht +TEAM_EVENT_OVER=§cDie Anmeldephase für dieses Event ist bereits vorbei +TEAM_EVENT_LEFT=§7Dein Team nimmt nicht mehr am Event teil +TEAM_EVENT_JOINED=§7Dein Team nimmt nun am Event §e{0} §7 teil! +TEAM_EVENT_HOW_TO_LEAVE=§7Um die Teilnahme abzusagen, wiederhole den Befehl. + +#Team Color +TEAM_COLOR_TITLE=Farbe wählen + +#Team Server +TEAM_SERVER_USAGE=§8/§7team server §8[§eIP/Adresse§8] §8(§7Port§8) §8- §7Setzt Adresse des Teamservers. +TEAM_SERVER_SET=§7Du hast die Teamserveradresse geändert§8! +TEAM_SERVER_PORT_INVALID=§cUnmögliche Portnummer. +TEAM_SERVER_ADDRESS_INVALID=§cUngültige Adresse. +TEAM_NO_ADDRESS=§cTeamserveradresse nicht gesetzt. +TEAM_OFFLINE=§cTeamserver scheint offline zu sein. +TEAM_UNKNOWN=§cEin unwerwarteter Fehler ist aufgetreten beim verbinden zum Teamserver. +TEAM_TP_NO_TEAM=§cUnbekanntes Team. + +#TpCommand +TP_USAGE=§8/§7tp §8[§eSpieler§8] +TP_USAGE_EVENT=§8/§7tp §8[§eSpieler §7oder §eTeam§8] + +#UnignoreCommand +UNIGNORE_USAGE=§8/§7unignore §8[§eSpieler§8] +UNIGNORE_NOT_PLAYER=§cDiesen Spieler gibt es nicht! +UNIGNORE_NOT_IGNORED=§cDu ignorierst diesen Spieler nicht. +UNIGNORE_UNIGNORED=§7Du empfängst nun wieder Nachrichten von §e{0}§8. + +#WebregisterCommand +WEB_USAGE=§8/§7webpassword §8[§ePasswort§8] +WEB_UPDATED=§7Dein Passwort wurde aktualisiert. +WEB_CREATED=§7Dein Webaccount wurde erstellt. +WEB_PASSWORD_LENGTH=§cDein Passwort ist kürzer als 8 Zeichen. + +#ChatListener +CHAT_LIXFEL_ACTION_BAR=§4§lTechnische Probleme? +CHAT_LIXFEL_1=Du hast mich gerufen! +CHAT_LIXFEL_2=Leider bin ich nur ein Mensch und höre nicht alles. +CHAT_LIXFEL_3=Daher bitte ich dich, das Problem bzw. den Fehler im Forum in der Kategorie §eFehler melden §7mit einer ausreichenden Beschreibung zu hinterlegen. +CHAT_LIXFEL_4=Vielen Dank. +CHAT_LIXFEL_5=Ich wünsche dir noch weiterhin ein reibungsloses Spielerlebnis. +CHAT_YOYONOW_1=Du hast mich gerufen! +CHAT_YOYONOW_2=Ich würde dir gerne den Befehl "/bug " ans Herz legen. +CHAT_YOYONOW_3=Vielen Dank. +CHAT_YOYONOW_4=Ich wünsche dir noch weiterhin ein reibungsloses Spielerlebnis. +CHAT_CHAOSCAOT_1=Du hast mich gerufen! +CHAT_CHAOSCAOT_2=Wenn etwas nicht funktioniert, dann nen es einfach ein Feature. +CHAT_CHAOSCAOT_3=Und wenn es ein Feature ist, dann kann es nicht kaputt. +CHAT_CHAOSCAOT_4=Kaputt ist nur eine Definition. Wenn du es als Feature definiert, dann kann es nicht kaputt sein. +CHAT_CHAOSCAOT_5=Und wenn du es als kaputt definiert, dann sag uns bescheid mit dem Befehl "/bug ". +CHAT_CHAOSCAOT_6=Vielen Dank. +CHAT_RECEIVE=§cUm Chatnachrichten versenden zu können, musst du auch welche empfangen! +CHAT_NO_LINKS=§cDu darfst keine Links versenden. +CHAT_BC_USAGE=§8/§7bc §8[§eNachricht§8] +CHAT_NO_RECEIVER=§cNiemand empfängt deine Nachricht +CHAT_EMPTY=§cSchreibe keine inhaltslosen Nachrichten. + +CHAT_SERVERTEAM=§8STC §e{0}§8» §f{2} +CHAT_GLOBAL={3}{4}{5}{6}{0}§8» {7}{2} +CHAT_DISCORD_GLOBAL=§8Dc {5}{6}{0}§8» {7}{2} +CHAT_TEAM=§8TC §e{0}§8» §f{2} +CHAT_MSG=§e{0}§8»§e{1} §7{2} + +#CheckListner +CHECK_UNCHECKED=§7Du hast noch §e{0} §7ungeprüfte Schematic§8(§7s§8)! +CHECK_CHECKING=§cDu prüfst gerade eine Schematic! + +#ConnectionListener +JOIN_ARENA=§7Klicke §ehier§7, um §e{0} §7beizutreten +JOIN_ARENA_HOVER=§eArena beitreten +JOIN_FIRST=§7Begrüßt alle mal §e{0} §7auf dem Server§8! + +#EventModeListener +EVENTMODE_KICK=§cDu bist kein Eventteilnehmer. + +#PollSystem +POLL_HEADER=§e§lUmfrage +POLL_HEADER2=§7Klicke die Antwort an, die dir gefällt! +POLL_QUESTION=§e{0} +POLL_ANSWER=§7{0} +POLL_ANSWER_HOVER=§e{0} §ewählen + +#TablistManager +TABLIST_PHASE_WEBSITE=§8Website: https://§eSteam§8War.de +TABLIST_PHASE_DISCORD=§8Discord: https://§eSteam§8War.de/discord +TABLIST_FOOTER=§e{0} {1}§8ms §eSpieler§8: §7{2} +TABLIST_BAU=§7§lBau +LIST_COMMAND=§e{0}§8: §7{1} + +#EventStarter +EVENT_FIGHT_BROADCAST=§7Hier §eklicken §7für den Kampf §{0}{1} §8vs §{2}{3} +EVENT_FIGHT_BROADCAST_HOVER=§eEvent beitreten + +#SubserverSystem +SERVER_IGNORED=§cDieser Spieler hat dich geblockt! +SERVER_ADD_MEMBER=§e{0} §7möchte auf deine Bauwelt. +SERVER_ADD_MESSAGE=§7Klicke §ehier§7, wenn du das erlauben möchtest. +SERVER_ADD_MESSAGE_HOVER=§8/§7bau addmember §e{0} +SERVER_WORLD_ERROR=§cDas Erstellen der Welt ist fehlgeschlagen. + + +#WhoisCommand +WHOIS_USAGE=§c/whois [Spieler/ID] [-a/-m] +WHOIS_USERNAME=§7Username§8: §e{0} +WHOIS_PREFIX=§7Chat-Präfix§8: {0} +WHOIS_PERMS=§7Rechte§8: §7{0} +WHOIS_UUID=§7UUID§8: §e{0} +WHOIS_UUID_HOVER=§eUUID Kopieren +WHOIS_ID=§7ID§8: §e{0} +WHOIS_DISCORD_ID=§7Discord-ID§8: §e{0} +WHOIS_JOINED_FIRST=§7Beigetreten am§8: §e{0} +WHOIS_HOURS_PLAYED=§7Spielzeit§8: §e{0}h +WHOIS_CURRENT_PLAYED=§7Aktuell online§8: §e{0}m +WHOIS_CURRENT_SERVER=§7Aktueller Server§8: §e{0} +WHOIS_CURRENT_PROTOCOL=§7Aktuelle Version§8: §e{0} +WHOIS_TEAM=§7Team§8: §e[§{0}{1}§e] {2} +WHOIS_TEAM_HOVER=§e{0} anzeigen +WHOIS_PUNISHMENTS=§7Strafen: +WHOIS_PUNISHMENT=§7{0}§8» §f§l{1}: §e{2} - {3} §f{4} +WHOIS_NO_PUNISHMENT=§a✓ §7Der Spieler hat keine aktiven Strafen. +WHOIS_NO_ALL_PUNISHMENT=§a✓ §7Der Spieler hat noch nichts getan. +WHOIS_ACTIVE_MODS=§7Aktive Mods ({0}): {1} +WHOIS_NO_ACTIVE_MODS=§7Der Spieler hat keine aktiven Mods. + +#VerifyCommand +VERIFY_USAGE=§c/verify [Code] +VERIFY_INVALID=§cInvalider Code +VERIFY_SUCCESS=§7Erfolgreich mit dem Discord Account §e{0} §7verknüpft + +#Discord +DISCORD_TICKET_HOVER=§eZur Nachricht +DISCORD_TICKET_MESSAGE=§7Ticket §e{0}§7» §f§l{1}: §7{2} +DISCORD_TICKET_NEW=§7Ticket §e{0}§7» §aTicket wurde geöffnet! +DISCORD_TICKET_CLOSED=§7Ticket §e{0}§7» §cTicket wurde geschlossen! + +#GDPR Query +GDPR_STATUS_WEBSITE=§7Website kann nicht automatisiert gepackt werden und muss daher manuell hinzugefügt werden. +GDPR_STATUS_WORLD=§7Packe Bauwelten... +GDPR_STATUS_INVENTORIES=§7Suche und packe Inventare... +GDPR_STATUS_DATABASE=§7Packe Datenbankinhalte... +GDPR_STATUS_LOGS=§7Suche und packe logs... +GDPR_STATUS_FINISHED=§7Packen abgeschlossen + +#Playtime Command +HOURS_PLAYED=§7Deine Spielzeit beträgt§8: §e{0}h + +#Arena command +ARENA_NOT_FOUND=§cDie angegebene Arena konnte nicht gefunden werden + +#Rank +RANK_PLAYER_NOT_FOUND=§cSpieler nicht gefunden +RANK_PLAYER_FOUND=§eRang §7von §e{0} +RANK_HEADER=§e{0} {1} +RANK_UNPLACED=§7unplatziert +RANK_PLACED=§e{0}§8. §7mit §e{1} §7Elo§8. +RANK_EMBLEM=§7Emblem§8: {0} + +#Fabric Mod Sender +MODIFICATION_BAN_MESSAGE=Du hast probiert den FabricModSender zu umgehen / zu modifizieren! +MODIFICATION_BAN_LOG={0} hat probiert den Fabric Mod Sender zu editieren / umzugehen! Grund: {1} + +#Arena Merging +FIGHT_MERGE_TITLE=Gleicher Kampf gefunden! +FIGHT_MERGE_DECLINE=§cNeue Arena starten +FIGHT_MERGE_ACCEPT=§aKampf beitreten +FIGHT_MERGE_INFO_LORE_1=§8Von: §e{0} +FIGHT_MERGE_OFFLINE=§7Die vorgeschlagene Arena wurde in der Zwischenzeit beendet, es wird eine neue Arena gestartet. + +#Locale Locking +LOCK_LOCALE_CHANGED=§aSprache gespeichert + +#Builder Cloud +BUILDERCLOUD_USAGE=§8/§7buildercloud §8[§eVersion§8] §8[§eWelt§8] +BUILDERCLOUD_CREATE_USAGE=§8/§7buildercloud create §8[§eVersion§8] §8[§eWelt§8] §8<§7Generator§8> +BUILDERCLOUD_RENAME_USAGE=§8/§7buildercloud rename §8[§eVersion§8] §8[§eWElt§8] §8[§eNeuer Name§8] +BUILDERCLOUD_VERSION=§cUnbekannte Version. +BUILDERCLOUD_EXISTING_MAP=§cWelt existiert bereits. +BUILDERCLOUD_UNKNOWN_MAP=§cUnbekannte Welt. +BUILDERCLOUD_RENAMED=§7Umbenennung erfolgreich. + +# Advent Calendar +ADVENT_CALENDAR_TITLE=§eAdventskalender +ADVENT_CALENDAR_DAY=§7Tag§8: §e{0} +ADVENT_CALENDAR_MESSAGE=§eHast du heute schon dein Geschenk geholt? +ADVENT_CALENDAR_MESSAGE_HOVER=§eKlicken zum öffnen! +ADVENT_CALENDAR_OPEN=§7Du hast §e{0}§7 aus dem Adventskalender erhalten! + +#StreamInv +INV_PAGE_BACK=§{0}Page back +INV_PAGE_NEXT=§{0}Next page + +#Discord +DC_UNLINKED=Für diese Aktion muss dein Minecraftaccount mit deinem Discordaccount verknüpft sein, gehe dazu auf dem SteamWar-Discord in den `regeln-infos` Channel und Klicke auf `Minecraft Verknüpfen`. +DC_TITLE_SCHEMINFO=Schematicinfo +DC_SCHEM_ACCEPT=Deine Schematic **{0}** wurde angenommen. +DC_SCHEM_DECLINE=Deine Schematic **{0}** wurde abgelehnt. **Grund:** \n{1} +DC_AUTH_SUCCESS=:white_check_mark: Dein Discordaccount wurde mit **{0}** verknüpft. +DC_ROLE_ADDED=:tada: Du bekommst nun {0}. +DC_ROLE_REMOVED=Du bekommst nun keine {0} mehr. + +DC_TICKET_CREATED=Dein Ticket {0} wurde erstellt. +DC_TICKETINTRO_REPORT=Bitte beantworte für die Ahndung des Fehlverhaltens möglichst genau folgende Fragen und füge nach Möglichkeit Beweismaterial hinzu:\n - Welche(r) Spieler?\n - Auf welchem Server?\n - Zu welchem Zeitpunkt?\n - Art und Natur des Fehlverhaltens? +DC_TICKETINTRO_IDEA=Beschreibe deine Idee möglichst detailiert. Hierzu gehört: Was, Warum, Wie, Wo? +DC_TICKETINTRO_BUG=Bitte beschreibe das beobachtete unerwartete bzw. inkorrekte Verhalten der Serversoftware. Falls notwendig, beschreibe die Schritte, mit denen der Fehler reproduziert werden kann. +DC_TICKETINTRO_QUESTION=Bitte stelle deine Frage, ein Serverteammitglied wird sich dieser zeitnah annehmen. +DC_TICKETINTRO_APPEAL=Fragen wirkt Wunder! +DC_TICKET_CLOSE=Schließen + +DC_SCHEMUPLOAD_NOPERM=Du darfst keine Schematics hochladen. +DC_SCHEMUPLOAD_IGNORED=`{0}` wird ignoriert, da die Datei keine Schematic ist. +DC_SCHEMUPLOAD_INVCHAR=`{0}` hat unzulässige Buchstaben im Namen. +DC_SCHEMUPLOAD_SUCCESS=`{0}` wurde erfolgreich hochgeladen. +DC_SCHEMUPLOAD_ERROR=Ein Fehler ist beim Hochladen von `{0}` aufgetreten. Für nähere Informationen wende dich an einen Developer. diff --git a/VelocityCore/src/de/steamwar/messages/Chatter.java b/VelocityCore/src/de/steamwar/messages/Chatter.java new file mode 100644 index 00000000..c6571303 --- /dev/null +++ b/VelocityCore/src/de/steamwar/messages/Chatter.java @@ -0,0 +1,276 @@ +/* + 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.messages; + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.event.connection.LoginEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; + +import java.text.DateFormat; +import java.text.MessageFormat; +import java.util.Date; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public interface Chatter { + + static Stream allPlayers() { + return VelocityCore.getProxy().getAllPlayers().stream(); + } + + static Stream allStream() { + return Stream.concat(Stream.of(Chatter.console()), allPlayers().map(Chatter::of)); + } + + static ChatterGroup broadcast() { + return new ChatterGroup(allStream()); + } + + static ChatterGroup globalChat() { + return new ChatterGroup(Stream.concat(Stream.of(Chatter.console()), allPlayers().filter(player -> { + Subserver subserver = Subserver.getSubserver(player); + return subserver == null || !(subserver.getType() == Servertype.ARENA && subserver.getServer() == player.getCurrentServer().map(ServerConnection::getServerInfo).orElse(null)); + }).map(Chatter::of))); + } + + static ChatterGroup serverteam() { + return new ChatterGroup(allStream().filter(player -> player.user().hasPerm(UserPerm.TEAM))); + } + + SteamwarUser user(); + + Locale getLocale(); + boolean chatShown(); + void sendMessage(Component msg); + Player getPlayer(); + + default T withPlayerOrOffline(Function withPlayer, Supplier withOffline) { + Player player = getPlayer(); + if(player == null) + return withOffline.get(); + else + return withPlayer.apply(player); + } + default void withPlayerOrOffline(Consumer withPlayer, Runnable withOffline) { + Player player = getPlayer(); + if(player == null) + withOffline.run(); + else + withPlayer.accept(player); + } + default void withPlayer(Consumer function) { + withPlayerOrOffline(function, () -> {}); + } + + default void system(String format, Object... params) { + system(new Message(format, params)); + } + + default void system(Message message) { + send(true, null, null, message); + } + + default void system(String format, Message onHover, ClickEvent onClick, Object... params) { + send(true, onHover, onClick, new Message(format, params)); + } + + default void prefixless(String format, Object... params) { + prefixless(format, null, null, params); + } + + default void prefixless(String format, Message onHover, ClickEvent onClick, Object... params) { + send(false, onHover, onClick, new Message(format, params)); + } + + default void send(boolean prefixed, Message onHover, ClickEvent onClick, Message message) { + Component msg = parse(prefixed, message); + if(onHover != null) + msg = msg.hoverEvent(HoverEvent.showText(parse(false, onHover))); + if(onClick != null) + msg = msg.clickEvent(onClick); + sendMessage(msg); + } + + default String parseToPlain(String format, Object... params) { + return parseToPlain(new Message(format, params)); + } + + default String parseToPlain(Message message) { + return PlainTextComponentSerializer.plainText().serialize(parse(message)); + } + + default String parseToLegacy(String format, Object... params) { + return parseToLegacy(new Message(format, params)); + } + + default String parseToLegacy(Message message) { + return LegacyComponentSerializer.legacySection().serialize(parse(message)); + } + + default Component parse(String format, Object... params) { + return parse(false, new Message(format, params)); + } + + default Component parse(Message message) { + return parse(false, message); + } + + default Component parse(boolean prefixed, String format, Object... params) { + return parse(prefixed, new Message(format, params)); + } + + default Component parse(boolean prefixed, Message message) { + Locale locale = getLocale(); + ResourceBundle resourceBundle = SteamwarResourceBundle.getResourceBundle(locale); + String pattern = ""; + if(prefixed) + pattern = resourceBundle.getObject("PREFIX") + " "; + pattern += (String)resourceBundle.getObject(message.format()); + + MessageFormat format = new MessageFormat(pattern, locale); + Object[] params = message.params(); + for (int i = 0; i < params.length; i++) { + if(params[i] instanceof Message msg) { + params[i] = parseToLegacy(msg); + } else if(params[i] instanceof Date date) { + params[i] = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale).format(date); + } else if(params[i] instanceof SteamwarUser user) { + params[i] = user.getUserName(); + } else if(params[i] instanceof Player player) { + params[i] = player.getUsername(); + } else if(params[i] instanceof Chatter chatter) { + params[i] = chatter.user().getUserName(); + } else if(params[i] instanceof Function func) { + params[i] = func.apply(this); + } + } + return LegacyComponentSerializer.legacySection().deserialize(format.format(params)); + } + + static PlayerChatter of(Player player) { + return new PlayerChatter(player, player::sendMessage); + } + + static PlayerChatter disconnect(Player player) { + return new PlayerChatter(player, player::disconnect); + } + + static Chatter of(CommandSource sender) { + if(sender instanceof Player player) + return of(player); + + //Console + return new PlayerlessChatter() { + @Override + public SteamwarUser user() { + return SteamwarUser.get(-1); + } + + @Override + public boolean chatShown() { + return true; + } + + @Override + public void sendMessage(Component msg) { + sender.sendMessage(msg); + } + }; + } + + static Chatter of(SteamwarUser user) { + return of(user.getUUID()); + } + + static Chatter of(UUID uuid) { + Player player = VelocityCore.getProxy().getPlayer(uuid).orElse(null); + if(player != null) + return Chatter.of(player); + + return new PlayerlessChatter() { + @Override + public SteamwarUser user() { + return SteamwarUser.get(uuid); + } + + @Override + public boolean chatShown() { + return false; + } + + @Override + public void sendMessage(Component msg) { + // Nowhere to send + } + }; + } + + static Chatter console() { + return of(VelocityCore.getProxy().getConsoleCommandSource()); + } + + static Chatter of(LoginEvent event) { + return new PlayerlessChatter() { + @Override + public SteamwarUser user() { + return SteamwarUser.get(event.getPlayer().getUniqueId()); + } + + @Override + public boolean chatShown() { + return false; + } + + @Override + public void sendMessage(Component msg) { + event.setResult(ResultedEvent.ComponentResult.denied(msg)); + } + }; + } + + abstract class PlayerlessChatter implements Chatter { + @Override + public Locale getLocale() { + return user().getLocale(); + } + + @Override + public Player getPlayer() { + return VelocityCore.getProxy().getPlayer(user().getUUID()).orElse(null); + } + } +} diff --git a/VelocityCore/src/de/steamwar/messages/ChatterGroup.java b/VelocityCore/src/de/steamwar/messages/ChatterGroup.java new file mode 100644 index 00000000..c94975d6 --- /dev/null +++ b/VelocityCore/src/de/steamwar/messages/ChatterGroup.java @@ -0,0 +1,63 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.messages; + +import de.steamwar.sql.SteamwarUser; +import lombok.Getter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; + +import java.util.Arrays; +import java.util.stream.Stream; + +@Getter +public class ChatterGroup extends Chatter.PlayerlessChatter { + + final Chatter[] chatters; + + public ChatterGroup(Stream stream) { + chatters = stream.toArray(Chatter[]::new); + } + + public ChatterGroup(Chatter... chatters) { + this.chatters = chatters; + } + + @Override + public boolean chatShown() { + return Arrays.stream(chatters).allMatch(Chatter::chatShown); + } + + @Override + public void send(boolean prefixed, Message onHover, ClickEvent onClick, Message message) { + for(Chatter sender : chatters) + sender.send(prefixed, onHover, onClick, message); + } + + @Override + public SteamwarUser user() { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMessage(Component msg) { + throw new UnsupportedOperationException(); + } +} diff --git a/VelocityCore/src/de/steamwar/messages/Message.java b/VelocityCore/src/de/steamwar/messages/Message.java new file mode 100644 index 00000000..3394c274 --- /dev/null +++ b/VelocityCore/src/de/steamwar/messages/Message.java @@ -0,0 +1,22 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.messages; + +public record Message(String format, Object... params) { } \ No newline at end of file diff --git a/VelocityCore/src/de/steamwar/messages/PlayerChatter.java b/VelocityCore/src/de/steamwar/messages/PlayerChatter.java new file mode 100644 index 00000000..6febd74f --- /dev/null +++ b/VelocityCore/src/de/steamwar/messages/PlayerChatter.java @@ -0,0 +1,64 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.messages; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.player.PlayerSettings; +import de.steamwar.sql.SteamwarUser; +import lombok.AllArgsConstructor; +import net.kyori.adventure.text.Component; + +import java.util.Locale; +import java.util.function.Consumer; + +@AllArgsConstructor +public class PlayerChatter implements Chatter { + + private final Player player; + private final Consumer sendMessage; + + @Override + public SteamwarUser user() { + return SteamwarUser.get(player.getUniqueId()); + } + + @Override + public Locale getLocale() { + return user().getLocale(); + } + + @Override + public boolean chatShown() { + if(!player.hasSentPlayerSettings()) + return false; + + return player.getPlayerSettings().getChatMode() == PlayerSettings.ChatMode.SHOWN; + } + + @Override + public void sendMessage(Component msg) { + sendMessage.accept(msg); + } + + @Override + public Player getPlayer() { + return player; + } +} diff --git a/VelocityCore/src/de/steamwar/messages/SteamwarResourceBundle.java b/VelocityCore/src/de/steamwar/messages/SteamwarResourceBundle.java new file mode 100644 index 00000000..ffb236f4 --- /dev/null +++ b/VelocityCore/src/de/steamwar/messages/SteamwarResourceBundle.java @@ -0,0 +1,53 @@ +/* + 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.messages; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +public class SteamwarResourceBundle extends PropertyResourceBundle { + + private static final String BASE_PATH = "/" + "de.steamwar.messages.BungeeCore".replace('.', '/'); + + private static final Map bundles = new HashMap<>(); + + public static ResourceBundle getResourceBundle(Locale locale) { + return getResourceBundle(locale.toString(), getResourceBundle(locale.getLanguage(), getResourceBundle( "", null))); + } + + private static synchronized ResourceBundle getResourceBundle(String locale, ResourceBundle parent) { + return bundles.computeIfAbsent(locale, locale1 -> { + InputStream inputStream = Message.class.getResourceAsStream(BASE_PATH + ("".equals(locale) ? "" : "_" + locale) + ".properties"); + if(inputStream == null) + return parent; + try { + return new SteamwarResourceBundle(inputStream, parent); + } catch (IOException e) { + return parent; + } + }); + } + + private SteamwarResourceBundle(InputStream stream, ResourceBundle parent) throws IOException { + super(stream); + setParent(parent); + } +} diff --git a/VelocityCore/src/de/steamwar/sql/SQLConfigImpl.java b/VelocityCore/src/de/steamwar/sql/SQLConfigImpl.java new file mode 100644 index 00000000..199afe9f --- /dev/null +++ b/VelocityCore/src/de/steamwar/sql/SQLConfigImpl.java @@ -0,0 +1,37 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 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.sql; + +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.sql.internal.SQLConfig; + +import java.util.logging.Logger; + +public class SQLConfigImpl implements SQLConfig { + @Override + public Logger getLogger() { + return VelocityCore.getLogger(); + } + + @Override + public int maxConnections() { + return 4; + } +} diff --git a/VelocityCore/src/de/steamwar/sql/SQLWrapperImpl.java b/VelocityCore/src/de/steamwar/sql/SQLWrapperImpl.java new file mode 100644 index 00000000..1ac26bd8 --- /dev/null +++ b/VelocityCore/src/de/steamwar/sql/SQLWrapperImpl.java @@ -0,0 +1,83 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 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.sql; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.velocitycore.GameModeConfig; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.commands.CheckCommand; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Map; + +public class SQLWrapperImpl implements SQLWrapper { + private static final SimpleDateFormat deadlineFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm"); + private static Date parseDeadline(String deadline) { + if(deadline == null) + return null; + + try { + return deadlineFormat.parse(deadline); + } catch (ParseException e) { + throw new SecurityException(e.getMessage(), e); + } + } + + @Override + public void loadSchemTypes(List tmpTypes, Map tmpFromDB) { + GameModeConfig.loadAll(GameModeConfig.class, (file, config) -> { + if(config.getSchematic() == null || tmpFromDB.containsKey(config.getSchemType().toLowerCase())) + return; + + String shortcut = config.getSchematic().getShortcut(); + String material = config.getSchematic().getMaterial(); + + SchematicType checktype = null; + if(!config.getCheckQuestions().isEmpty()) { + checktype = new SchematicType("C" + config.getSchemType(), "C" + shortcut, SchematicType.Type.CHECK_TYPE, null, material, true); + tmpTypes.add(checktype); + tmpFromDB.put(checktype.toDB(), checktype); + CheckCommand.setCheckQuestions(checktype, config.getCheckQuestions()); + } + + SchematicType current = new SchematicType(config.getSchemType(), shortcut, config.getServer() != null ? SchematicType.Type.FIGHT_TYPE : SchematicType.Type.NORMAL, checktype, material, parseDeadline(config.getDeadline()), config.getSchematic().isManualCheck()); + tmpTypes.add(current); + tmpFromDB.put(config.getSchemType().toLowerCase(), current); + if(checktype != null) + CheckCommand.addFightType(checktype, current); + }); + } + + @Override + public void additionalExceptionMetadata(StringBuilder builder) { + builder.append("\nServers: "); + for(RegisteredServer server : VelocityCore.getProxy().getAllServers()) { + builder.append(server.getServerInfo().getName()).append("("); + for(Player player : server.getPlayersConnected()) { + builder.append(player.getUsername()).append(" "); + } + builder.append(") "); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/ArenaMode.java b/VelocityCore/src/de/steamwar/velocitycore/ArenaMode.java new file mode 100644 index 00000000..a185dcb8 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/ArenaMode.java @@ -0,0 +1,121 @@ +/* + 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.velocitycore; + +import de.steamwar.sql.SchematicType; +import lombok.Getter; + +import java.util.*; + +public class ArenaMode extends GameModeConfig { + + private static final Random random = new Random(); + + private static final Map byChat = new HashMap<>(); + private static final Map byInternal = new HashMap<>(); + private static final Map bySchemType = new HashMap<>(); + @Getter + private static final List allModes = new LinkedList<>(); + + static { + init(); + } + + public static void init() { + byChat.clear(); + byInternal.clear(); + bySchemType.clear(); + allModes.clear(); + + GameModeConfig.loadAll(ArenaMode.class, (file, mode) -> { + if(mode.getServer() == null) + return; + + mode.config = file.getName(); + mode.internalName = file.getName().replace(".yml", ""); + mode.getMaps().forEach(map -> mode.lowerToRealMapNames.put(map.toLowerCase(), map)); + if(mode.getGameName() == null) + mode.setGameName(mode.internalName); + + allModes.add(mode); + byInternal.put(mode.internalName, mode); + for(String name : mode.getServer().getChatNames()){ + byChat.put(name.toLowerCase(), mode); + } + + if(mode.getSchematic() != null && mode.getSchemType() != null) + bySchemType.put(SchematicType.fromDB(mode.getSchemType()), mode); + }); + } + + public static ArenaMode getByChat(String name){ + return byChat.get(name.toLowerCase()); + } + + public static ArenaMode getByInternal(String name){ + return byInternal.get(name); + } + + public static List getAllChatNames(boolean historic) { + List chatNames = new LinkedList<>(); + for(ArenaMode mode : byInternal.values()){ + if(historic == mode.isHistoric()) + chatNames.addAll(mode.getServer().getChatNames()); + } + return chatNames; + } + + public static ArenaMode getBySchemType(SchematicType schemType){ + return bySchemType.get(schemType); + } + + + private final Map lowerToRealMapNames = new HashMap<>(); + + @Getter + private String internalName; + @Getter + private String config; + + public String hasMap(String map){ + for(String m : getMaps()) { + if(m.equalsIgnoreCase(map)) + return m; + } + return null; + } + + public String getRandomMap(){ + return getMaps().get(random.nextInt(getMaps().size())); + } + + public String convertToRealMapName(String map){ + return lowerToRealMapNames.get(map.toLowerCase()); + } + + public String getChatName(){ + return getServer().getChatNames().get(0); + } + + public boolean withoutChatName(){ + return getServer().getChatNames().isEmpty(); + } + +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/Broadcaster.java b/VelocityCore/src/de/steamwar/velocitycore/Broadcaster.java new file mode 100644 index 00000000..f5df6d10 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/Broadcaster.java @@ -0,0 +1,44 @@ +/* + 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.velocitycore; + +import de.steamwar.messages.Chatter; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +class Broadcaster { + + private final List broadcasts = VelocityCore.get().getConfig().getBroadcasts(); + private int lastBroadCast = 0; + + Broadcaster() { + if(!broadcasts.isEmpty()) + VelocityCore.schedule(this::broadcast).repeat(10, TimeUnit.MINUTES).schedule(); + } + + private void broadcast() { + if(!VelocityCore.getProxy().getAllPlayers().isEmpty()) + Chatter.broadcast().system("PLAIN_STRING", broadcasts.get(lastBroadCast++)); + + if(lastBroadCast == broadcasts.size()) + lastBroadCast = 0; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/Config.java b/VelocityCore/src/de/steamwar/velocitycore/Config.java new file mode 100644 index 00000000..40de2791 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/Config.java @@ -0,0 +1,88 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import lombok.Getter; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.TypeDescription; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.introspector.BeanAccess; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +@Getter +public class Config { + + public static T load(Class clazz, File file, Consumer description) { + TypeDescription typeDescription = new TypeDescription(Config.class); + description.accept(typeDescription); + + Constructor constructor = new Constructor(clazz, new LoaderOptions()); + constructor.addTypeDescription(typeDescription); + + Representer representer = new Representer(); + representer.getPropertyUtils().setSkipMissingProperties(true); + + Yaml yaml = new Yaml(constructor, representer); + yaml.setBeanAccess(BeanAccess.FIELD); + + try{ + return yaml.load(new FileInputStream(file)); + }catch(IOException e){ + VelocityCore.getProxy().shutdown(); + throw new SecurityException("Could not load config", e); + } + } + + public static Config load() { + return load(Config.class, new File(VelocityCore.get().getDataDirectory().toFile(), "config.yml"), description -> description.addPropertyParameters("servers", String.class, Config.Server.class)); + } + + private String lobbyserver; + private boolean eventmode = false; + private Map servers = Collections.emptyMap(); + private List broadcasts = Collections.emptyList(); + private Poll poll = null; + + public RegisteredServer lobbyserver() { + return VelocityCore.getProxy().getServer(lobbyserver).orElseThrow(); + } + + @Getter + public static class Poll { + private String question; + private List answers; + } + + @Getter + public static class Server { + private int spectatePort = 0; + private List commands; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/ErrorLogger.java b/VelocityCore/src/de/steamwar/velocitycore/ErrorLogger.java new file mode 100644 index 00000000..714011f2 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/ErrorLogger.java @@ -0,0 +1,83 @@ +/* + 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.velocitycore; + +import de.steamwar.sql.SWException; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.AbstractConfiguration; +import org.apache.logging.log4j.core.config.Property; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.List; + +public class ErrorLogger extends AbstractAppender { + + ErrorLogger() { + super("SW ErrorLogger", null, null, false, Property.EMPTY_ARRAY); + start(); + + LoggerContext context = (LoggerContext) LogManager.getContext(false); + context.getConfiguration().addAppender(this); + context.getRootLogger().addAppender(this); + context.updateLoggers(); + } + + void unregister() { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + ((AbstractConfiguration) context.getConfiguration()).removeAppender(getName()); + context.getRootLogger().removeAppender(this); + context.updateLoggers(); + } + + @Override + public void append(LogEvent event) { + if(event.getLevel().isLessSpecificThan(Level.WARN)) + return; + + String message = event.getMessage().getFormattedMessage(); + for(String reason : filteredMessages) + if(message.contains(reason)) + return; + + ByteArrayOutputStream stacktraceOutput = new ByteArrayOutputStream(); + if(event.getThrown() != null) + event.getThrown().printStackTrace(new PrintStream(stacktraceOutput)); + String stacktrace = stacktraceOutput.toString(); + for(String reason : filteredStacktraces) + if (stacktrace.contains(reason)) + return; + + SWException.log(message, stacktrace); + } + + private static final List filteredMessages = List.of( + "read timed out", + "disconnected while connecting to Lobby" + ); + private static final List filteredStacktraces = List.of( + "ErrorLogger", + "Connection reset by peer" + ); +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java new file mode 100644 index 00000000..1be170f3 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java @@ -0,0 +1,90 @@ +/* + 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.velocitycore; + +import de.steamwar.messages.Chatter; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.EventFight; +import de.steamwar.sql.Team; +import net.kyori.adventure.text.event.ClickEvent; + +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.TimeUnit; + +import static de.steamwar.persistent.Storage.eventServer; + +public class EventStarter { + + private static final Map spectatePorts = new HashMap<>(); + + public static void addSpectateServer(int port, String command) { + spectatePorts.put(port, command); + } + + public EventStarter() { + EventFight.loadAllComingFights(); + VelocityCore.schedule(this::run).delay(10, TimeUnit.SECONDS).schedule(); + } + + public static Map getEventServer() { + return eventServer; + } + + private void run() { + eventServer.entrySet().removeIf(entry -> Subserver.getSubserver(entry.getValue().getServer()) == null); + Queue fights = EventFight.getFights(); + + EventFight next; + while((next = nextFight(fights)) != null){ + Team blue = Team.get(next.getTeamBlue()); + Team red = Team.get(next.getTeamRed()); + + //Don't start EventServer if not the event bungee + String command; + if(VelocityCore.get().getConfig().isEventmode() || next.getSpectatePort() == 0) { + ServerStarter starter = new ServerStarter().event(next); + + starter.callback(subserver -> { + eventServer.put(blue.getTeamId(), subserver); + eventServer.put(red.getTeamId(), subserver); + }).start(); + + command = "/event " + blue.getTeamKuerzel(); + } else { + command = "/" + spectatePorts.get(next.getSpectatePort()); + } + Chatter.broadcast().system("EVENT_FIGHT_BROADCAST", "EVENT_FIGHT_BROADCAST_HOVER", ClickEvent.runCommand(command), blue.getTeamColor(), blue.getTeamName(), red.getTeamColor(), red.getTeamName()); + } + } + + private EventFight nextFight(Queue fights){ + EventFight next = fights.peek(); + if(next == null) + return null; + + if(!next.getStartTime().before(new Timestamp(System.currentTimeMillis()))) + return null; + + return fights.poll(); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/GameModeConfig.java b/VelocityCore/src/de/steamwar/velocitycore/GameModeConfig.java new file mode 100644 index 00000000..8dab4d8d --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/GameModeConfig.java @@ -0,0 +1,92 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore; + +import lombok.Getter; +import lombok.Setter; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; + +@Getter +public class GameModeConfig { + + public static void loadAll(Class config, BiConsumer consumer) { + File folder = new File(VelocityCore.get().getDataDirectory().getParent().toFile(), "FightSystem"); + if(!folder.exists()) + return; + + for(File file : Arrays.stream(folder.listFiles((file, name) -> name.endsWith(".yml") && !name.endsWith(".kits.yml"))).sorted().toList()) { + consumer.accept(file, Config.load(config, file, description -> {})); + } + } + + private Server Server = null; + private List CheckQuestions = Collections.emptyList(); + private String deadline = null; + private Schematic Schematic = null; + @Setter + private String GameName; + + @Getter + public static class Server { + private String Folder; + private String ServerJar; + private List ChatNames = Collections.emptyList(); + private List Maps; + private boolean Ranked = false; + private boolean Historic = false; + } + + @Getter + public static class Schematic { + private String Type; + private String Shortcut; + private String Material; + private boolean ManualCheck = true; + } + + public String getServerJar() { + return getServer().getServerJar(); + } + + public String getFolder() { + return getServer().getFolder(); + } + + public List getMaps() { + return getServer().getMaps(); + } + + public boolean isHistoric() { + return getServer().isHistoric(); + } + + public boolean isRanked() { + return getServer().isRanked(); + } + + public String getSchemType() { + return getSchematic().getType(); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/Node.java b/VelocityCore/src/de/steamwar/velocitycore/Node.java new file mode 100644 index 00000000..b97ef0ba --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/Node.java @@ -0,0 +1,218 @@ +/* + 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.velocitycore; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.logging.Level; + +public abstract class Node { + + //-Xquickstart Langzeitperformance testen! + private static final List OPENJ9_ARGS = Arrays.asList( + "-XX:+EnableCRIUSupport", "-XX:-CRIURestoreNonPortableMode", + "-Xgc:excessiveGCratio=80", "-Xdisableexplicitgc", "-Xnoclassgc", "-Xmos128M", "-Xmns48M", "-XX:+ExitOnOutOfMemoryError", // initial heap half values of memory observed by 1.19 spectate server + "-Xsyslog:none", "-Xtrace:none", "-Xverify:none", "-Xdump:system:none", "-Xdump:jit:none", "-Xdump:snap:none", + "-XX:+EnableDynamicAgentLoading", "-Dlog4j.configurationFile=log4j2.xml" + ); + private static final Set JAVA_8 = new HashSet<>(); + static { + JAVA_8.add("paper-1.8.8.jar"); + JAVA_8.add("paper-1.10.2.jar"); + JAVA_8.add("spigot-1.8.8.jar"); + JAVA_8.add("spigot-1.9.4.jar"); + JAVA_8.add("spigot-1.10.2.jar"); + } + + private static final long MIN_FREE_MEM = 4 * 1024 * 1024L; // 4 GiB + + private static final List nodes = new ArrayList<>(); + + public static Node getNode() { + for(Node node : nodes) { + if(node.belowLoadLimit) + return node; + } + return null; + } + + public static void forEach(Consumer consumer) { + nodes.forEach(consumer); + } + + public abstract ProcessBuilder startServer(String serverJar, File directory, String worldDir, String levelName, int port, String xmx, String... dParams); + + protected abstract ProcessBuilder prepareExecution(String... command); + protected abstract void calcLoadLimit(); + + protected final String hostname; + protected volatile boolean belowLoadLimit = true; + + protected Node(String hostname) { + this.hostname = hostname; + nodes.add(this); + VelocityCore.schedule(this::calcLoadLimit).repeat(2, TimeUnit.SECONDS).schedule(); + } + + public void execute(String... command) { + try { + prepareExecution(command).start().waitFor(); + } catch (IOException e) { + throw new SecurityException("Could not execute command", e); + } catch (InterruptedException e) { + VelocityCore.getLogger().log(Level.SEVERE, "Interrupted during execution", e); + Thread.currentThread().interrupt(); + } + } + + public boolean belowLoadLimit() { + return belowLoadLimit; + } + public String getName() { + return hostname; + } + + protected void constructServerstart(File directory, List cmd, String serverJar, String worldDir, String levelName, int port, String xmx, String... dParams) { + if (JAVA_8.contains(serverJar)) + cmd.add("/usr/lib/jvm/java-8-openj9-amd64/bin/java"); + else + cmd.add("/usr/lib/jvm/java-21-openj9-amd64/bin/java"); + + for(String param : dParams){ + cmd.add("-D" + param); + } + cmd.add("-Xshareclasses:nonfatal,name=" + directory.getName()); + cmd.add("-Xmx" + xmx); + cmd.addAll(OPENJ9_ARGS); + if (!JAVA_8.contains(serverJar)) { + cmd.add("--add-opens"); + cmd.add("java.base/jdk.internal.misc=ALL-UNNAMED"); + cmd.add("-XX:-CRIUSecProvider"); + } + cmd.add("-jar"); + cmd.add("/binarys/" + serverJar); + cmd.add("--log-strip-color"); + cmd.add("--world-dir"); + cmd.add(worldDir); + cmd.add("--level-name"); + cmd.add(levelName); + cmd.add("--port"); + cmd.add(String.valueOf(port)); + cmd.add("nogui"); + } + + protected void calcLoadLimit(BufferedReader meminfo) throws IOException { + String line = meminfo.readLine(); + while(!line.startsWith("MemAvailable")) { + line = meminfo.readLine(); + } + + long availableMem = Long.parseLong(line.replaceAll(" +", " ").split(" ")[1]); + belowLoadLimit = availableMem >= MIN_FREE_MEM; + } + + public static class LocalNode extends Node { + private static final File MEMINFO = new File("/proc/meminfo"); + + public LocalNode() { + super("sw"); + } + + @Override + public ProcessBuilder startServer(String serverJar, File directory, String worldDir, String levelName, int port, String xmx, String... dParams) { + List cmd = new ArrayList<>(); + constructServerstart(directory, cmd, serverJar, worldDir, levelName, port, xmx, dParams); + ProcessBuilder builder = new ProcessBuilder(cmd); + builder.directory(directory); + return builder; + } + + @Override + protected void calcLoadLimit() { + try (BufferedReader meminfo = new BufferedReader(new InputStreamReader(Files.newInputStream(MEMINFO.toPath())))) { + calcLoadLimit(meminfo); + } catch (IOException e) { + VelocityCore.getLogger().log(Level.SEVERE, "Could not read local load", e); + belowLoadLimit = false; + } + } + + @Override + protected ProcessBuilder prepareExecution(String... command) { + return new ProcessBuilder(command); + } + } + + public static class RemoteNode extends Node { + + public RemoteNode(String hostname) { + super(hostname); + VelocityCore.getLogger().log(Level.INFO, "Added node {0}", hostname); + } + + @Override + public ProcessBuilder startServer(String serverJar, File directory, String worldDir, String levelName, int port, String xmx, String... dParams) { + List cmd = new ArrayList<>(); + cmd.add("ssh"); + cmd.add("-L"); + cmd.add(port + ":localhost:" + port); + cmd.add(hostname); + cmd.add("cd"); + cmd.add(directory.getPath()); + cmd.add(";"); + constructServerstart(directory, cmd, serverJar, worldDir, levelName, port, xmx, dParams); + return new ProcessBuilder(cmd); + } + + @Override + protected ProcessBuilder prepareExecution(String... command) { + List cmd = new ArrayList<>(); + cmd.add("ssh"); + cmd.add(hostname); + cmd.addAll(Arrays.asList(command)); + return new ProcessBuilder(cmd); + } + + @Override + protected void calcLoadLimit() { + try { + Process process = prepareExecution("cat /proc/meminfo").start(); + if(!process.waitFor(1, TimeUnit.SECONDS)) + throw new IOException(hostname + " timeout"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + calcLoadLimit(reader); + } + } catch (IOException e) { + if(belowLoadLimit) + VelocityCore.getLogger().log(Level.SEVERE, "Could read remote load", e); + belowLoadLimit = false; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + belowLoadLimit = false; + } + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java b/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java new file mode 100644 index 00000000..75edf040 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java @@ -0,0 +1,373 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.messages.Chatter; +import de.steamwar.persistent.Arenaserver; +import de.steamwar.persistent.Bauserver; +import de.steamwar.persistent.Builderserver; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.*; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ServerStarter { + + private static final Portrange BAU_PORTS = VelocityCore.MAIN_SERVER ? new Portrange(10100, 20000) : new Portrange(2100, 2200); + private static final Portrange ARENA_PORTS = VelocityCore.MAIN_SERVER ? new Portrange(3000, 3100) : (VelocityCore.get().getConfig().isEventmode() ? new Portrange(4000, 5000) : BAU_PORTS); + + public static final String SERVER_PATH = "/servers/"; + private static final String USER_HOME = System.getProperty("user.home") + "/"; + private static final String EVENT_PATH = USER_HOME + "event/"; + public static final String TEMP_WORLD_PATH = USER_HOME + "arenaserver/"; + public static final String TUTORIAL_PATH = USER_HOME + "tutorials/"; + + public static final String WORLDS_BASE_PATH = USER_HOME + "userworlds"; + public static final String BUILDER_BASE_PATH = USER_HOME + "builder"; + + private File directory = null; + private String worldDir = null; + + private Node node = null; + private String serverJar = "spigot-1.15.2.jar"; + private String xmx = "768M"; + private Portrange portrange = BAU_PORTS; + private Function serverNameProvider = port -> node.getName() + port; + private BooleanSupplier startCondition = () -> true; + private Runnable worldSetup = () -> {}; + private String worldName = null; + private Runnable worldCleanup = () -> {}; + private boolean allowMerge = false; + private String fightMap = null; + private String gameMode = null; + private boolean checkpoint = false; + private ServerConstructor constructor = (serverName, port, builder, shutdownCallback, failureCallback) -> new Arenaserver(serverName, gameMode, fightMap, allowMerge, port, builder, shutdownCallback); + private Consumer callback = subserver -> {}; + + private final Set playersToSend = new HashSet<>(); + private final Map arguments = new HashMap<>(); + + public ServerStarter arena(ArenaMode mode, String map) { + portrange = ARENA_PORTS; + serverNameProvider = port -> mode.getGameName() + (port - portrange.start); + serverJar = mode.getServerJar(); + allowMerge = true; + fightMap = map; + gameMode = mode.getInternalName(); + directory = new File(SERVER_PATH, mode.getFolder()); + arguments.put("config", mode.getConfig()); + tempWorld(SERVER_PATH + mode.getFolder() + "/arenas/" + map); + return this; + } + + public ServerStarter event(EventFight eventFight) { + arena(ArenaMode.getByInternal(eventFight.getSpielmodus()), eventFight.getMap()); + node = VelocityCore.local; + worldDir = EVENT_PATH; + worldCleanup = () -> {}; + arguments.put("fightID", String.valueOf(eventFight.getFightID())); + fightMap = eventFight.getMap(); + gameMode = eventFight.getSpielmodus(); + + String serverName = Team.get(eventFight.getTeamBlue()).getTeamKuerzel() + " vs " + Team.get(eventFight.getTeamRed()).getTeamKuerzel(); + serverNameProvider = port -> serverName; + worldName = serverToWorldName(serverName + eventFight.getStartTime().toLocalDateTime().format(DateTimeFormatter.ISO_TIME)); + return this; + } + + public ServerStarter test(ArenaMode mode, String map, Player owner) { + arena(mode, map); + buildWithTemp(owner); + portrange = BAU_PORTS; + arguments.put("fightID", String.valueOf(-1)); + return send(owner); + } + + public ServerStarter blueLeader(Player player) { + arguments.put("blueLeader", player.getUniqueId().toString()); + return send(player); + } + + public ServerStarter redLeader(Player player) { + arguments.put("redLeader", player.getUniqueId().toString()); + return send(player); + } + + public ServerStarter check(int schemID) { + arguments.put("checkSchemID", String.valueOf(schemID)); + return this; + } + + public ServerStarter prepare(int schemID) { + arguments.put("prepareSchemID", String.valueOf(schemID)); + return this; + } + + public ServerStarter replay(int replayID) { + arguments.put("replay", String.valueOf(replayID)); + return this; + } + + public ServerStarter build(ServerVersion version, UUID owner) { + directory = version.getServerDirectory("Bau"); + serverJar = version.getServerJar(); + worldDir = version.getWorldFolder(WORLDS_BASE_PATH); + worldName = version != ServerVersion.SPIGOT_12 ? String.valueOf(SteamwarUser.get(owner).getId()) : owner.toString(); + checkpoint = true; + + build(owner); + + worldSetup = () -> { + File world = new File(worldDir, worldName); + if (!world.exists()) + copyWorld(node, new File(directory, "Bauwelt").getPath(), world.getPath()); + }; + + // Send players to existing server + startCondition = () -> { + Bauserver subserver = Bauserver.get(owner); + if(subserver != null) { + for(Player p : playersToSend) + SubserverSystem.sendPlayer(subserver, p); + return false; + } + boolean atLeastOneSupervisor = playersToSend.stream().anyMatch(player -> { + if (player.getUniqueId().equals(owner)) return true; + BauweltMember bauweltMember = BauweltMember.getBauMember(owner, player.getUniqueId()); + return bauweltMember.isSupervisor(); + }); + if (!atLeastOneSupervisor) { + for (Player p : playersToSend) { + Chatter.of(p).system("BAU_START_NOT_ALLOWED"); + } + } + return atLeastOneSupervisor; + }; + + return this; + } + + public ServerStarter tutorial(Player owner, Tutorial tutorial) { + directory = new File(SERVER_PATH, "Tutorial"); + buildWithTemp(owner); + tempWorld(TUTORIAL_PATH + tutorial.getTutorialId()); + arguments.put("tutorial", String.valueOf(tutorial.getTutorialId())); + return send(owner); + } + + private void tempWorld(String template) { + worldDir = TEMP_WORLD_PATH; + worldSetup = () -> copyWorld(node, template, worldDir + worldName); + worldCleanup = () -> SubserverSystem.deleteFolder(node, worldDir + worldName); + } + + private void buildWithTemp(Player owner) { + build(owner.getUniqueId()); + + // Stop existing build server + startCondition = () -> { + if(startingBau(owner)) + return false; + + Bauserver subserver = Bauserver.get(owner.getUniqueId()); + if(subserver != null && subserver.isStarted()) + subserver.stop(); + + return !startingBau(owner); + }; + } + + private void build(UUID owner) { + constructor = (serverName, port, builder, shutdownCallback, failureCallback) -> new Bauserver(serverName, owner, port, builder, shutdownCallback, failureCallback); + serverNameProvider = port -> bauServerName(SteamwarUser.get(owner)); + } + + public ServerStarter builder(ServerVersion version, String map, File generator) { + serverJar = version.getServerJar(); + directory = version.getServerDirectory("Builder"); + worldDir = version.getWorldFolder(BUILDER_BASE_PATH); + worldName = map; + serverNameProvider = port -> "*" + map; + checkpoint = true; + constructor = (serverName, port, builder, shutdownCallback, failureCallback) -> new Builderserver(serverName, worldName, port, builder, shutdownCallback, failureCallback); + + // Send players to existing server + startCondition = () -> { + Builderserver subserver = Builderserver.get(worldName); + if (subserver != null) { + for(Player p : playersToSend) + SubserverSystem.sendPlayer(subserver, p); + return false; + } + return true; + }; + + if(generator != null) { + worldSetup = () -> { + File leveldat = new File(new File(worldDir, worldName), "level.dat"); + try { + Files.copy(generator.toPath(), leveldat.toPath()); + } catch (IOException e) { + throw new SecurityException(e); + } + }; + } + + return this; + } + + public ServerStarter send(Player player) { + playersToSend.add(player); + return this; + } + + public ServerStarter callback(Consumer callback) { + this.callback = callback; + return this; + } + + public boolean start() { + if(!startCondition.getAsBoolean()) + return false; + + int port = portrange.freePort(); + String serverName = serverNameProvider.apply(port); + + if(node == null) { + node = Node.getNode(); + if(node == null) { + for (Player p : playersToSend) + Chatter.of(p).system("SERVER_START_OVERLOAD"); + + return false; + } + } + if(worldName == null) + worldName = serverToWorldName(serverName); + + worldSetup.run(); + arguments.put("logPath", worldName); + + File checkpointDir = new File("/tmp/" + System.getProperty("user.name") + ".checkpoints/" + directory.getName() + "/" + worldName); + if(checkpoint) + arguments.put("checkpoint", checkpointDir.getPath()); + + if(checkpoint && checkpointDir.exists()) { + try(DataOutputStream out = new DataOutputStream(Files.newOutputStream(new File(checkpointDir, "port").toPath()))) { + out.writeInt(port); + } catch (IOException e) { + throw new SecurityException(e); + } + + postStart(constructor.construct(serverName, port, node.prepareExecution( + "criu", "restore", "-D", checkpointDir.getPath(), "--auto-dedup", "--shell-job", "-v" + ), worldCleanup, e -> regularStart(serverName, port))); + } else { + regularStart(serverName, port); + } + + return true; + } + + private void regularStart(String serverName, int port) { + postStart(constructor.construct(serverName, port, node.startServer( + serverJar, directory, worldDir, worldName, port, xmx, arguments.entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue()).toArray(String[]::new) + ), worldCleanup, null)); + } + + private void postStart(Subserver subserver) { + for(Player p : playersToSend) + SubserverSystem.sendPlayer(subserver, p); + + callback.accept(subserver); + } + + private static boolean startingBau(Player p) { + Bauserver subserver = Bauserver.get(p.getUniqueId()); + if(subserver != null && !subserver.isStarted()) { + Chatter.of(p).system("BAU_START_ALREADY"); + return true; + } + return false; + } + + public static String bauServerName(SteamwarUser user) { + return user.getUserName() + "s Bau"; + } + + public static String serverToWorldName(String serverName) { + return serverName.replace(' ', '_').replace("[", "").replace("]", ""); + } + + public static void copyWorld(Node node, String template, String target) { + node.execute("cp", "-r", template, target); + } + + private interface ServerConstructor { + Subserver construct(String serverName, int port, ProcessBuilder builder, Runnable shutdownCallback, Consumer failureCallback); + } + + private static class Portrange { + + private final int start; + private final int end; + private int current; + + private Portrange(int start, int end) { + this.start = start; + this.end = end; + current = start; + } + + private void increment() { + current++; + + if(current == end) + current = start; + } + + private synchronized int freePort() { + Set usedPorts; + + synchronized (Subserver.getServerList()) { + usedPorts = Subserver.getServerList().stream().map(server -> server.getServer().getAddress().getPort()).collect(Collectors.toSet()); + } + + while(usedPorts.contains(current)) { + increment(); + } + + int result = current; + increment(); + return result; + } + } + +} \ No newline at end of file diff --git a/VelocityCore/src/de/steamwar/velocitycore/ServerVersion.java b/VelocityCore/src/de/steamwar/velocitycore/ServerVersion.java new file mode 100644 index 00000000..5da93505 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/ServerVersion.java @@ -0,0 +1,87 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore; + +import lombok.Getter; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +@Getter +public enum ServerVersion { + SPIGOT_12("spigot-1.12.2.jar", 12), + SPIGOT_15("spigot-1.15.2.jar", 15), + PAPER_19("paper-1.19.3.jar", 19), + PAPER_20("paper-1.20.1.jar", 20); + + private static final Map chatMap = new HashMap<>(); + + static { + chatMap.put("20", ServerVersion.PAPER_20); + chatMap.put("1.20", ServerVersion.PAPER_20); + chatMap.put("1.20.1", ServerVersion.PAPER_20); + chatMap.put("as", ServerVersion.PAPER_20); + chatMap.put("airship", ServerVersion.PAPER_20); + chatMap.put("wg", ServerVersion.PAPER_20); + chatMap.put("wargear", ServerVersion.PAPER_20); + chatMap.put("ws", ServerVersion.PAPER_20); + chatMap.put("warship", ServerVersion.PAPER_20); + chatMap.put("mwg", ServerVersion.PAPER_20); + chatMap.put("miniwargear", ServerVersion.PAPER_20); + + chatMap.put("19", ServerVersion.PAPER_19); + chatMap.put("1.19", ServerVersion.PAPER_19); + chatMap.put("1.19.2", ServerVersion.PAPER_19); + + chatMap.put("15", ServerVersion.SPIGOT_15); + chatMap.put("1.15", ServerVersion.SPIGOT_15); + chatMap.put("1.15.2", ServerVersion.SPIGOT_15); + + chatMap.put("12", ServerVersion.SPIGOT_12); + chatMap.put("1.12", ServerVersion.SPIGOT_12); + chatMap.put("1.12.2", ServerVersion.SPIGOT_12); + } + + public static ServerVersion get(String chat) { + return chatMap.get(chat.toLowerCase()); + } + + public static Set chatVersions() { + return chatMap.keySet(); + } + + private final String serverJar; + private final int versionSuffix; + + ServerVersion(String serverJar, int versionSuffix) { + this.serverJar = serverJar; + this.versionSuffix = versionSuffix; + } + + public String getWorldFolder(String base) { + return base + versionSuffix + "/"; + } + + public File getServerDirectory(String base) { + return new File(ServerStarter.SERVER_PATH, base + versionSuffix); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/SubserverSystem.java b/VelocityCore/src/de/steamwar/velocitycore/SubserverSystem.java new file mode 100644 index 00000000..bd2c7f73 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/SubserverSystem.java @@ -0,0 +1,58 @@ +/* + 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.velocitycore; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.velocitycore.network.handlers.FightInfoHandler; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.network.packets.server.StartingServerPacket; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.IgnoreSystem; +import de.steamwar.sql.SteamwarUser; +import net.kyori.adventure.text.event.ClickEvent; + +import java.util.UUID; + +public class SubserverSystem { + private SubserverSystem(){} + + public static void deleteFolder(Node node, String worldName) { + node.execute("rm", "-r", worldName); + } + + public static void sendDeniedMessage(Chatter p, UUID owner) { + if(IgnoreSystem.isIgnored(owner, p.user().getUUID())){ + p.system("SERVER_IGNORED"); + return; + } + + Chatter o = Chatter.of(owner); + o.system("SERVER_ADD_MEMBER", p); + o.prefixless("SERVER_ADD_MESSAGE", new Message("SERVER_ADD_MESSAGE_HOVER"), ClickEvent.runCommand("/bau addmember " + p.user().getUserName())); + } + + public static void sendPlayer(Subserver subserver, Player player) { + subserver.sendPlayer(player); + if(!subserver.isStarted() && FightInfoHandler.onLobby(player)) + NetworkSender.send(player, new StartingServerPacket(SteamwarUser.get(player.getUniqueId()).getId())); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java new file mode 100644 index 00000000..39442713 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java @@ -0,0 +1,282 @@ +/* + 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.velocitycore; + +import com.google.inject.Inject; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; +import com.velocitypowered.api.plugin.Dependency; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.scheduler.Scheduler; +import de.steamwar.command.*; +import de.steamwar.messages.Chatter; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.persistent.ReloadablePlugin; +import de.steamwar.sql.Punishment; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.Team; +import de.steamwar.sql.UserElo; +import de.steamwar.sql.internal.Statement; +import de.steamwar.velocitycore.commands.*; +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.velocitycore.discord.DiscordConfig; +import de.steamwar.velocitycore.listeners.*; +import de.steamwar.velocitycore.mods.*; +import de.steamwar.velocitycore.network.handlers.*; +import de.steamwar.velocitycore.tablist.TablistManager; +import lombok.Getter; + +import java.nio.file.Path; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Plugin( + id = "velocitycore", + name = "VelocityCore", + dependencies = { @Dependency(id = "persistentvelocitycore") } +) +public class VelocityCore implements ReloadablePlugin { + + public static boolean MAIN_SERVER; + + private static VelocityCore instance; + public static VelocityCore get() { + return instance; + } + + public static Node local; + + public static Scheduler.TaskBuilder schedule(Runnable runnable) { + return instance.proxyServer.getScheduler().buildTask(instance, runnable); + } + + public static ProxyServer getProxy() { + return instance.proxyServer; + } + + public static Logger getLogger() { + return instance.logger; + } + + private final ProxyServer proxyServer; + private final Logger logger; + @Getter + private final Path dataDirectory; + + private Driver sqlDriver; + @Getter + private Config config; + private ErrorLogger errorLogger; + private TablistManager tablistManager; + + @Getter + private TeamCommand teamCommand; + + @Inject + public VelocityCore(ProxyServer proxyServer, Logger logger, @DataDirectory Path dataDirectory) { + this.proxyServer = proxyServer; + this.logger = logger; + this.dataDirectory = dataDirectory; + + } + + @Subscribe + @Override + public void onProxyInitialization(ProxyInitializeEvent event) { + instance = this; + config = Config.load(); + MAIN_SERVER = proxyServer.getBoundAddress().getPort() == 25565; + + try { + sqlDriver = Statement.mysqlMode() ? new com.mysql.cj.jdbc.Driver() : new org.sqlite.JDBC(); + DriverManager.registerDriver(sqlDriver); + } catch (SQLException e) { + throw new SecurityException(e); + } + + errorLogger = new ErrorLogger(); + + SWCommandUtils.init((SWTypeMapperCreator, Chatter, Object>) (mapper, tabCompleter) -> new TypeMapper<>() { + @Override + public Object map(Chatter sender, PreviousArguments previousArguments, String s) { + return mapper.apply(s); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + return tabCompleter.apply(sender, s); + } + }); + schedule(TabCompletionCache::invalidateOldEntries).repeat(1, TimeUnit.SECONDS).schedule(); + + initStaticServers(); + PollSystem.init(); + + new Hostname(); + new PluginMessage(); + new Schematica(); + new Badlion(); + new FabricModSender(); + new ReplayMod(); + new FML2(); + + new ConnectionListener(); + new ChatListener(); + new BanListener(); + new CheckListener(); + new IPSanitizer(); + + local = new Node.LocalNode(); + if(MAIN_SERVER) { + //new Node.RemoteNode("lx"); + } + + new TeamchatCommand(); + new MsgCommand(); + new RCommand(); + new PingCommand(); + new AlertCommand(); + new KickCommand(); + new JoinmeCommand(); + new TpCommand(); + HelpCommand helpCommand = new HelpCommand(); + teamCommand = new TeamCommand(); + new ServerTeamchatCommand(); + new DevCommand(); + new EventCommand(); + new EventreloadCommand(); + new EventRescheduleCommand(); + new PollCommand(); + new BugCommand(); + new WhoisCommand(); + new RulesCommand(); + new IgnoreCommand(); + new UnIgnoreCommand(); + new PollresultCommand(); + new ListCommand(); + new StatCommand(); + new VerifyCommand(); + new GDPRQuery(); + new PlaytimeCommand(); + new ArenaCommand(); + new RankCommand(); + new LocalCommand(); + new SetLocaleCommand(); + new BuilderCloudCommand(); + new CheckCommand(); + + new ModCommand(); + + // Punishment Commands: + new PunishmentCommand("ban", Punishment.PunishmentType.Ban); + new PunishmentCommand("mute", Punishment.PunishmentType.Mute); + new PunishmentCommand("noschemreceiving", Punishment.PunishmentType.NoSchemReceiving); + new PunishmentCommand("noschemsharing", Punishment.PunishmentType.NoSchemSharing); + new PunishmentCommand("noschemsubmitting", Punishment.PunishmentType.NoSchemSubmitting); + new PunishmentCommand("nodev", Punishment.PunishmentType.NoDevServer); + new PunishmentCommand("nofight", Punishment.PunishmentType.NoFightServer); + new PunishmentCommand("noteamserver", Punishment.PunishmentType.NoTeamServer); + new PunishmentCommand("note", Punishment.PunishmentType.Note); + + if(!config.isEventmode()){ + new BauCommand(helpCommand); + new WebpasswordCommand(); + new FightCommand(); + new ChallengeCommand(); + new HistoricCommand(); + new ReplayCommand(); + new TutorialCommand(); + + new Broadcaster(); + }else{ + new EventModeListener(); + } + + for(PacketHandler handler : new PacketHandler[] { + new EloPlayerHandler(), new EloSchemHandler(), new ExecuteCommandHandler(), new FightInfoHandler(), + new ImALobbyHandler(), new InventoryCallbackHandler(), new PrepareSchemHandler() + }) + handler.register(); + + new EventStarter(); + new SessionManager(); + tablistManager = new TablistManager(); + new SettingsChangedListener(); + + schedule(() -> { + SteamwarUser.clear(); + UserElo.clear(); + Team.clear(); + }).repeat(1, TimeUnit.HOURS).schedule(); + + DiscordConfig discordConfig = DiscordConfig.load(); + if (discordConfig != null) { + try { + new DiscordBot(discordConfig); + } catch (Throwable e) { + logger.log(Level.SEVERE, "Could not initialize discord bot", e); + } + } + } + + @Subscribe + @Override + public void onProxyShutdown(ProxyShutdownEvent event) { + try { + DiscordBot.withBot(bot -> bot.getJda().shutdownNow()); + } catch (Throwable e) { + logger.log(Level.SEVERE, "Could not shutdown discord bot", e); + } + + if(tablistManager != null) + tablistManager.disable(); + errorLogger.unregister(); + Statement.closeAll(); + + try { + DriverManager.deregisterDriver(sqlDriver); + } catch (SQLException e) { + throw new SecurityException(e); + } + } + + private void initStaticServers() { + for (Map.Entry entry : config.getServers().entrySet()) { + Config.Server server = entry.getValue(); + List cmds = server.getCommands(); + String cmd = cmds.remove(0); + + if(server.getSpectatePort() != 0) + EventStarter.addSpectateServer(server.getSpectatePort(), cmd); + + new ServerSwitchCommand(cmd, entry.getKey(), cmds.toArray(new String[0])); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java new file mode 100644 index 00000000..b95bbd69 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/AlertCommand.java @@ -0,0 +1,46 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.UserPerm; + +public class AlertCommand extends SWCommand { + + public AlertCommand() { + super("alert", UserPerm.MODERATION, "broadcast", "bbc"); + } + + @Register(description = "USAGE_ALERT") + public void broadcast(Chatter sender, @OptionalValue("") @StaticValue("-discord") String sendToDiscord, String... message) { + if (message.length == 0) { + sender.system("USAGE_ALERT"); + return; + } + + String s = String.join(" ", message); + Chatter.broadcast().system("ALERT", s.replace('&', '§')); + + if ("-discord".equals(sendToDiscord)) + DiscordBot.withBot(bot -> bot.getAnnouncementChannel().send(s)); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ArenaCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ArenaCommand.java new file mode 100644 index 00000000..1fa04e0e --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ArenaCommand.java @@ -0,0 +1,72 @@ +/* + 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeMapper; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; + +import java.util.Collection; +import java.util.List; + +public class ArenaCommand extends SWCommand { + + public ArenaCommand() { + super("arena"); + } + + @Register + public void arenaJoin(PlayerChatter sender, Subserver server) { + TpCommand.teleport(sender, server.getRegisteredServer()); + } + + @ClassMapper(value = Subserver.class, local = true) + public TypeMapper serverInfoTypeMapper() { + return new TypeMapper<>() { + @Override + public Subserver map(Chatter sender, PreviousArguments previousArguments, String s) { + return Subserver.getSubserver(VelocityCore.getProxy().getServer(s).map(RegisteredServer::getServerInfo).orElse(null)); + } + + @Override + public boolean validate(Chatter sender, Subserver value, MessageSender messageSender) { + if (value == null || value.getType() != Servertype.ARENA) { + sender.system("ARENA_NOT_FOUND"); + return false; + } + return true; + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + List subserverList = Subserver.getServerList(); + synchronized (subserverList) { + return subserverList.stream().filter(subserver -> subserver.getType() == Servertype.ARENA).map(subserver -> subserver.getServer().getName()).toList(); + } + } + }; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java new file mode 100644 index 00000000..037b4ab6 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java @@ -0,0 +1,251 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.velocitycore.*; +import de.steamwar.velocitycore.inventory.SWInventory; +import de.steamwar.velocitycore.inventory.SWItem; +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.velocitycore.util.BauLock; +import de.steamwar.velocitycore.util.BauLockState; +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeMapper; +import de.steamwar.command.TypeValidator; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.network.packets.server.BaumemberUpdatePacket; +import de.steamwar.persistent.Bauserver; +import de.steamwar.sql.BauweltMember; +import de.steamwar.sql.SteamwarUser; + +import java.util.Collection; +import java.util.function.Consumer; + +public class BauCommand extends SWCommand { + + private final HelpCommand command; + + public BauCommand(HelpCommand command) { + super("bau", "b", "build", "gs"); + this.command = command; + } + + @Register(noTabComplete = true) + public void genericHelp(Chatter sender, String... args) { + this.command.sendBauHelp(sender); + } + + @Register + public void toBau(PlayerChatter sender, @OptionalValue(value = "", onlyUINIG = true) ServerVersion version) { + new ServerStarter().build(version, sender.user().getUUID()).send(sender.getPlayer()).start(); + } + + @Register(value = "addmember", description = "BAU_ADDMEMBER_USAGE") + public void addmember(Chatter sender, @Validator("addMemberTarget") SteamwarUser target) { + BauweltMember.addMember(sender.user().getUUID(), target.getUUID()); + sender.system("BAU_ADDMEMBER_ADDED"); + Chatter.of(target.getUUID()).system("BAU_ADDMEMBER_ADDED_TARGET", sender); + } + + @Validator(value = "addMemberTarget", local = true) + public TypeValidator addMemberTargetValidator() { + return (sender, value, messageSender) -> { + if (value == null) { + messageSender.send("UNKNOWN_PLAYER"); + return false; + } + if (sender.user().getUUID().equals(value.getUUID())) { + messageSender.send("BAU_ADDMEMBER_SELFADD"); + return false; + } + if (BauweltMember.getBauMember(sender.user().getId(), value.getId()) != null) { + messageSender.send("BAU_ADDMEMBER_ISADDED"); + return false; + } + return true; + }; + } + + @Register(value = "tp", description = "BAU_TP_USAGE") + @Register("teleport") + public void teleport(PlayerChatter sender, @Validator("teleportTarget") SteamwarUser worldOwner, @OptionalValue(value = "", onlyUINIG = true) ServerVersion version) { + new ServerStarter().build(version, worldOwner.getUUID()).send(sender.getPlayer()).start(); + } + + @Validator(value = "teleportTarget", local = true) + public TypeValidator teleportTargetValidator() { + return (sender, owner, messageSender) -> { + if (owner == null) { + messageSender.send("UNKNOWN_PLAYER"); + return false; + } + if (sender.user().getId() != owner.getId() && BauweltMember.getBauMember(owner.getId(), sender.user().getId()) == null) { + SubserverSystem.sendDeniedMessage(sender, owner.getUUID()); + messageSender.send("BAU_TP_NOALLOWED"); + return false; + } + + if (BauLock.isLocked(owner, sender.user())) { + messageSender.send("BAU_LOCKED_NOALLOWED"); + Chatter.of(owner.getUUID()).system("BAU_LOCK_BLOCKED", sender); + return false; + } + return true; + }; + } + + @Register("info") + public void info(PlayerChatter sender) { + sender.getPlayer().spoofChatInput("/bauinfo"); + } + + @Register("setspectator") + public void setSpectator(Chatter sender, @Mapper("addedUsers") @AllowNull @OptionalValue("") SteamwarUser user) { + setPerms(sender, user, "setspectator", "BAU_MEMBER_SET_SPECTATOR", member -> { + member.setBuild(false); + member.setSupervisor(false); + }); + } + + @Register("setbuild") + public void setBuild(Chatter sender, @Mapper("addedUsers") @AllowNull @OptionalValue("") SteamwarUser user) { + setPerms(sender, user, "setbuild", "BAU_MEMBER_SET_BUILDER", member -> { + member.setBuild(true); + member.setSupervisor(false); + }); + } + + @Register("setsupervisor") + public void setSupervisor(Chatter sender, @Mapper("addedUsers") @AllowNull @OptionalValue("") SteamwarUser user) { + setPerms(sender, user, "setsupervisor", "BAU_MEMBER_SET_SUPERVISOR", member -> { + member.setBuild(true); + member.setSupervisor(true); + }); + } + + private void setPerms(Chatter owner, SteamwarUser user, String name, String permName, Consumer setter) { + if (user == null) { + owner.system("BAU_MEMBER_SET_USAGE", name); + return; + } + + withMember(owner, user, target -> { + setter.accept(target); + + Bauserver bauserver = Bauserver.get(owner.user().getUUID()); + if(bauserver != null) + bauserver.getRegisteredServer().getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket())); + + Chatter.of(user.getUUID()).system("BAU_MEMBER_SET_TARGET", owner, new Message(permName)); + owner.system("BAU_MEMBER_SET", new Message(permName)); + }); + } + + @Register(value = "delmember", description = "BAU_DELMEMBER_USAGE") + public void delmember(Chatter owner, @Mapper("addedUsers") SteamwarUser user) { + withMember(owner, user, target -> { + target.remove(); + + Chatter member = Chatter.of(user.getUUID()); + member.system("BAU_DELMEMBER_DELETED_TARGET", owner); + member.withPlayer(player -> { + Bauserver bauserver = Bauserver.get(owner.user().getUUID()); + if (bauserver != null && bauserver.getRegisteredServer().getPlayersConnected().contains(player)) + player.createConnectionRequest(VelocityCore.get().getConfig().lobbyserver()).fireAndForget(); + }); + + owner.system("BAU_DELMEMBER_DELETED"); + }); + } + + @Mapper(value = "addedUsers", local = true) + public TypeMapper addedUsers() { + return new TypeMapper() { + @Override + public SteamwarUser map(Chatter sender, PreviousArguments previousArguments, String s) { + return SteamwarUser.get(s); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + return BauweltMember.getMembers(sender.user().getId()).stream() + .map(bauweltMember -> SteamwarUser.get(bauweltMember.getMemberID()).getUserName()) + .toList(); + } + }; + } + + @Register("resetall") + @Register("delete") + public void delete(PlayerChatter sender, @OptionalValue(value = "", onlyUINIG = true) ServerVersion version) { + SWInventory inventory = new SWInventory(sender, 9, new Message("BAU_DELETE_GUI_NAME")); + inventory.addItem(0, new SWItem(new Message("BAU_DELETE_GUI_DELETE"), 10), click -> { + String world = version.getWorldFolder(ServerStarter.WORLDS_BASE_PATH) + (version != ServerVersion.SPIGOT_12 ? sender.user().getId() : sender.user().getUUID().toString()); + + VelocityCore.schedule(() -> { + Bauserver subserver = Bauserver.get(sender.user().getUUID()); + if(subserver != null) + subserver.stop(); + + SubserverSystem.deleteFolder(VelocityCore.local, world); + sender.system("BAU_DELETE_DELETED"); + }).schedule(); + + inventory.close(); + }); + inventory.addItem(8, new SWItem(new Message("BAU_DELETE_GUI_CANCEL"), 1), click -> inventory.close()); + inventory.open(); + } + + @Register("test") + @Register("testarena") + public void testarena(PlayerChatter sender, @Mapper("nonHistoricArenaMode") @OptionalValue("") @AllowNull ArenaMode arenaMode, @Mapper("arenaMap") @OptionalValue("") @AllowNull String map) { + FightCommand.createArena(sender, "/bau testarena ", false, arenaMode, map, false, (chatter, mode, m) -> + VelocityCore.schedule(() -> new ServerStarter().test(mode, m, sender.getPlayer()).start()).schedule() + ); + } + + @Register(value = "lock", description = "BAU_LOCKED_OPTIONS") + public void lock(Chatter sender, BauLockState bauLockState) { + BauLock.setLocked(sender, bauLockState); + } + + @Register("unlock") + public void unlock(Chatter sender) { + BauLock.setLocked(sender, BauLockState.OPEN); + } + + private static void withMember(Chatter owner, SteamwarUser member, Consumer function) { + if (member == null) { + owner.system("UNKNOWN_PLAYER"); + return; + } + + BauweltMember target = BauweltMember.getBauMember(owner.user().getId(), member.getId()); + if (target == null) { + owner.system("BAU_MEMBER_NOMEMBER"); + return; + } + + function.accept(target); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/BugCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/BugCommand.java new file mode 100644 index 00000000..22c9f391 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/BugCommand.java @@ -0,0 +1,39 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.sql.SWException; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; + +public class BugCommand extends SWCommand { + public BugCommand() { + super("bug"); + } + + @Register + public void bugMessage(Chatter sender, String... message) { + int id = SWException.logGetId( + String.join(" ", message), + sender.withPlayerOrOffline(player -> player.getCurrentServer().map(connection -> connection.getServerInfo().getName()).orElse("offline"), () -> "offline") + " " + sender.user().getUserName() + " " + sender.user().getId() + ); + sender.system("BUG_MESSAGE", id); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/BuilderCloudCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/BuilderCloudCommand.java new file mode 100644 index 00000000..4c5c115d --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/BuilderCloudCommand.java @@ -0,0 +1,167 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.commands; + +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.velocitycore.ServerStarter; +import de.steamwar.velocitycore.ServerVersion; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeMapper; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.UserPerm; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +public class BuilderCloudCommand extends SWCommand { + + public BuilderCloudCommand() { + super("buildercloud", UserPerm.BUILD, "builder"); + } + + @Register(value = "create", description = "BUILDERCLOUD_CREATE_USAGE") + public void create(Chatter sender, @ErrorMessage("BUILDERCLOUD_VERSION") ServerVersion version, @Mapper("map") String map, @OptionalValue("") @Mapper("generator") @AllowNull File generator) { + mapFile(version, map).mkdir(); + sender.withPlayer(p -> new ServerStarter().builder(version, map, generator).send(p).start()); + } + + @Register(description = "BUILDERCLOUD_USAGE") + public void start(PlayerChatter sender, @ErrorMessage("BUILDERCLOUD_VERSION") ServerVersion version, @Mapper("map") String map) { + if(!mapFile(version, map).exists()) { + sender.system("BUILDERCLOUD_UNKNOWN_MAP"); + return; + } + + new ServerStarter().builder(version, map, null).send(sender.getPlayer()).start(); + } + + @Register(value = "rename", description = "BUILDERCLOUD_RENAME_USAGE") + public void rename(Chatter sender, @ErrorMessage("BUILDERCLOUD_VERSION") ServerVersion version, @Mapper("map") String oldName, String newName) { + File oldMap = mapFile(version, oldName); + if(!oldMap.exists()) { + sender.system("BUILDERCLOUD_UNKNOWN_MAP"); + return; + } + + File newMap = mapFile(version, newName); + if(newMap.exists()) { + sender.system("BUILDERCLOUD_EXISTING_MAP"); + return; + } + + try { + Files.move(oldMap.toPath(), newMap.toPath()); + } catch (IOException e) { + throw new SecurityException(e); + } + + sender.system("BUILDERCLOUD_RENAMED"); + } + + @Register(value = "deploy", description = "BUILDERCLOUD_DEPLOY_USAGE") + public void deploy(Chatter sender, @Mapper("nonHistoricArenaMode") ArenaMode arenaMode, @ErrorMessage("BUILDERCLOUD_VERSION") ServerVersion version, @Mapper("map") String map) { + if(!mapFile(version, map).exists()) { + sender.system("BUILDERCLOUD_UNKNOWN_MAP"); + return; + } + + VelocityCore.schedule(() -> { + VelocityCore.local.execute("/binarys/deployarena.py", arenaMode.getConfig(), Integer.toString(version.getVersionSuffix()), map); + ArenaMode.init(); + sender.system("BUILDERCLOUD_DEPLOY_FINISHED"); + }).schedule(); + } + + @Cached(global = true) + @Mapper(value = "map", local = true) + private TypeMapper mapTypeMapper() { + + return new TypeMapper() { + @Override + public String map(Chatter sender, PreviousArguments previousArguments, String s) { + return s; + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + File folder = getWorldFolder(previousArguments, 1); + + String[] files; + if(folder == null || (files = folder.list()) == null) + return Collections.emptyList(); + + return Arrays.stream(files).filter(file -> new File(folder, file).isDirectory()).filter(file -> s.startsWith(".") || !file.startsWith(".")).toList(); + } + }; + } + + @Cached(global = true) + @Mapper(value = "generator", local = true) + private TypeMapper generatorTypeMapper() { + + return new TypeMapper() { + @Override + public File map(Chatter sender, PreviousArguments previousArguments, String s) { + if(s.isEmpty()) + return null; + + File folder = getWorldFolder(previousArguments, 2); + + if(folder == null) + throw new SecurityException(); + + File generator = new File(folder, s + ".dat"); + if(!generator.exists() || !generator.isFile()) + throw new SecurityException(); + + return generator; + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + File folder = getWorldFolder(previousArguments, 2); + + String[] files; + if(folder == null || (files = folder.list()) == null) + return Collections.emptyList(); + + return Arrays.stream(files).filter(file -> new File(folder, file).isFile()).filter(file -> file.endsWith(".dat")).map(file -> file.substring(0, file.length() - 4)).toList(); + } + }; + } + + private File mapFile(ServerVersion version, String map) { + return new File(version.getWorldFolder(ServerStarter.BUILDER_BASE_PATH), map); + } + + private File getWorldFolder(PreviousArguments previousArguments, int offset) { + ServerVersion v = ServerVersion.get(previousArguments.userArgs[previousArguments.userArgs.length - offset]); + if(v == null) + return null; + return new File(v.getWorldFolder(ServerStarter.BUILDER_BASE_PATH)); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ChallengeCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ChallengeCommand.java new file mode 100644 index 00000000..ddc33654 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ChallengeCommand.java @@ -0,0 +1,99 @@ +/* + 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.velocitycore.ServerStarter; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeValidator; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.IgnoreSystem; +import net.kyori.adventure.text.event.ClickEvent; + +import java.util.LinkedList; + +import static de.steamwar.persistent.Storage.challenges; + +public class ChallengeCommand extends SWCommand { + + public ChallengeCommand() { + super("challenge"); + } + + @Register(description = "CHALLENGE_USAGE") + public void challenge(@Validator("arenaPlayer") PlayerChatter sender, @Validator("target") Player target, @Mapper("nonHistoricArenaMode") @OptionalValue("") @AllowNull ArenaMode arenaMode, @Mapper("arenaMap") @OptionalValue("") @AllowNull String map) { + FightCommand.createArena(sender, "/challenge " + target.getUsername() + " ", false, arenaMode, map, false, (chatter, mode, m) -> { + Player p = sender.getPlayer(); + if(challenges.containsKey(target) && challenges.get(target).contains(p)){ + challenges.remove(target); + challenges.remove(p); + + new ServerStarter().arena(mode, map).blueLeader(sender.getPlayer()).redLeader(target).callback( + arena -> Chatter.broadcast().system("CHALLENGE_BROADCAST", new Message("CHALLENGE_BROADCAST_HOVER"), ClickEvent.runCommand("/arena " + arena.getServer().getName()), mode.getGameName(), p, target) + ).start(); + }else{ + if(!challenges.containsKey(p)){ + challenges.put(p, new LinkedList<>()); + } + + challenges.get(p).add(target); + + sender.system("CHALLENGE_CHALLENGED", target, mode.getGameName()); + Chatter.of(target).system("CHALLENGE_CHALLENGED_TARGET", p, mode.getGameName(), mode.getMaps().size() != 1 ? new Message("CHALLENGE_CHALLENGED_MAP", m) : ""); + + Chatter.of(target).system("CHALLENGE_ACCEPT", new Message("CHALLENGE_ACCEPT_HOVER"), ClickEvent.runCommand("/challenge " + p.getUsername() + " " + mode.getChatName() + " " + m)); + } + }); + } + + @Validator(value = "target", local = true) + public TypeValidator targetValidator() { + return (sender, value, messageSender) -> { + if (value == null) { + messageSender.send("CHALLENGE_OFFLINE"); + return false; + } + if (sender == value) { + messageSender.send("CHALLENGE_SELF"); + return false; + } + if (IgnoreSystem.isIgnored(value.getUniqueId(), sender.user().getUUID())) { + messageSender.send("CHALLENGE_IGNORED"); + return false; + } + + Subserver subserver = Subserver.getSubserver(value); + if (subserver != null && subserver.getType() == Servertype.ARENA) { + messageSender.send("CHALLENGE_INARENA"); + return false; + } + return true; + }; + } + + public static void remove(Player player){ + challenges.remove(player); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java new file mode 100644 index 00000000..af6328b2 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java @@ -0,0 +1,296 @@ +/* + 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.persistent.Bauserver; +import de.steamwar.sql.*; +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.velocitycore.ServerStarter; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.velocitycore.discord.util.DiscordAlert; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.awt.*; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.List; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +public class CheckCommand extends SWCommand { + private static final Map fightTypes = new HashMap<>(); + private static final Map> checkQuestions = new HashMap<>(); + + private static final Map currentCheckers = new HashMap<>(); + private static final Map currentSchems = new HashMap<>(); + + public static void setCheckQuestions(SchematicType checkType, List checkQuestions) { + CheckCommand.checkQuestions.put(checkType, checkQuestions); + } + + public static void addFightType(SchematicType checkType, SchematicType fightType) { + fightTypes.put(checkType, fightType); + } + + public static boolean isChecking(Player player){ + return currentCheckers.containsKey(player.getUniqueId()); + } + + public static SchematicNode getCheckingSchem(Player player) { + return currentCheckers.get(player.getUniqueId()).schematic; + } + + public static Message getWaitTime(SchematicNode schematic) { + long waitedMillis = Timestamp.from(Instant.now()).getTime() - schematic.getLastUpdate().getTime(); + String ce = waitedMillis > 86400000 ? "c" : "e"; + String color = waitedMillis > 14400000 ? ce : "a"; + long hours = waitedMillis / 3600000; + long minutes = (waitedMillis - hours * 3600000) / 60000; + return new Message("CHECK_LIST_WAIT", color, hours, (minutes < 10) ? "0" + minutes : minutes); + } + + public CheckCommand() { + super("check", UserPerm.CHECK); + + VelocityCore.schedule(() -> sendReminder(Chatter.serverteam())).repeat(10, TimeUnit.MINUTES).schedule(); + } + + public static void sendReminder(Chatter chatter) { + List schematics = getSchemsToCheck(); + if(schematics.size() == currentCheckers.size()) + return; + + chatter.system("CHECK_REMINDER", new Message("CHECK_REMINDER_HOVER"), ClickEvent.runCommand("/check list"), schematics.size() - currentCheckers.size()); + } + + @Register(value = "list", description = "CHECK_HELP_LIST") + public void list(Chatter sender) { + List schematicList = getSchemsToCheck(); + + sender.system("CHECK_LIST_HEADER", schematicList.size()); + + for (SchematicNode schematic : schematicList) { + CheckSession current = currentSchems.get(schematic.getId()); + if (current == null) { + sender.prefixless("CHECK_LIST_TO_CHECK", + new Message("CHECK_LIST_TO_CHECK_HOVER"), + ClickEvent.runCommand("/check schematic " + schematic.getId()), + getWaitTime(schematic), + schematic.getSchemtype().getKuerzel(), SteamwarUser.get(schematic.getOwner()).getUserName(), schematic.getName()); + } else { + sender.prefixless("CHECK_LIST_CHECKING", + new Message("CHECK_LIST_CHECKING_HOVER"), + ClickEvent.runCommand("/join " + current.checker.user().getUserName()), + getWaitTime(schematic), + schematic.getSchemtype().getKuerzel(), SteamwarUser.get(schematic.getOwner()).getUserName(), schematic.getName(), current.checker.user().getUserName()); + } + } + } + + @Register(value = "schematic", noTabComplete = true) + public void schematic(PlayerChatter sender, String schemID) { + if(isChecking(sender.getPlayer())){ + sender.system("CHECK_SCHEMATIC_ALREADY_CHECKING"); + return; + } + + SchematicNode schem = SchematicNode.getSchematicNode(Integer.parseInt(schemID)); + if(!schem.getSchemtype().check()){ + VelocityCore.getLogger().log(Level.SEVERE, () -> sender.user().getUserName() + " tried to check an uncheckable schematic!"); + return; + }else if(schem.getOwner() == sender.user().getId()) { + sender.system("CHECK_SCHEMATIC_OWN"); + return; + } + + int playerTeam = sender.user().hasPerm(UserPerm.MODERATION) ? 0 : sender.user().getTeam(); + if (playerTeam != 0 && SteamwarUser.get(schem.getOwner()).getTeam() == playerTeam) { + sender.system("CHECK_SCHEMATIC_OWN_TEAM"); + return; + } + new CheckSession(sender, schem); + } + + @Register(value = "cancel", description = "CHECK_HELP_CANCEL") + @Register("abort") + public void abortCommand(PlayerChatter sender) { + abort(sender.getPlayer()); + } + + public static void abort(Player player) { + if(notChecking(player)) + return; + + Chatter.of(player).system("CHECK_ABORT"); + currentCheckers.get(player.getUniqueId()).abort(); + } + + @Register(value = "next", description = "CHECK_HELP_NEXT") + public void next(PlayerChatter sender) { + if(notChecking(sender.getPlayer())) + return; + + currentCheckers.get(sender.getPlayer().getUniqueId()).next(); + } + + @Register(value = "accept") + public void accept(PlayerChatter sender) { + next(sender); + } + + @Register(value = "decline", description = "CHECK_HELP_DECLINE") + public void decline(PlayerChatter sender, String... message) { + if(notChecking(sender.getPlayer())) + return; + + currentCheckers.get(sender.getPlayer().getUniqueId()).decline(String.join(" ", message)); + } + + public static List getSchemsToCheck(){ + List schematicList = new ArrayList<>(); + + for (SchematicType type : SchematicType.values()) { + if (type.check()) + schematicList.addAll(SchematicNode.getAllSchematicsOfType(type)); + } + return schematicList; + } + + public static String getChecker(SchematicNode schematic) { + if (currentSchems.get(schematic.getId()) == null) return null; + return currentSchems.get(schematic.getId()).checker.user().getUserName(); + } + + private static boolean notChecking(Player player){ + if(!isChecking(player)){ + Chatter.of(player).system("CHECK_NOT_CHECKING"); + return true; + } + return false; + } + + private static class CheckSession{ + private final PlayerChatter checker; + private final SchematicNode schematic; + private final Timestamp startTime; + private final ListIterator checkList; + + private CheckSession(PlayerChatter checker, SchematicNode schematic){ + this.checker = checker; + this.schematic = schematic; + this.startTime = Timestamp.from(Instant.now()); + this.checkList = checkQuestions.get(schematic.getSchemtype()).listIterator(); + + VelocityCore.schedule(() -> { + ArenaMode mode = ArenaMode.getBySchemType(fightTypes.get(schematic.getSchemtype())); + if(!new ServerStarter().test(mode, mode.getRandomMap(), checker.getPlayer()).check(schematic.getId()).start()) { + remove(); + return; + } + + currentCheckers.put(checker.user().getUUID(), this); + currentSchems.put(schematic.getId(), this); + for(CheckedSchematic previous : CheckedSchematic.previousChecks(schematic)) + checker.prefixless("CHECK_SCHEMATIC_PREVIOUS", previous.getEndTime(), SteamwarUser.get(previous.getValidator()).getUserName(), previous.getDeclineReason()); + next(); + }).schedule(); + } + + private void next() { + if(!checkList.hasNext()){ + accept(); + return; + } + + checker.prefixless("PLAIN_STRING", checkList.next()); + + checker.sendMessage(Component + .text(checker.parseToPlain(checkList.hasNext() ? "CHECK_NEXT" : "CHECK_ACCEPT")) + .color(NamedTextColor.GREEN) + .clickEvent(ClickEvent.runCommand("/check next")) + .append(Component + .text(" " + checker.parseToPlain("CHECK_DECLINE")) + .color(NamedTextColor.RED) + .clickEvent(ClickEvent.suggestCommand("/check decline ")))); + } + + private void accept(){ + if(concludeCheckSession("freigegeben", fightTypes.get(schematic.getSchemtype()))) { + Chatter owner = Chatter.of(SteamwarUser.get(schematic.getOwner()).getUUID()); + owner.withPlayerOrOffline( + player -> owner.system("CHECK_ACCEPTED", schematic.getSchemtype().name(), schematic.getName()), + () -> DiscordAlert.send(owner, Color.GREEN, new Message("DC_TITLE_SCHEMINFO"), new Message("DC_SCHEM_ACCEPT", schematic.getName()), true) + ); + notifyTeam(new Message("CHECK_ACCEPTED_TEAM", schematic.getName(), owner.user().getUserName())); + } + } + + private void decline(String reason){ + if(concludeCheckSession(reason, SchematicType.Normal)) { + Chatter owner = Chatter.of(SteamwarUser.get(schematic.getOwner()).getUUID()); + owner.withPlayerOrOffline( + player -> owner.system("CHECK_DECLINED", schematic.getSchemtype().name(), schematic.getName(), reason), + () -> DiscordAlert.send(owner, Color.RED, new Message("DC_TITLE_SCHEMINFO"), new Message("DC_SCHEM_DECLINE", schematic.getName(), reason), false) + ); + notifyTeam(new Message("CHECK_DECLINED_TEAM", schematic.getName(), owner.user().getUserName(), reason)); + } + } + + private void notifyTeam(Message message) { + Chatter.serverteam().system(message); + DiscordBot.withBot(bot -> bot.getChecklistChannel().system(message)); + } + + private void abort(){ + concludeCheckSession("Prüfvorgang abgebrochen", null); + } + + private boolean concludeCheckSession(String reason, SchematicType type) { + if(SchematicNode.getSchematicNode(schematic.getId()) == null) // Schematic was deleted + return false; + + CheckedSchematic.create(schematic, checker.user().getId(), startTime, Timestamp.from(Instant.now()), reason); + if(type != null) + schematic.setSchemtype(type); + + remove(); + VelocityCore.schedule(() -> { + Bauserver subserver = Bauserver.get(checker.user().getUUID()); + if(subserver != null) + subserver.stop(); + }).schedule(); + return true; + } + + private void remove() { + currentCheckers.remove(checker.user().getUUID()); + currentSchems.remove(schematic.getId()); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/DevCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/DevCommand.java new file mode 100644 index 00000000..005e5fe8 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/DevCommand.java @@ -0,0 +1,127 @@ +/* + * 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; +import de.steamwar.messages.Message; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.command.SWCommand; +import de.steamwar.command.SWCommandUtils; +import de.steamwar.command.TypeMapper; +import de.steamwar.command.TypeValidator; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.Punishment; +import de.steamwar.sql.SteamwarUser; + +import java.io.File; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; + +public class DevCommand extends SWCommand { + + private final File devServerDir = new File("/configs/DevServer"); + private final Map devServers = new HashMap<>(); + + public DevCommand() { + super("dev"); + } + + @Register + public void simpleCommand(@Validator PlayerChatter sender) { + updateDevServers(); + if (devServers.isEmpty()) { + sender.system("DEV_NO_SERVER"); + return; + } else if (devServers.size() == 1) { + devServers.values().stream().findAny().ifPresent(server -> sender.getPlayer().createConnectionRequest(server).fireAndForget()); + return; + } + + RegisteredServer server = devServers.get(sender.getPlayer().getUsername().toLowerCase()); + if (server == null) { + sender.system("DEV_UNKNOWN_SERVER"); + return; + } + + sender.getPlayer().createConnectionRequest(server).fireAndForget(); + } + + @Register + public void selectedCommand(@Validator PlayerChatter sender, @Mapper("dev") String name) { + updateDevServers(); + RegisteredServer server = devServers.get(name.toLowerCase()); + if (server == null) { + sender.system("DEV_NO_SERVER"); + return; + } + + sender.getPlayer().createConnectionRequest(server).fireAndForget(); + } + + @ClassValidator(value = PlayerChatter.class, local = true) + public TypeValidator punishmentGuardChecker() { + return (sender, value, messageSender) -> { + if (sender.user().isPunished(Punishment.PunishmentType.NoDevServer)) { + Message message = PunishmentCommand.punishmentMessage(sender.user(), Punishment.PunishmentType.NoDevServer); + messageSender.send(message.format(), message.params()); + return false; + } + return true; + }; + } + + @Mapper(value = "dev", local = true) + public TypeMapper devServers() { + return SWCommandUtils.createMapper(s -> s, s -> { + updateDevServers(); + return devServers.keySet(); + }); + } + + private void updateDevServers() { + String[] serverFiles = devServerDir.list(); + + Map devServerFiles = new HashMap<>(); + if (serverFiles != null) { + for (String serverFile : serverFiles) { + String[] server = serverFile.split("\\."); + devServerFiles.put(server[0], Integer.parseInt(server[1])); + } + } + + devServers.entrySet().removeIf(entry -> { + if (!devServerFiles.containsKey(entry.getKey())) { + VelocityCore.getProxy().unregisterServer(entry.getValue().getServerInfo()); + return true; + } + return false; + }); + + devServerFiles.forEach((key, value) -> { + if (devServers.containsKey(key)) + return; + + SteamwarUser user = SteamwarUser.get(key); + devServers.put(user.getUserName().toLowerCase(), VelocityCore.getProxy().registerServer(new ServerInfo("Dev " + user.getUserName(), new InetSocketAddress("127.0.0.1", value)))); + }); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/EventCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/EventCommand.java new file mode 100644 index 00000000..df90c02b --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/EventCommand.java @@ -0,0 +1,117 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.velocitycore.EventStarter; +import de.steamwar.persistent.Subserver; +import de.steamwar.velocitycore.SubserverSystem; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeValidator; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.*; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class EventCommand extends SWCommand { + + public EventCommand() { + super("event"); + } + + @Validator("noEvent") + public TypeValidator noEventValidator() { + return (sender, value, messageSender) -> Event.get() == null; + } + + @Register + public void noCurrentEvent(@Validator("noEvent") Chatter sender) { + sender.system("EVENT_NO_CURRENT"); + + List coming = Event.getComing(); + if(coming.isEmpty()) + return; + + sender.system("EVENT_COMING"); + + Instant now = Instant.now(); + DateTimeFormatter format = DateTimeFormatter.ofPattern(sender.parseToPlain("EVENT_DATE_FORMAT")); + for(Event e : coming){ + sender.prefixless("EVENT_COMING_EVENT", e.getStart().toLocalDateTime().format(format), e.getEnd().toLocalDateTime().format(format), e.getEventName()); + + if(now.isBefore(e.getDeadline().toInstant())) + sender.prefixless("EVENT_COMING_DEADLINE", e.getDeadline()); + + SchematicType schemType = e.getSchematicType(); + if (schemType != null && schemType.getDeadline() != null && now.isBefore(schemType.getDeadline().toInstant())) + sender.prefixless("EVENT_COMING_SCHEM_DEADLINE", schemType.getDeadline()); + + Set teams = TeamTeilnahme.getTeams(e.getEventID()); + if(!teams.isEmpty()) + sender.prefixless("EVENT_COMING_TEAMS", teams.stream().map(team -> sender.parseToLegacy("EVENT_COMING_TEAM", team.getTeamColor(), team.getTeamKuerzel())).collect(Collectors.joining(" "))); + } + } + + @Register + public void eventOverview(@Validator(value = "noEvent", invert = true) Chatter sender) { + sender.system("EVENT_USAGE"); + + Event currentEvent = Event.get(); + sender.system("EVENT_CURRENT_EVENT", currentEvent.getEventName()); + + DateTimeFormatter format = DateTimeFormatter.ofPattern(sender.parseToPlain("EVENT_TIME_FORMAT")); + for(EventFight fight : EventFight.getEvent(currentEvent.getEventID())){ + Team blue = Team.get(fight.getTeamBlue()); + Team red = Team.get(fight.getTeamRed()); + StringBuilder fline = new StringBuilder(sender.parseToLegacy("EVENT_CURRENT_FIGHT", fight.getStartTime().toLocalDateTime().format(format), blue.getTeamColor(), blue.getTeamKuerzel(), red.getTeamColor(), red.getTeamKuerzel())); + + if(fight.hasFinished()){ + switch(fight.getErgebnis()){ + case 1: + fline.append(sender.parseToLegacy("EVENT_CURRENT_FIGHT_WIN", blue.getTeamColor(), blue.getTeamKuerzel())); + break; + case 2: + fline.append(sender.parseToLegacy("EVENT_CURRENT_FIGHT_WIN", red.getTeamColor(), red.getTeamKuerzel())); + break; + default: + fline.append(sender.parseToLegacy("EVENT_CURRENT_FIGHT_DRAW")); + } + } + + sender.prefixless("PLAIN_STRING", fline.toString()); + } + } + + @Register + public void eventWithTeam(@Validator(value = "noEvent", invert = true) PlayerChatter sender, @ErrorMessage("EVENT_NO_TEAM") Team team) { + Subserver eventArena = EventStarter.getEventServer().get(team.getTeamId()); + if(eventArena == null || !Subserver.getServerList().contains(eventArena)){ + sender.system("EVENT_NO_FIGHT_TEAM"); + return; + } + if (!PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.NoFightServer)) { + SubserverSystem.sendPlayer(eventArena, sender.getPlayer()); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/EventRescheduleCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/EventRescheduleCommand.java new file mode 100644 index 00000000..d639abd4 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/EventRescheduleCommand.java @@ -0,0 +1,66 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.Event; +import de.steamwar.sql.EventFight; +import de.steamwar.sql.Team; +import de.steamwar.sql.UserPerm; + +import java.sql.Timestamp; +import java.util.Date; +import java.util.List; +import java.util.ListIterator; + +public class EventRescheduleCommand extends SWCommand { + + public EventRescheduleCommand() { + super("eventreschedule", UserPerm.MODERATION); + } + + @Register + public void reschedule(Chatter sender, Team teamBlue, Team teamRed) { + Event event = Event.get(); + if(event == null){ + sender.system("EVENTRESCHEDULE_UNKNOWN_TEAM"); + return; + } + + List fights = EventFight.getEvent(event.getEventID()); + ListIterator it = fights.listIterator(fights.size()); + Timestamp now = Timestamp.from(new Date().toInstant()); + while(it.hasPrevious()){ + EventFight fight = it.previous(); + if(fight.getStartTime().after(now)) + continue; + + if(fight.getTeamBlue() == teamBlue.getTeamId() && fight.getTeamRed() == teamRed.getTeamId()){ + sender.system("EVENTRESCHEDULE_STARTING"); + fight.reschedule(); + EventFight.loadAllComingFights(); + return; + } + } + + sender.system("EVENTRESCHEDULE_NO_FIGHT"); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/EventreloadCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/EventreloadCommand.java new file mode 100644 index 00000000..45aa61b8 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/EventreloadCommand.java @@ -0,0 +1,36 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.EventFight; +import de.steamwar.sql.UserPerm; + +public class EventreloadCommand extends SWCommand { + public EventreloadCommand() { + super("eventreload", UserPerm.MODERATION); + } + + @Register + public void execute(Chatter sender) { + EventFight.loadAllComingFights(); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/FightCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/FightCommand.java new file mode 100644 index 00000000..1d5283ea --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/FightCommand.java @@ -0,0 +1,141 @@ +/* + 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.*; +import de.steamwar.velocitycore.inventory.SWInventory; +import de.steamwar.velocitycore.inventory.SWItem; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.persistent.Arenaserver; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + +public class FightCommand extends SWCommand { + + public FightCommand() { + super("fight", "f"); + } + + private static void getModes(Chatter sender, String precommand, boolean historic){ + Component start = Component.empty(); + for(ArenaMode mode : ArenaMode.getAllModes()){ + if(mode.withoutChatName() || mode.isHistoric() != historic) + continue; + + String command = precommand + mode.getChatName(); + start = start.append(Component + .text(mode.getChatName() + " ") + .color(NamedTextColor.GRAY) + .decorate(TextDecoration.BOLD) + .hoverEvent(HoverEvent.showText(Component.text(("§e" + command)))) + .clickEvent(ClickEvent.runCommand(command))); + } + + sender.sendMessage(start); + } + + private static boolean alreadyInArena(PlayerChatter sender){ + Subserver subserver = Subserver.getSubserver(sender.getPlayer()); + if(subserver != null && subserver.getType() == Servertype.ARENA){ + sender.system("FIGHT_IN_ARENA"); + return true; + } + + return false; + } + + static void createArena(PlayerChatter sender, String precommand, boolean allowMerging, ArenaMode arenaMode, String map, boolean historic, FightCallback callback) { + if(alreadyInArena(sender)) + return; + + if (arenaMode == null) { + getModes(sender, precommand, historic); + return; + } + + if (map == null) + map = arenaMode.getRandomMap(); + + if (!allowMerging) { + callback.run(sender, arenaMode, map); + } else { + suggestMerging(sender, arenaMode, map, callback); + } + } + + private static void suggestMerging(PlayerChatter sender, ArenaMode mode, String map, FightCallback declineMerge) { + Arenaserver mergable = null; + synchronized (Subserver.getServerList()) { + for (Subserver subserver : Subserver.getServerList()) { + if(subserver instanceof Arenaserver arenaserver && + (mode.getInternalName().equals(arenaserver.getMode()) && arenaserver.isAllowMerge() && arenaserver.getRegisteredServer().getPlayersConnected().size() == 1)) { + mergable = arenaserver; + break; + } + } + } + + if(mergable == null) { + declineMerge.run(sender, mode, map); + return; + } + + SWInventory inventory = new SWInventory(sender, 9, new Message("FIGHT_MERGE_TITLE")); + inventory.addItem(0, new SWItem(new Message("FIGHT_MERGE_DECLINE"), 1), click -> { + inventory.close(); + declineMerge.run(sender, mode, map); + }); + Arenaserver finalMergable = mergable; + SWItem item = new SWItem(new Message("FIGHT_MERGE_INFO", mode.getGameName(), finalMergable.getMap()), 11); + item.addLore(new Message("FIGHT_MERGE_INFO_LORE_1", finalMergable.getRegisteredServer().getPlayersConnected().toArray(new Player[1])[0].getUsername())); + inventory.addItem(4, item, click -> {}); + inventory.addItem(8, new SWItem(new Message("FIGHT_MERGE_ACCEPT"), 10), click -> { + if(Subserver.getServerList().contains(finalMergable)) { + finalMergable.sendPlayer(sender.getPlayer()); + } else { + sender.system("FIGHT_MERGE_OFFLINE"); + declineMerge.run(sender, mode, map); + } + }); + inventory.open(); + } + + @Register + public void fight(@Validator("arenaPlayer") PlayerChatter sender, @Mapper("nonHistoricArenaMode") @OptionalValue("") @AllowNull ArenaMode arenaMode, @Mapper("arenaMap") @OptionalValue("") @AllowNull String map) { + createArena(sender, "/fight ", true, arenaMode, map, false, + (p, mode, m) -> new ServerStarter().arena(mode, m).blueLeader(p.getPlayer()).callback( + arena -> Chatter.broadcast().system("FIGHT_BROADCAST", new Message("FIGHT_BROADCAST_HOVER"), ClickEvent.runCommand("/arena " + arena.getServer().getName()), mode.getGameName(), p.getPlayer().getUsername()) + ).start() + ); + } + + public interface FightCallback { + void run(PlayerChatter player, ArenaMode mode, String map); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java b/VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java new file mode 100644 index 00000000..e54a7ffa --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java @@ -0,0 +1,284 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.commands; + +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.sql.internal.Statement; + +import java.io.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class GDPRQuery extends SWCommand { + + public GDPRQuery() { + super("gdprquery", UserPerm.ADMINISTRATION); + } + + @Register + public void generate(PlayerChatter sender) { + generate(sender, sender.user()); + } + + @Register + public void generate(Chatter sender, SteamwarUser user) { + VelocityCore.schedule(() -> { + try { + createZip(sender, user); + } catch (IOException e) { + throw new SecurityException("Could not create zip", e); + } + }).schedule(); + } + + private void createZip(Chatter sender, SteamwarUser user) throws IOException { + sender.system("GDPR_STATUS_WEBSITE"); + + ZipOutputStream out = new ZipOutputStream(new FileOutputStream(user.getUserName() + ".zip")); + + copy(getClass().getClassLoader().getResourceAsStream("GDPRQueryREADME.md"), out, "README.md"); + copy(getClass().getClassLoader().getResourceAsStream("GDPRQueryREADME.md"), out, "README.txt"); + + sender.system("GDPR_STATUS_WORLD"); + copyBauwelt(user, out, "/home/minecraft/userworlds/" + user.getUUID().toString(), "BuildWorld12"); + copyBauwelt(user, out, "/home/minecraft/userworlds15/" + user.getId(), "BuildWorld15"); + + sender.system("GDPR_STATUS_INVENTORIES"); + copyPlayerdata(user, out, "/home/minecraft/userworlds", "BuildInventories12"); + copyPlayerdata(user, out, "/home/minecraft/userworlds15", "BuildInventories15"); + + sender.system("GDPR_STATUS_DATABASE"); + sqlCSV(user, out, bannedIPs, "BannedIPs.csv"); + sqlCSV(user, out, bauweltMember, "BuildMember.csv"); + sqlCSV(user, out, bauweltMembers, "BuildMembers.csv"); + sqlCSV(user, out, checkedSchems, "SchematicChecksessions.csv"); + sqlCSV(user, out, userElo, "UserElo.csv"); + sqlCSV(user, out, fights, "Fights.csv"); + sqlCSV(user, out, ignoredPlayers, "IgnoredPlayers.csv"); + sqlCSV(user, out, ignoringPlayers, "IgnoringPlayers.csv"); + sqlCSV(user, out, schematicMember, "SchematicMember.csv"); + sqlCSV(user, out, schematicMembers, "SchematicMembers.csv"); + sqlCSV(user, out, pollAnswers, "PollAnswers.csv"); + sqlCSV(user, out, punishments, "Punishments.csv"); + sqlCSV(user, out, sessions, "Sessions.csv"); + sqlCSV(user, out, userData, "UserData.csv"); + sqlCSV(user, out, personalKits, "PersonalKits.csv"); + sqlCSV(user, out, schematics, "Schematics.csv"); + + personalKits(user, out); + schematics(user, out); + userConfig(user, out); + + sender.system("GDPR_STATUS_LOGS"); + copyLogs(user, out, new File("/logs"), "logs"); + + out.close(); + sender.system("GDPR_STATUS_FINISHED"); + } + + private static final Statement bannedIPs = new Statement("SELECT Timestamp, IP FROM BannedUserIPs WHERE UserID = ?"); + private static final Statement bauweltMember = new Statement("SELECT BauweltID AS Bauwelt, WorldEdit, World FROM BauweltMember WHERE MemberID = ?"); + private static final Statement bauweltMembers = new Statement("SELECT u.UserName AS 'User', m.WorldEdit AS WorldEdit, m.World AS World FROM BauweltMember m INNER JOIN UserData u ON m.MemberID = u.id WHERE m.BauweltID = ?"); + private static final Statement checkedSchems = new Statement("SELECT NodeName AS Schematic, StartTime, EndTime, DeclineReason AS Result FROM CheckedSchematic WHERE NodeOwner = ? ORDER BY StartTime ASC"); + private static final Statement userElo = new Statement("SELECT GameMode, Elo, Season FROM Elo WHERE UserID = ?"); + private static final Statement fights = new Statement("SELECT p.Team AS Team, p.Kit AS Kit, p.Kills AS Kills, p.IsOut AS Died, f.GameMode AS GameMode, f.Server AS Server, f.StartTime AS StartTime, f.Duration AS Duration, (f.BlueLeader = p.UserID) AS IsBlueLeader, (f.RedLeader = p.UserID) AS IsRedLeader, f.Win AS Winner, f.WinCondition AS WinCondition FROM Fight f INNER JOIN FightPlayer p ON f.FightID = p.FightID WHERE p.UserID = ? ORDER BY StartTime ASC"); + private static final Statement ignoredPlayers = new Statement("SELECT u.UserName AS IgnoredPlayer FROM IgnoredPlayers i INNER JOIN UserData u ON i.Ignored = u.id WHERE Ignorer = ?"); + private static final Statement ignoringPlayers = new Statement("SELECT Ignorer AS IgnoringPlayers FROM IgnoredPlayers WHERE Ignored = ?"); + private static final Statement schematicMember = new Statement("SELECT s.NodeName AS SchematicName, u.UserName AS SchematicOwner FROM NodeMember m INNER JOIN SchematicNode s ON m.NodeId = s.NodeId INNER JOIN UserData u ON s.NodeOwner = u.id WHERE m.UserId = ?"); + private static final Statement schematicMembers = new Statement("SELECT s.NodeName AS SchematicName, u.UserName AS Member FROM NodeMember m INNER JOIN SchematicNode s ON m.NodeId = s.NodeId INNER JOIN UserData u ON m.UserId = u.id WHERE s.NodeOwner = ?"); + private static final Statement pollAnswers = new Statement("SELECT Question, Answer FROM PollAnswer WHERE UserID = ?"); + private static final Statement punishments = new Statement("SELECT Type, StartTime, EndTime, Perma, Reason FROM Punishments WHERE UserId = ?"); + private static final Statement sessions = new Statement("SELECT StartTime, EndTime FROM Session WHERE UserID = ?"); + private static final Statement userData = new Statement("SELECT * FROM UserData WHERE id = ?"); + private static final Statement personalKits = new Statement("SELECT GameMode, Name, InUse FROM PersonalKit WHERE UserID = ?"); + private static final Statement personalKitData = new Statement("SELECT GameMode, Name, Inventory, Armor FROM PersonalKit WHERE UserID = ?"); + private static final Statement schematics = new Statement("SELECT NodeName AS SchematicName, ParentNode, LastUpdate, NodeItem, NodeType, NodeRank FROM SchematicNode WHERE NodeOwner = ?"); + private static final Statement schematicData = new Statement("SELECT NodeName, ParentNode, NodeFormat, NodeData FROM SchematicNode WHERE NodeOwner = ?"); + private static final Statement userConfig = new Statement("SELECT * FROM UserConfig WHERE User = ?"); + + private void sqlCSV(SteamwarUser user, ZipOutputStream out, Statement statement, String path) throws IOException { + write(stream -> statement.select(rs -> { + try { + OutputStreamWriter writer = new OutputStreamWriter(stream); + int columns = rs.getMetaData().getColumnCount(); + + for(int i = 1; i <= columns; i++) { + writer.write(rs.getMetaData().getColumnLabel(i)); + writer.write(";"); + } + writer.write("\n"); + + while(rs.next()) { + for(int i = 1; i <= columns; i++) { + try { + writer.write(rs.getString(i)); + } catch (NullPointerException e) { + // ignored + } + writer.write(";"); + } + writer.write("\n"); + } + writer.flush(); + } catch (IOException e) { + throw new SecurityException("Could not write file", e); + } + return null; + }, user.getId()), out, path); + } + + private void personalKits(SteamwarUser user, ZipOutputStream out) { + personalKitData.select(rs -> { + while(rs.next()) { + try { + String path = "PersonalKit/" + rs.getString("GameMode") + "/" + rs.getString("Name"); + try(InputStream data = rs.getBinaryStream("Inventory")) { + copy(data, out, path + ".Inventory.yml"); + } + try(InputStream data = rs.getBinaryStream("Armor")) { + copy(data, out, path + ".Armor.yml"); + } + } catch (IOException e) { + throw new SecurityException("Could not export PersonalKits", e); + } + } + return null; + }, user.getId()); + } + + private void schematics(SteamwarUser user, ZipOutputStream out) { + schematicData.select(rs -> { + while(rs.next()) { + String name = (rs.getString("ParentNode") != null ? rs.getString("ParentNode") : "") + ":" + rs.getString("NodeName"); + boolean format = rs.getBoolean("NodeFormat"); + try(InputStream data = rs.getBinaryStream("NodeData")) { + copy(data, out, "Schematics/" + name + (format ? ".schem" : ".schematic")); + } catch (IOException e) { + throw new SecurityException("Could not export Schematic", e); + } + } + return null; + }, user.getId()); + } + + private void userConfig(SteamwarUser user, ZipOutputStream out) { + userConfig.select(rs -> { + while(rs.next()) { + String name = rs.getString("Config"); + try(InputStream data = rs.getBinaryStream("Value")) { + copy(data, out, name + ".yapion"); + } catch (IOException e) { + throw new SecurityException("Could not export UserConfig", e); + } + } + return null; + }, user.getId()); + } + + private void copyLogs(SteamwarUser user, ZipOutputStream out, File log, String outFile) throws IOException { + if (log.isDirectory()) { + for(File logfile : log.listFiles()) { + copyLogs(user, out, logfile, outFile + "/" + logfile.getName().replace(".gz", "")); + } + } else { + Process reader = new ProcessBuilder("zgrep", "^.*" + user.getUserName() + "\\( issued server command:\\| moved too quickly!\\| executed command:\\| lost connection:\\||\\|»\\|\\[\\|\\]\\).*$", log.getPath()).start(); + copy(reader.getInputStream(), out, outFile); + } + } + + private void copyBauwelt(SteamwarUser user, ZipOutputStream out, String inDir, String outDir) throws IOException { + File world = new File(inDir); + if(!world.exists()) + return; + + copy(new File(world, "level.dat"), out, outDir + "/level.dat"); + + File region = new File(world, "region"); + for(File regionfile : region.listFiles()) { + copy(regionfile, out, outDir + "/region/" + regionfile.getName()); + } + + File poi = new File(world, "poi"); + if(poi.exists()) { + for(File regionfile : poi.listFiles()) { + copy(regionfile, out, outDir + "/poi/" + regionfile.getName()); + } + } + + File playerdata = new File(world, "playerdata/" + user.getUUID().toString() + ".dat"); + if(playerdata.exists()) + copy(playerdata, out, outDir + "/playerdata/" + user.getUUID().toString() + ".dat"); + } + + private void copyPlayerdata(SteamwarUser user, ZipOutputStream out, String inDir, String outDir) throws IOException { + File worlds = new File(inDir); + String path = "playerdata/" + user.getUUID().toString() + ".dat"; + + int i = 0; + for(File world : worlds.listFiles()) { + File playerdata = new File(world, path); + if(!playerdata.exists()) + continue; + + copy(playerdata, out, outDir + "/" + (i++) + "/" + user.getUUID().toString() + ".dat"); + } + } + + private void copy(File file, ZipOutputStream out, String path) throws IOException { + try(FileInputStream in = new FileInputStream(file)) { + copy(in, out, path); + } + } + + private void copy(InputStream in, ZipOutputStream out, String path) throws IOException { + boolean initialized = false; + + int bytes; + for(byte[] buf = new byte[8192]; (bytes = in.read(buf)) > 0; ) { + if(!initialized) { + ZipEntry entry = new ZipEntry(path); + out.putNextEntry(entry); + initialized = true; + } + + out.write(buf, 0, bytes); + } + + if(initialized) { + out.closeEntry(); + } + } + + private void write(Writer writer, ZipOutputStream out, String path) throws IOException { + ZipEntry entry = new ZipEntry(path); + out.putNextEntry(entry); + writer.accept(out); + out.closeEntry(); + } + + private interface Writer { + void accept(OutputStream stream) throws IOException; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/HelpCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/HelpCommand.java new file mode 100644 index 00000000..8cb6fda4 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/HelpCommand.java @@ -0,0 +1,117 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import net.kyori.adventure.text.event.ClickEvent; + +import java.util.function.Function; + +public class HelpCommand extends SWCommand { + + public HelpCommand() { + super("help", "?"); + } + + @Register + public void genericCommand(Chatter sender) { + printPage(sender, ClickEvent::runCommand, + "HELP_LOBBY", "/l", + "HELP_BAU", "/build", + "HELP_BAUSERVER", "/help build", + "HELP_FIGHT", "/fight", + "HELP_CHALLENGE", "/challenge", + "HELP_HISTORIC", "/historic", + "HELP_TEAM", "/team", + "HELP_JOIN", "/join", + "HELP_LOCAL", "/local"); + } + + @Register({"build","world"}) + public void buildWorld(Chatter sender) { + printPage(sender, "HELP_BAU_GROUP_WORLD_TITLE", "HELP_TNT", "HELP_FIRE", "HELP_FREEZE", "HELP_TPSLIMIT", "HELP_PROTECT", "HELP_RESET"); + } + + @Register({"build","player"}) + public void buildPlayer(Chatter sender) { + printPage(sender, "HELP_BAU_GROUP_PLAYER_TITLE", "HELP_SPEED", "HELP_NV", "HELP_DEBUGSTICK", "HELP_TRACE", "HELP_LOADER"); + } + + @Register({"build","worldedit"}) + @Register({"build","we"}) + @Register({"build","world-edit"}) + @Register({"build","edit"}) + public void buildWorldedit(Chatter sender) { + printPage(sender, "HELP_BAU_GROUP_WE_TITLE", "HELP_WE_POS1", "HELP_WE_POS2", "HELP_WE_COPY", "HELP_WE_PASTE", "HELP_WE_FLOPY", "HELP_WE_FLOPYP", "HELP_WE_ROTATE_90", "HELP_WE_ROTATE_180", "HELP_WE_ROTATE_N90"); + } + + @Register({"build","other"}) + public void buildOther(Chatter sender) { + printPage(sender, "HELP_BAU_GROUP_OTHER_TITLE", "HELP_TESTBLOCK", "HELP_SKULL", "HELP_BAUINFO"); + sender.prefixless("HELP_SCHEMSUBMIT", new Message("HELP_SCHEMSUBMIT_HOVER"), ClickEvent.openUrl("https://www.youtube.com/watch?v=9QrQ3UBWveE")); + } + + @Register("build") + public void sendBauHelpGroup(Chatter sender) { + printPage(sender, ClickEvent::runCommand, + "HELP_BAU_GROUP_ADMIN", "/help build admin", + "HELP_BAU_GROUP_WORLD", "/help build world", + "HELP_BAU_GROUP_PLAYER", "/help build player", + "HELP_BAU_GROUP_WE", "/help build we", + "HELP_BAU_GROUP_OTHER", "/help build other"); + } + + @Register("buildserver") + @Register({"build","admin"}) + @Register({"build","owner"}) + @Register({"build","bauwelt"}) + public void sendBauHelp(Chatter sender) { + printPage(sender, ClickEvent::suggestCommand, + "HELP_BAU_TP", "/build tp ", + "HELP_BAU_ADDMEMBER", "/build addmember ", + "HELP_BAU_DELMEMBER", "/build delmember ", + "HELP_BAU_SET_SPECTATOR", "/build setSpectator ", + "HELP_BAU_SET_BUILDER", "/build setBuilder ", + "HELP_BAU_SET_SUPERVISOR", "/build setSupervisor ", + "HELP_BAU_DELETE", "/build delete ", + "HELP_BAU_TESTARENA", "/build testarena ", + "HELP_BAU_LOCK", "/build lock ", + "HELP_BAU_UNLOCK", "/build unlock"); + } + + private static void printPage(Chatter sender, Function action, String... args) { + for(int i = 0; i < args.length; i += 2) { + String message = args[i]; + String hoverMessage = message + "_HOVER"; + String command = args[i+1]; + + sender.system(message, new Message(hoverMessage), action.apply(command)); + } + } + + private static void printPage(Chatter sender, String title, String... messages) { + sender.system(title); + for (String message : messages) { + sender.prefixless(message); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/HistoricCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/HistoricCommand.java new file mode 100644 index 00000000..7176da84 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/HistoricCommand.java @@ -0,0 +1,41 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.velocitycore.ServerStarter; +import net.kyori.adventure.text.event.ClickEvent; + +public class HistoricCommand extends SWCommand { + public HistoricCommand() { + super("historic"); + } + + @Register + public void historic(@Validator("arenaPlayer") PlayerChatter player, @Mapper("historicArenaMode") @OptionalValue("") @AllowNull ArenaMode arenaMode, @Mapper("arenaMap") @OptionalValue("") @AllowNull String map) { + FightCommand.createArena(player, "/historic ", true, arenaMode, map, true, (p, mode, m) -> new ServerStarter().arena(mode, m).blueLeader(p.getPlayer()).callback( + arena -> Chatter.broadcast().system("HISTORIC_BROADCAST", new Message("HISTORIC_BROADCAST_HOVER"), ClickEvent.runCommand("/arena " + arena.getServer().getName()), mode.getGameName(), p.getPlayer()) + ).start()); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/IgnoreCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/IgnoreCommand.java new file mode 100644 index 00000000..684ab5a6 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/IgnoreCommand.java @@ -0,0 +1,49 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.IgnoreSystem; +import de.steamwar.sql.SteamwarUser; + +public class IgnoreCommand extends SWCommand { + + public IgnoreCommand() { + super("ignore"); + } + + @Register(description = "USAGE_IGNORE") + public void genericCommand(Chatter player, @ErrorMessage("UNKNOWN_PLAYER") SteamwarUser target) { + SteamwarUser user = player.user(); + + if(target.equals(user)){ + player.system("IGNORE_YOURSELF"); + return; + } + if(IgnoreSystem.isIgnored(user, target)){ + player.system("IGNORE_ALREADY"); + return; + } + + IgnoreSystem.ignore(user, target); + player.system("IGNORE_MESSAGE", target); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/JoinmeCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/JoinmeCommand.java new file mode 100644 index 00000000..f6b4262f --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/JoinmeCommand.java @@ -0,0 +1,38 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.UserPerm; +import net.kyori.adventure.text.event.ClickEvent; + +public class JoinmeCommand extends SWCommand { + + public JoinmeCommand() { + super("joinme", UserPerm.TEAM); + } + + @Register + public void genericCommand(PlayerChatter sender) { + Chatter.broadcast().system("JOINME_BROADCAST", "JOINME_BROADCAST_HOVER", ClickEvent.runCommand("/join " + sender.getPlayer().getUsername()), sender, sender.getPlayer().getCurrentServer().orElseThrow().getServerInfo().getName()); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/KickCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/KickCommand.java new file mode 100644 index 00000000..4bbd8bef --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/KickCommand.java @@ -0,0 +1,42 @@ +/* + 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.UserPerm; + +public class KickCommand extends SWCommand { + + public KickCommand() { + super("kick", UserPerm.MODERATION); + } + + @Register(description = "KICK_USAGE") + public void genericCommand(Chatter sender, @ErrorMessage("KICK_OFFLINE") Player target, String... message) { + if (message.length == 0) { + Chatter.disconnect(target).system("KICK_NORMAL"); + } else { + Chatter.disconnect(target).system("KICK_CUSTOM", String.join(" ", message)); + } + sender.system("KICK_CONFIRM", target); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ListCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ListCommand.java new file mode 100644 index 00000000..d2659836 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ListCommand.java @@ -0,0 +1,71 @@ +/* + * 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; + +import java.util.ArrayList; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class ListCommand extends SWCommand { + + public ListCommand() { + super("list"); + } + + public static synchronized SortedMap> getCustomTablist(){ + SortedMap> playerMap = new TreeMap<>(); + for (Player player : VelocityCore.getProxy().getAllPlayers()) { + ServerConnection pserver = player.getCurrentServer().orElse(null); + if (pserver == null) + continue; + + Subserver subserver = Subserver.getSubserver(pserver.getServerInfo()); + if (subserver != null && subserver.getType() == Servertype.BAUSERVER) { + playerMap.computeIfAbsent("Bau", s -> new ArrayList<>()).add(player); + } else { + playerMap.computeIfAbsent(pserver.getServerInfo().getName(), s -> new ArrayList<>()).add(player); + } + } + playerMap.forEach((server, players) -> players.sort((player, t1) -> player.getUsername().compareToIgnoreCase(t1.getUsername()))); + return playerMap; + } + + @Register + public void list(Chatter sender) { + SortedMap> playerMap = getCustomTablist(); + for (String server : playerMap.keySet()) { + String serverName = server; + if (server.equals("Bau")) { + serverName = sender.parseToLegacy("TABLIST_BAU"); + } + sender.prefixless("LIST_COMMAND", serverName, playerMap.get(server).stream().map(Player::getUsername).collect(Collectors.joining(", "))); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java new file mode 100644 index 00000000..d53afdb7 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/LocalCommand.java @@ -0,0 +1,36 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.PlayerChatter; + +public class LocalCommand extends SWCommand { + + public LocalCommand() { + super("local", "bc", "bauchat"); + } + + @Register + public void genericCommand(PlayerChatter player, String... message) { + ChatListener.localChat(player, String.join(" ", message)); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ModCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ModCommand.java new file mode 100644 index 00000000..c37a3e7c --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ModCommand.java @@ -0,0 +1,120 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2023 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.velocitycore.commands; + +import de.steamwar.velocitycore.inventory.SWInventory; +import de.steamwar.velocitycore.inventory.SWItem; +import de.steamwar.velocitycore.inventory.SWListInv; +import de.steamwar.velocitycore.inventory.SWStreamInv; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.Mod; +import de.steamwar.sql.UserPerm; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +public class ModCommand extends SWCommand { + + public ModCommand() { + super("mod", UserPerm.MODERATION, "mods"); + } + + public static final Map playerFilterType = new HashMap<>(); + + @Register + public void mods(PlayerChatter sender) { + playerFilterType.putIfAbsent(sender.getPlayer().getUniqueId(), Mod.ModType.UNKLASSIFIED); + openGui(sender); + } + + private void openGui(PlayerChatter player) { + SWStreamInv swStreamInv = new SWStreamInv<>(player, new Message("MOD_COMMAND_GUI_TITLE"), (click, mod) -> openTypeGUI(player, "MOD_COMMAND_CLASSICIATION_GUI", type -> { + mod.setModType(type); + SWInventory.close(player); + openGui(player); + }), page -> { + Mod.ModType filtertype = playerFilterType.get(player.user().getUUID()); + return Mod.getAllModsFiltered(page,45, filtertype).stream().map(mod -> new SWListInv.SWListEntry<>(new SWItem("NAME_TAG", new Message("PLAIN_STRING", mod.getModName())).addLore(new Message("PLAIN_STRING", mod.getPlatform().name())), mod)).toList(); + }); + + swStreamInv.addItem(52, new SWItem("NAME_TAG", new Message("MOD_TITLE_FILTER")), click -> { + swStreamInv.close(); + openTypeGUI(player, "MOD_TITLE_FILTER", type -> playerFilterType.replace(player.user().getUUID(), type)); + }); + + swStreamInv.open(); + } + + private void openTypeGUI(PlayerChatter player, String title, Consumer function) { + SWInventory inv = new SWInventory(player, 9, new Message(title)); + + inv.addItem(1, new SWItem(new Message("MOD_UNCLASSIFIED"), 8), click -> function.accept(Mod.ModType.UNKLASSIFIED)); + inv.addItem(2, new SWItem(new Message("MOD_ALLOWED"), 2), click -> function.accept(Mod.ModType.GREEN)); + inv.addItem(3, new SWItem(new Message("MOD_FORBIDDEN"), 11), click -> function.accept(Mod.ModType.YELLOW)); + inv.addItem(4, new SWItem(new Message("MOD_AUTOBAN"), 1), click -> function.accept(Mod.ModType.RED)); + inv.addItem(5, new SWItem(new Message("MOD_YT"), 13), click -> function.accept(Mod.ModType.YOUTUBER_ONLY)); + + inv.addItem(8, new SWItem("ARROW", new Message("MOD_ITEM_BACK")), click -> { + inv.close(); + openGui(player); + }); + + inv.open(); + } + + @Register(value = {"set"},description = "MOD_COMMAND_SET_USAGE") + public void set(Chatter sender, String modName, Mod.Platform platform, Mod.ModType newModType) { + Mod mod = Mod.get(modName, platform); + if(mod == null) { + sender.system("MOD_COMMAND_NOT_FOUND_IN_DATABASE", modName, platform.name()); + return; + } + + mod.setModType(newModType); + sender.system("MOD_CHANGED_TYPE", modName, platform.name(), newModType.name()); + } + + @Register(value = {"get"},description = "MOD_COMMAND_GET_USAGE") + public void get(Chatter sender, String modName, Mod.Platform platform) { + Mod mod = Mod.get(modName, platform); + if(mod == null) { + sender.system("MOD_COMMAND_NOT_FOUND_IN_DATABASE", modName, platform.name()); + return; + } + + sender.system("MOD_COMMAND_INFO", modName, platform.name(), mod.getModType().name()); + } + + @Register(value = {"next"}) + public void next(Chatter sender) { + Mod mod = Mod.findFirstMod(); + if(mod == null) { + sender.system("MOD_NO_MORE_UNCLASSIFIED_MODS"); + return; + } + + sender.system("MOD_FOUND_NEXT_MOD", mod.getModName(), mod.getPlatform().name()); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java new file mode 100644 index 00000000..8341318f --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/MsgCommand.java @@ -0,0 +1,63 @@ +/* + 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.ChatterGroup; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.IgnoreSystem; + +import static de.steamwar.persistent.Storage.lastChats; + +public class MsgCommand extends SWCommand { + + public MsgCommand() { + super("msg", "w", "tell"); + } + + @Register(description = "MSG_USAGE") + public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "MSG_OFFLINE") Player target, @ErrorMessage(value = "MSG_USAGE", allowEAs = false) String... message) { + msg(sender, target, message); + } + + public static void msg(PlayerChatter sender, Player target, String[] args) { + if(target == null) { + sender.system("MSG_OFFLINE"); + return; + } + + if (IgnoreSystem.isIgnored(target.getUniqueId(), sender.user().getUUID())) { + sender.system("MSG_IGNORED"); + return; + } + + Chatter receiver = Chatter.of(target); + ChatListener.sendChat(sender, new ChatterGroup(sender, receiver), "CHAT_MSG", receiver, String.join(" ", args)); + lastChats.put(sender.getPlayer(), target); + lastChats.put(target, sender.getPlayer()); + } + + public static void remove(Player player){ + lastChats.remove(player); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/PingCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/PingCommand.java new file mode 100644 index 00000000..583f263a --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/PingCommand.java @@ -0,0 +1,35 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.PlayerChatter; + +public class PingCommand extends SWCommand { + + public PingCommand() { + super("ping"); + } + + @Register + public void genericCommand(PlayerChatter sender) { + sender.system("PING_RESPONSE", sender.getPlayer().getPing()); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/PlaytimeCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/PlaytimeCommand.java new file mode 100644 index 00000000..bf04c28a --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/PlaytimeCommand.java @@ -0,0 +1,43 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.SteamwarUser; + +import java.text.NumberFormat; + +public class PlaytimeCommand extends SWCommand { + + public PlaytimeCommand() { + super("playtime"); + } + + @Register + public void genericCommand(Chatter sender) { + SteamwarUser user = sender.user(); + NumberFormat format = NumberFormat.getNumberInstance(user.getLocale()); + format.setMaximumFractionDigits(2); + String formattedText = format.format((user.getOnlinetime() / 3600d)); + + sender.system("HOURS_PLAYED", formattedText); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/PollCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/PollCommand.java new file mode 100644 index 00000000..72646164 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/PollCommand.java @@ -0,0 +1,64 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.velocitycore.listeners.PollSystem; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeValidator; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.PollAnswer; + +public class PollCommand extends SWCommand { + + public PollCommand() { + super("poll"); + } + + @Register + public void genericCommand(@Validator Chatter sender) { + PollSystem.sendPoll(sender); + } + + @Register(noTabComplete = true) + public void answerPoll(@Validator Chatter sender, String answerString) { + int answer; + try { + answer = Integer.parseUnsignedInt(answerString); + if(answer < 1 || answer > PollSystem.answers()) + throw new NumberFormatException(); + }catch(NumberFormatException e){ + sender.system("POLL_NO_ANSWER"); + return; + } + + PollAnswer pollAnswer = PollAnswer.get(sender.user().getId()); + if(pollAnswer.hasAnswered()) + sender.system("POLL_ANSWER_REFRESH"); + else + sender.system("POLL_ANSWER_NEW"); + + pollAnswer.setAnswer(answer); + } + + @ClassValidator(value = Chatter.class, local = true) + public TypeValidator noPoll() { + return PollSystem.noPoll(); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/PollresultCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/PollresultCommand.java new file mode 100644 index 00000000..611fd7ed --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/PollresultCommand.java @@ -0,0 +1,50 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.velocitycore.listeners.PollSystem; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeValidator; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.PollAnswer; +import de.steamwar.sql.UserPerm; + +import java.util.Map; + +public class PollresultCommand extends SWCommand { + + public PollresultCommand() { + super("pollresult", UserPerm.MODERATION); + } + + @Register + public void genericCommand(@Validator Chatter sender) { + Map voted = PollAnswer.getCurrentResults(); + sender.system("POLLRESULT_HEADER", voted.values().stream().reduce(Integer::sum).orElse(0), PollAnswer.getCurrentPoll()); + for (Map.Entry e: voted.entrySet()) { + sender.prefixless("POLLRESULT_LIST", PollSystem.getAnswer(e.getKey()), e.getValue()); + } + } + + @ClassValidator(value = Chatter.class, local = true) + public TypeValidator noPoll() { + return PollSystem.noPoll(); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/PunishmentCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/PunishmentCommand.java new file mode 100644 index 00000000..da847019 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/PunishmentCommand.java @@ -0,0 +1,274 @@ +/* + * 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.velocitycore.commands; + +import com.google.gson.JsonParser; +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.IPSanitizer; +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeMapper; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.BannedUserIPs; +import de.steamwar.sql.Punishment; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; + +import java.io.IOException; +import java.net.URL; +import java.sql.Timestamp; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.logging.Level; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class PunishmentCommand { + + private static final String API_URL = "https://api.mojang.com/users/profiles/minecraft/"; + + public static SteamwarUser getOrCreateOfflinePlayer(String name) { + SteamwarUser user = SteamwarUser.get(name); + if (user != null) { + return user; + } + + UUID uuid = getUUIDofOfflinePlayer(name); + if (uuid == null) { + return null; + } + + return SteamwarUser.getOrCreate(uuid, name, u -> {}, (o, n) -> {}); + } + + private static UUID getUUIDofOfflinePlayer(String playerName) { + try { + final URL url = new URL(API_URL + playerName); + String uuid = JsonParser.parseString(new Scanner(url.openConnection().getInputStream()).nextLine()).getAsJsonObject().get("id").getAsString(); + return UUID.fromString(uuid.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")); + } catch (NoSuchElementException e) { + // ignore, player does not exist + } catch (IOException e) { + VelocityCore.getLogger().log(Level.SEVERE, "Could not get offline player UUID %s".formatted(playerName), e); + } + return null; + } + + public static boolean isPunishedWithMessage(Chatter player, Punishment.PunishmentType punishment) { + SteamwarUser user = player.user(); + if (!user.isPunished(punishment)) { + return false; + } + player.system(punishmentMessage(user, punishment)); + return true; + } + + public static void ban(SteamwarUser user, Timestamp time, String banReason, SteamwarUser punisher, boolean perma) { + Player player = VelocityCore.getProxy().getPlayer(user.getUUID()).orElse(null); + if (player != null) { + String ip = IPSanitizer.getTrueAddress(player).getHostAddress(); + Chatter.disconnect(player).system(punishmentMessage(user, Punishment.PunishmentType.Ban)); + for (BannedUserIPs banned : BannedUserIPs.get(ip)) { + SteamwarUser bannedUser = SteamwarUser.get(banned.getUserID()); + if (bannedUser.isPunished(Punishment.PunishmentType.Ban) && bannedUser.getPunishment(Punishment.PunishmentType.Ban).getEndTime().before(time)) { + bannedUser.punish(Punishment.PunishmentType.Ban, time, banReason, punisher.getId(), perma); + } + } + BannedUserIPs.banIP(user.getId(), ip); + } + } + + public static Message punishmentMessage(SteamwarUser user, Punishment.PunishmentType punishment) { + Punishment currentPunishment = user.getPunishment(punishment); + if (currentPunishment.isPerma()) { + return new Message(punishment.getPlayerMessagePerma(), currentPunishment.getReason()); + } else { + return new Message(punishment.getPlayerMessageUntil(), currentPunishment.getEndTime(), currentPunishment.getReason()); + } + } + + public PunishmentCommand(String command, Punishment.PunishmentType punishmentType) { + new PunishCommand(command, punishmentType); + + if (punishmentType.getUnpunishmentMessage() == null) + return; + + new UnpunishCommand("un" + command, punishmentType); + } + + private static final Pattern RELATIVE_PATTERN = Pattern.compile("([1-9]\\d*[hdwmy])+"); + + public static Timestamp parseTime(Chatter player, String arg) { + if (arg.equalsIgnoreCase("perma")) { + return Punishment.PERMA_TIME; + } else { + if (RELATIVE_PATTERN.matcher(arg).matches()) { + Instant instant = Instant.now(); + StringBuilder st = new StringBuilder(); + for (int i = 0; i < arg.length(); i++) { + char c = arg.charAt(i); + if (c >= '0' && c <= '9') { + st.append(c); + continue; + } + int amount = Integer.parseInt(st.toString()); + st = new StringBuilder(); + switch (c) { + case 'h': + instant = instant.plus(amount, ChronoUnit.HOURS); + break; + case 'd': + instant = instant.plus(amount, ChronoUnit.DAYS); + break; + case 'w': + instant = instant.plus(Duration.ofSeconds(amount * ChronoUnit.WEEKS.getDuration().getSeconds())); + break; + case 'm': + instant = instant.plus(Duration.ofSeconds(amount * ChronoUnit.MONTHS.getDuration().getSeconds())); + break; + case 'y': + instant = instant.plus(Duration.ofSeconds(amount * ChronoUnit.YEARS.getDuration().getSeconds())); + break; + } + } + return Timestamp.from(instant); + } + SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy_HH:mm"); + try { + Date parsedDate = dateFormat.parse(arg); + return new java.sql.Timestamp(parsedDate.getTime()); + } catch (ParseException e) { + dateFormat = new SimpleDateFormat("dd.MM.yyyy"); + try { + Date parsedDate = dateFormat.parse(arg.split("_")[0]); + return new java.sql.Timestamp(parsedDate.getTime()); + } catch (ParseException exception) { + player.system("INVALID_TIME"); + return null; + } + } + } + } + + private static class PunishCommand extends SWCommand { + + private final String command; + private final Punishment.PunishmentType punishmentType; + + private PunishCommand(String command, Punishment.PunishmentType punishmentType) { + super(command, UserPerm.TEAM); + this.command = command; + this.punishmentType = punishmentType; + } + + private SteamwarUser unsafeUser(Chatter sender, String arg) { + SteamwarUser target = getOrCreateOfflinePlayer(arg); + if(target == null) + sender.system("UNKNOWN_PLAYER"); + return target; + } + + @Register + public void genericCommand(Chatter sender, @Mapper("toPunish") String toPunish, String date, @ErrorMessage(allowEAs = false, value = "PUNISHMENT_USAGE_REASON") String... message) { + SteamwarUser punisher = sender.user(); + if (punishmentType.isNeedsAdmin() && !punisher.hasPerm(UserPerm.MODERATION)) + return; + + SteamwarUser target = unsafeUser(sender, toPunish); + if (target == null) + return; + + Timestamp banTime = parseTime(sender, date); + if (banTime == null) + return; + + boolean isPerma = date.equalsIgnoreCase("perma"); + String msg = String.join(" ", message); + target.punish(punishmentType, banTime, msg, punisher.getId(), isPerma); + if(punishmentType == Punishment.PunishmentType.Ban) + ban(target, banTime, msg, punisher, isPerma); + Chatter.serverteam().system(punishmentType.getTeamMessage(), new Message("PREFIX"), target, sender, new Message((isPerma ? "PUNISHMENT_PERMA" : "PUNISHMENT_UNTIL"), banTime), msg); + } + + @Register + public void genericError(Chatter sender, String... args) { + sender.system("PUNISHMENT_USAGE", command); + } + + @Mapper(value = "toPunish", local = true) + public TypeMapper allUsers() { + return new TypeMapper() { + @Override + public String map(Chatter sender, PreviousArguments previousArguments, String s) { + return s; + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + List players = VelocityCore.getProxy().getAllPlayers().stream() + .map(Player::getUsername) + .collect(Collectors.toList()); + players.add(s); + return players; + } + }; + } + } + + private static class UnpunishCommand extends SWCommand { + + private final String command; + private final Punishment.PunishmentType punishmentType; + + private UnpunishCommand(String command, Punishment.PunishmentType punishmentType) { + super(command, UserPerm.TEAM); + this.command = command; + this.punishmentType = punishmentType; + } + + @Register + public void genericCommand(Chatter sender, @ErrorMessage("UNKNOWN_PLAYER") SteamwarUser target) { + if (punishmentType.isNeedsAdmin() && !sender.user().hasPerm(UserPerm.MODERATION)) + return; + + if (!target.isPunished(punishmentType)) { + sender.system(punishmentType.getUsageNotPunished()); + return; + } + + target.punish(punishmentType, Timestamp.from(new Date().toInstant()), command, sender.user().getId(), false); + if(punishmentType == Punishment.PunishmentType.Ban) + BannedUserIPs.unbanIPs(target.getId()); + + sender.system(punishmentType.getUnpunishmentMessage(), target.getUserName()); + } + + @Register + public void genericError(Chatter sender, String... args) { + sender.system("UNPUNISHMENT_USAGE", command); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java new file mode 100644 index 00000000..8ed4a38b --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/RCommand.java @@ -0,0 +1,37 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.PlayerChatter; + +import static de.steamwar.persistent.Storage.lastChats; + +public class RCommand extends SWCommand { + + public RCommand() { + super("r", "reply"); + } + + @Register(description = "R_USAGE") + public void genericCommand(PlayerChatter sender, @ErrorMessage(value = "R_USAGE", allowEAs = false) String... message) { + MsgCommand.msg(sender, lastChats.get(sender.getPlayer()), message); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/RankCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/RankCommand.java new file mode 100644 index 00000000..5254d52c --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/RankCommand.java @@ -0,0 +1,64 @@ +/* + * 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.velocitycore.commands; + +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.messages.Message; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserElo; + +import java.util.Optional; + +public class RankCommand extends SWCommand { + + public RankCommand() { + super("rank"); + } + + @Register + public void ownRank(Chatter sender) { + rank(sender, sender.user()); + } + + @Register + public void rank(Chatter sender, @ErrorMessage("RANK_PLAYER_NOT_FOUND") SteamwarUser user) { + if (!sender.user().equals(user)) + sender.prefixless("RANK_PLAYER_FOUND", user); + + for(ArenaMode mode : ArenaMode.getAllModes()) { + if (!mode.isRanked()) + continue; + + Optional elo = UserElo.getElo(user.getId(), mode.getSchemType()); + Message eloMsg; + if (elo.isPresent()) { + int placement = UserElo.getPlacement(elo.get(), mode.getSchemType()); + eloMsg = new Message("RANK_PLACED", placement, elo.get()); + } else { + eloMsg = new Message("RANK_UNPLACED"); + } + + sender.prefixless("RANK_HEADER", mode.getChatName(), eloMsg); + sender.prefixless("RANK_EMBLEM", UserElo.getEmblemProgression(mode.getChatName(), user.getId())); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java new file mode 100644 index 00000000..985bf3c5 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ReplayCommand.java @@ -0,0 +1,88 @@ +/* + * 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.velocitycore.commands; + +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.messages.Message; +import de.steamwar.velocitycore.ServerStarter; +import de.steamwar.velocitycore.inventory.SWItem; +import de.steamwar.velocitycore.inventory.SWListInv; +import de.steamwar.velocitycore.inventory.SWStreamInv; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.*; + +import java.util.ArrayList; +import java.util.List; + +public class ReplayCommand extends SWCommand { + + public ReplayCommand() { + super("replay"); + } + + @Register + public void genericCommand(PlayerChatter sender, @OptionalValue("") String optionalMap) { + if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.NoFightServer)) + return; + + new SWStreamInv<>(sender, new Message("REPLAY_TITLE"), (click, fight) -> { + ArenaMode mode = ArenaMode.getBySchemType(fight.getSchemType()); + ServerStarter starter = new ServerStarter().replay(fight.getFightID()).blueLeader(sender.getPlayer()); + + String map = mode.getRandomMap(); + if (!optionalMap.equals("")) { + String tMap = mode.hasMap(optionalMap); + if (tMap != null) map = tMap; + } + + if (sender.user().hasPerm(UserPerm.MODERATION) && click.isShiftClick() && fight.replayExists()) { + starter.test(mode, map, sender.getPlayer()).start(); + } else if(!fight.replayAllowed()) { + sender.system("REPLAY_UNAVAILABLE"); + } else { + starter.arena(mode, map).start(); + } + }, page -> Fight.getPage(page, 45).stream().map(fight -> new SWListInv.SWListEntry<>(getFightItem(fight), fight)).toList()).open(); + } + + private SWItem getFightItem(Fight fight) { + SchematicType type = fight.getSchemType(); + SWItem item = new SWItem(type != null ? type.getMaterial() : "BARRIER", parseLeader(fight.getBlueLeader(), fight.getBluePlayers().size(), fight.getWin() == 1)); + + List lore = new ArrayList<>(); + lore.add(parseLeader(fight.getRedLeader(), fight.getRedPlayers().size(), fight.getWin() == 2)); + lore.add(new Message("REPLAY_TIME", fight.getStartTime())); + lore.add(new Message("SPACER")); + lore.add(new Message("REPLAY_SERVER", fight.getServer())); + if(!fight.replayAllowed()) + lore.add(new Message("REPLAY_UNAVAILABLE")); + item.setLore(lore); + + if(fight.replayAllowed()) + item.setEnchanted(true); + + return item; + } + + private Message parseLeader(SteamwarUser leader, int players, boolean winner) { + return new Message("REPLAY_" + (players > 1 ? "" : "SOLO_") + (winner ? "WINNER" : "LOSER"), leader.getUserName(), players - 1); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/RulesCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/RulesCommand.java new file mode 100644 index 00000000..cf403c4d --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/RulesCommand.java @@ -0,0 +1,41 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import net.kyori.adventure.text.event.ClickEvent; + +import java.util.Arrays; + +public class RulesCommand extends SWCommand { + public RulesCommand() { + super("rules", "regeln"); + } + + @Register + public void genericCommand(Chatter sender) { + sender.system("REGELN_RULES"); + + for(String ruleset : Arrays.asList("REGELN_AS", "REGELN_MWG", "REGELN_WG", "REGELN_WS", "REGELN_QG", "REGELN_CONDUCT")) + sender.prefixless(ruleset, new Message(ruleset + "_HOVER"), ClickEvent.openUrl(sender.parseToPlain(ruleset + "_URL"))); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerSwitchCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerSwitchCommand.java new file mode 100644 index 00000000..142c5f56 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerSwitchCommand.java @@ -0,0 +1,40 @@ +/* + 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.PlayerChatter; + +public class ServerSwitchCommand extends SWCommand { + + private final RegisteredServer server; + + public ServerSwitchCommand(String cmd, String name, String... aliases) { + super(cmd, null, aliases); + server = VelocityCore.getProxy().getServer(name).orElseThrow(); + } + + @Register + public void genericCommand(PlayerChatter sender) { + sender.getPlayer().createConnectionRequest(server).fireAndForget(); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java new file mode 100644 index 00000000..927644ab --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/ServerTeamchatCommand.java @@ -0,0 +1,37 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.UserPerm; + +public class ServerTeamchatCommand extends SWCommand { + + public ServerTeamchatCommand() { + super("stc", UserPerm.TEAM, "serverteamchat"); + } + + @Register(description = "STC_USAGE") + public void genericCommand(Chatter sender, @ErrorMessage(value = "STC_USAGE", allowEAs = false) String... message) { + ChatListener.sendChat(sender, Chatter.serverteam(), "CHAT_SERVERTEAM", null, String.join(" ", message)); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/SetLocaleCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/SetLocaleCommand.java new file mode 100644 index 00000000..689485cf --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/SetLocaleCommand.java @@ -0,0 +1,41 @@ +/* + * 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.velocitycore.commands; + +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.network.packets.server.LocaleInvalidationPacket; + +import java.util.Objects; + +public class SetLocaleCommand extends SWCommand { + + public SetLocaleCommand() { + super("setlocale", "setlanguage"); + } + + @Register + public void genericCommand(Chatter sender) { + sender.user().setLocale(Objects.requireNonNull(sender.getLocale()), true); + sender.withPlayer(player -> NetworkSender.send(player, new LocaleInvalidationPacket(sender.user().getId()))); + sender.system("LOCK_LOCALE_CHANGED"); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/StatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/StatCommand.java new file mode 100644 index 00000000..d9694d6c --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/StatCommand.java @@ -0,0 +1,57 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.velocitycore.Node; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.UserPerm; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; + +public class StatCommand extends SWCommand { + + public StatCommand() { + super("stat", UserPerm.ADMINISTRATION, "stats"); + } + + @Register + public void genericCommand(Chatter sender) { + Map serverCount = new HashMap<>(); + try { + Process process = new ProcessBuilder("ps", "x").start(); + new BufferedReader(new InputStreamReader(process.getInputStream())).lines().forEach(s -> { + if (!s.contains("--port")) + return; + serverCount.compute( + s.contains("ssh -L") ? s.substring(s.indexOf("ssh -L") + 6).split(" ")[2] : "sw", + (server, count) -> (count != null ? count : 0) + 1 + ); + }); + } catch (Exception e) { + throw new SecurityException(e.getMessage(), e); + } + + Node.forEach(node -> sender.prefixless("STAT_SERVER", node.getName(), node.belowLoadLimit(), serverCount.getOrDefault(node.getName(), 0))); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java new file mode 100644 index 00000000..32b65e93 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java @@ -0,0 +1,601 @@ +/* + 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.ConnectionRequestBuilder; +import com.velocitypowered.api.proxy.server.ServerInfo; +import de.steamwar.persistent.Storage; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.velocitycore.inventory.SWItem; +import de.steamwar.velocitycore.inventory.SWListInv; +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeMapper; +import de.steamwar.command.TypeValidator; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.net.*; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static de.steamwar.persistent.Storage.teamInvitations; + +public class TeamCommand extends SWCommand { + + public TeamCommand() { + super("team"); + } + + @Register(noTabComplete = true) + public void help(Chatter sender, String... args){ + helpMessages(sender, "TEAM_HELP_HEADER", "TEAM_HELP_LIST", "TEAM_HELP_INFO", "TEAM_HELP_TP"); + + SteamwarUser user = sender.user(); + if(user.getTeam() == 0) { + helpMessages(sender, "TEAM_HELP_CREATE", "TEAM_HELP_JOIN"); + }else{ + helpMessages(sender, "TEAM_HELP_CHAT", "TEAM_HELP_EVENT", "TEAM_HELP_LEAVE"); + + if(user.isLeader()) + helpMessages(sender, "TEAM_HELP_INVITE", "TEAM_HELP_REMOVE", "TEAM_HELP_KUERZEL", "TEAM_HELP_NAME", "TEAM_HELP_COLOR", "TEAM_HELP_LEADER", "TEAM_HELP_STEP_BACK", "TEAM_HELP_SERVER"); + } + } + + private void helpMessages(Chatter sender, String... messages) { + for(String message : messages) + sender.system(message); + } + + @Register(value = "create", description = "TEAM_CREATE_USAGE") + public void create(@Validator("isNotInTeam") Chatter sender, @Length(min = 2, max = 4) @ErrorMessage("TEAM_KUERZEL_LENGTH") String kuerzel, @Length(min = 4, max = 15) @ErrorMessage("TEAM_NAME_LENGTH") String name){ + SteamwarUser user = sender.user(); + Team team = Team.get(user.getTeam()); + + if(checkTeamKuerzel(sender, team, kuerzel) || checkTeamName(sender, team, name)) + return; + + Team.create(kuerzel, name); + user.setTeam(Team.get(kuerzel).getTeamId()); + user.setLeader(true); + sender.system("TEAM_CREATE_CREATED", name); + } + + @Register("join") + public void join(@Validator("isNotInTeam") Chatter sender, String... args){ + SteamwarUser user = sender.user(); + + if(notDuringEvent(sender)) + return; + + if(!teamInvitations.containsKey(user.getId())){ + sender.system("TEAM_JOIN_NO_INVITE"); + return; + } + + List invs = teamInvitations.get(user.getId()); + Integer t = null; + + if(invs.size() == 1){ + t = invs.get(0); + }else{ + if(args.length != 1){ + sender.system("TEAM_JOIN_USAGE"); + StringBuilder sb = new StringBuilder(); + for(int inv : invs){ + Team team = Team.get(inv); + sb.append(team.getTeamName()).append(" "); + } + sender.system("TEAM_JOIN_INVITED", sb.toString()); + return; + } + + for(int inv : invs){ + Team team = Team.get(inv); + if(team.getTeamKuerzel().equalsIgnoreCase(args[0]) || team.getTeamName().equalsIgnoreCase(args[0])){ + t = inv; + break; + } + } + + if(t == null){ + sender.system("TEAM_JOIN_NOT_BY_TEAM"); + return; + } + } + + user.setTeam(t); + teamInvitations.remove(user.getId()); + sender.system("TEAM_JOIN_JOINED", Team.get(t).getTeamName()); + } + + @Register("stepback") + public void stepBack(@Validator("isLeader") Chatter sender) { + SteamwarUser user = sender.user(); + Team team = Team.get(user.getTeam()); + + if(noRemainingLeaders(team, user)){ + sender.system("TEAM_OTHER_LEADER_REQUIRED"); + return; + } + + user.setLeader(false); + sender.system("TEAM_STEP_BACK"); + } + + @Register("leave") + public void leave(@Validator("isInTeam") Chatter sender) { + SteamwarUser user = sender.user(); + Team team = Team.get(user.getTeam()); + + int teamSize = team.size(); + if(teamSize > 1 && user.isLeader() && noRemainingLeaders(team, user)) { + sender.system("TEAM_OTHER_LEADER_REQUIRED"); + return; + } + + user.setTeam(0); + + if(teamSize == 1) + team.disband(user); + + sender.system("TEAM_LEAVE_LEFT"); + } + + private boolean noRemainingLeaders(Team team, SteamwarUser except) { + return SteamwarUser.getTeam(team.getTeamId()).stream().filter(member -> except.getId() != member.getId()).noneMatch(SteamwarUser::isLeader); + } + + + @Register(value = "invite", description = "TEAM_INVITE_USAGE") + public void invite(@Validator("isLeader") Chatter sender, @ErrorMessage("TEAM_INVITE_NO_PLAYER") SteamwarUser target){ + Team team = Team.get(sender.user().getTeam()); + + if(notDuringEvent(sender)) + return; + + if(target.getTeam() != 0){ + sender.system("TEAM_INVITE_IN_TEAM"); + return; + } + + List invitations = teamInvitations.computeIfAbsent(target.getId(), id -> new ArrayList<>()); + if(invitations.contains(team.getTeamId())){ + sender.system("TEAM_INVITE_ALREADY_INVITED"); + return; + } + + invitations.add(team.getTeamId()); + sender.system("TEAM_INVITE_INVITED", target.getUserName()); + Chatter.of(target).system("TEAM_INVITE_INVITED_TARGET", team.getTeamColor(), team.getTeamName()); + } + + @Register(value = "remove", description = "TEAM_REMOVE_USAGE") + public void remove(@Validator("isLeader") Chatter sender, @ErrorMessage("TEAM_REMOVE_NOT_PLAYER") @Mapper("memberList") SteamwarUser target){ + Team team = Team.get(sender.user().getTeam()); + + if (target.isLeader()) { + sender.system("TEAM_REMOVE_NOT_LEADER"); + return; + } + + List invitations = teamInvitations.get(target.getId()); + if(invitations != null) { + sender.system(invitations.remove((Integer) team.getTeamId()) ? "TEAM_REMOVE_INVITE" : "TEAM_REMOVE_NO_INVITE"); + return; + } + + if(target.getTeam() != team.getTeamId()) { + sender.system("TEAM_REMOVE_NOT_IN_TEAM"); + return; + } + + target.setTeam(0); + + sender.system("TEAM_REMOVE_REMOVED"); + Chatter.of(target).system("TEAM_REMOVE_REMOVED_TARGET"); + } + + @Mapper(value = "memberList", local = true) + public TypeMapper memberList() { + return new TypeMapper() { + @Override + public SteamwarUser map(Chatter sender, PreviousArguments previousArguments, String s) { + return SteamwarUser.get(s); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + return Team.get(sender.user().getTeam()).getMembers().stream() + .map(SteamwarUser::get) + .map(SteamwarUser::getUserName) + .toList(); + } + }; + } + + @Register(value = "changekurzel", description = "TEAM_KUERZEL_USAGE") + public void changekuerzel(@Validator("isLeader") Chatter sender, @Length(min = 2, max = 4) @ErrorMessage("TEAM_KUERZEL_LENGTH") String kuerzel){ + Team team = Team.get(sender.user().getTeam()); + + if(notDuringEvent(sender)) + return; + + if(checkTeamKuerzel(sender, team, kuerzel)) + return; + + team.setTeamKuerzel(kuerzel); + sender.system("TEAM_KUERZEL_CHANGED"); + } + + @Register(value = "changename", description = "TEAM_NAME_USAGE") + public void changename(@Validator("isLeader") Chatter sender, @Length(min = 4, max = 15) @ErrorMessage("TEAM_NAME_LENGTH") String name){ + Team team = Team.get(sender.user().getTeam()); + + if(notDuringEvent(sender)) + return; + + if(checkTeamName(sender, team, name)) + return; + + team.setTeamName(name); + sender.system("TEAM_NAME_CHANGED"); + } + + @Register(value = "promote", description = "TEAM_LEADER_USAGE") + public void promote(@Validator("isLeader") Chatter sender, @ErrorMessage("TEAM_LEADER_NOT_USER") @Mapper("memberList") SteamwarUser target){ + if(notDuringEvent(sender)) + return; + + if (target.getTeam() != sender.user().getTeam()) { + sender.system("TEAM_LEADER_NOT_MEMBER"); + return; + } + + target.setLeader(true); + sender.system("TEAM_LEADER_PROMOTED", target); + } + + @Register("info") + public void info(@Validator("isInTeam") Chatter sender){ + info(sender, Team.get(sender.user().getTeam())); + } + + @Register(value = "info", description = "TEAM_INFO_USAGE") + public void info(Chatter sender, @ErrorMessage("UNKNOWN_TEAM") Team team){ + List users = team.getMembers().stream().map(SteamwarUser::get).toList(); + + sender.system("TEAM_INFO_TEAM", team.getTeamName(), team.getTeamColor(), team.getTeamKuerzel()); + sender.prefixless("TEAM_INFO_LEADER", users.stream().filter(SteamwarUser::isLeader).count(), getMemberList(users, true)); + + String members = getMemberList(users, false); + if(!members.isEmpty()) + sender.prefixless("TEAM_INFO_MEMBER", users.stream().filter(u -> !u.isLeader()).count(), members); + + Set events = TeamTeilnahme.getEvents(team.getTeamId()); + if(!events.isEmpty()){ + sender.prefixless("TEAM_INFO_EVENTS", events.stream().map(Event::getEventName).collect(Collectors.joining(", "))); + } + } + + private String getMemberList(List users, boolean leaders) { + return users.stream() + .filter(user -> user.isLeader() == leaders) + .map(user -> (VelocityCore.getProxy().getPlayer(user.getUUID()).isPresent() ? "§a" : "§e") + user.getUserName()) + .collect(Collectors.joining(" ")); + } + + @Register("list") + public void list(Chatter sender, @Min(intValue = 1) @OptionalValue("1") @ErrorMessage("TEAM_LIST_NOT_PAGE") int page){ + final int TEAMS_PER_PAGE = 10; + + List all = Team.getAll(); + final int lastPage = ((all.size() - 1) / 10) + 1; + if(page < 1 || page > lastPage){ + sender.system("TEAM_LIST_UNKNOWN_PAGE"); + return; + } + + sender.system("TEAM_LIST_HEADER", page, lastPage); + + for(int i = (page-1) * TEAMS_PER_PAGE; i < (page-1) * TEAMS_PER_PAGE + TEAMS_PER_PAGE && i < all.size(); i++){ + Team tm = all.get(i); + + sender.prefixless("TEAM_LIST_TEAM", new Message("TEAM_LIST_TEAM_HOVER"), + ClickEvent.runCommand("/team info " + tm.getTeamKuerzel()), tm.getTeamColor(), tm.getTeamKuerzel(), tm.getTeamName()); + } + + Component beforePage = Component + .text("«« ") + .color(page > 1 ? NamedTextColor.YELLOW : NamedTextColor.DARK_GRAY); + if(page > 1) + beforePage = beforePage + .hoverEvent(HoverEvent.showText(sender.parse("TEAM_LIST_PREV"))) + .clickEvent(ClickEvent.runCommand("/team list " + (page - 1))); + + Component nextPage = sender.parse("TEAM_LIST_PAGE") + .color(page > 1 ? NamedTextColor.YELLOW : NamedTextColor.DARK_GRAY); + if(page < lastPage) + nextPage = nextPage + .hoverEvent(HoverEvent.showText(sender.parse("TEAM_LIST_NEXT"))) + .clickEvent(ClickEvent.runCommand("/team list " + (page + 1))); + + sender.sendMessage(beforePage.append(nextPage)); + } + + @Register("event") + public void event(@Validator("isInTeam") Chatter sender) { + Team team = Team.get(sender.user().getTeam()); + + sender.system("TEAM_EVENT_USAGE"); + Set events = TeamTeilnahme.getEvents(team.getTeamId()); + if(!events.isEmpty()){ + sender.system("TEAM_EVENT_HEADER"); + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern(sender.parseToPlain("EVENT_DATE_FORMAT")); + for(Event e : events) + sender.prefixless("TEAM_EVENT_EVENT", e.getStart().toLocalDateTime().format(dateFormat), e.getEventName()); + } + } + + @Register("event") + public void event(@Validator("isLeader") Chatter sender, Event event){ + Team team = Team.get(sender.user().getTeam()); + + if(notDuringEvent(sender)) + return; + + if(Instant.now().isAfter(event.getDeadline().toInstant())){ + sender.system("TEAM_EVENT_OVER"); + return; + } + + if(TeamTeilnahme.nimmtTeil(team.getTeamId(), event.getEventID())){ + TeamTeilnahme.notTeilnehmen(team.getTeamId(), event.getEventID()); + sender.system("TEAM_EVENT_LEFT"); + }else{ + TeamTeilnahme.teilnehmen(team.getTeamId(), event.getEventID()); + sender.system("TEAM_EVENT_JOINED", event.getEventName()); + sender.prefixless("TEAM_EVENT_HOW_TO_LEAVE"); + } + + DiscordBot.withBot(bot -> bot.getEventChannel().update()); + } + + @Register("tp") + public void tp(@Validator("isInTeam") PlayerChatter sender) { + Team team = Team.get(sender.user().getTeam()); + tp(sender, team); + } + + @Register("tp") + public void tp(PlayerChatter sender, @ErrorMessage("TEAM_TP_NO_TEAM") Team targetTeam) { + if (targetTeam.getAddress() == null || targetTeam.getAddress().isEmpty()) { + sender.system("TEAM_NO_ADDRESS"); + return; + } + + InetSocketAddress address = new InetSocketAddress(targetTeam.getAddress(), targetTeam.getPort()); + ServerInfo serverInfo = Storage.teamServers.computeIfAbsent(targetTeam.getTeamId(), integer -> { + ServerInfo info = new ServerInfo("Team " + targetTeam.getTeamKuerzel(), address); + VelocityCore.getProxy().registerServer(info); + return info; + }); + + if (!address.equals(serverInfo.getAddress())) { + VelocityCore.getProxy().unregisterServer(Storage.teamServers.remove(targetTeam.getTeamId())); + tp(sender, targetTeam); + return; + } + + sender.getPlayer().createConnectionRequest(VelocityCore.getProxy().getServer(serverInfo.getName()).orElseThrow()).connect().whenComplete((result, throwable) -> { + if(result.getStatus() != ConnectionRequestBuilder.Status.SUCCESS || throwable != null) + sender.system("TEAM_OFFLINE"); + }); + } + + @Register(value = "server", description = "TEAM_SERVER_USAGE") + public void server(@Validator("isLeader") Chatter sender, String server, @Min(intValue = 1) @Max(intValue = 65535) @ErrorMessage("TEAM_SERVER_PORT_INVALID") int port){ + Team team = Team.get(sender.user().getTeam()); + if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.NoTeamServer)) { + return; + } + + try { + if (isLocalhost(InetAddress.getByName(server))) { + sender.system("TEAM_SERVER_ADDRESS_INVALID"); + return; + } + } catch (UnknownHostException e) { + sender.system("TEAM_SERVER_ADDRESS_INVALID"); + return; + } + + team.setAddress(server); + team.setPort(port); + Storage.teamServers.remove(team.getTeamId()); + sender.system("TEAM_SERVER_SET"); + } + + public static boolean isLocalhost(InetAddress addr) { + // Check if the address is a valid special local or loop back + if (addr.isAnyLocalAddress() || addr.isLoopbackAddress()) + return true; + + // Check if the address is defined on any interface + try { + return NetworkInterface.getByInetAddress(addr) != null; + } catch (SocketException e) { + return false; + } + } + + private static final Map COLOR_CODES = new HashMap<>(); + + static { + COLOR_CODES.put("4", 1); + COLOR_CODES.put("c", 15); + COLOR_CODES.put("6", 14); + COLOR_CODES.put("e", 11); + COLOR_CODES.put("2", 2); + COLOR_CODES.put("a", 10); + COLOR_CODES.put("b", 12); + COLOR_CODES.put("3", 6); + COLOR_CODES.put("1", 4); + COLOR_CODES.put("9", 6); + COLOR_CODES.put("d", 9); + COLOR_CODES.put("5", 5); + COLOR_CODES.put("f", 15); + COLOR_CODES.put("7", 7); + COLOR_CODES.put("8", 8); + COLOR_CODES.put("0", 16); + } + + @Register("color") + public void changeColor(@Validator("isLeader") PlayerChatter sender) { + Team team = Team.get(sender.user().getTeam()); + + if(notDuringEvent(sender)) + return; + + SWListInv inv = new SWListInv<>(sender, new Message("TEAM_COLOR_TITLE"), COLOR_CODES.entrySet().stream().map(entry -> new SWListInv.SWListEntry<>(new SWItem(new Message("PLAIN_STRING", "§" + entry.getKey() + team.getTeamKuerzel()), entry.getValue()), entry.getKey())).toList(), (click, element) -> {}); + inv.setCallback((click, element) -> { + inv.close(); + team.setTeamColor(element); + }); + inv.open(); + } + + @ClassMapper(Event.class) + public TypeMapper eventTypeMapper() { + return new TypeMapper() { + @Override + public Event map(Chatter sender, PreviousArguments previousArguments, String s) { + return Event.get(s); + } + + @Override + public boolean validate(Chatter sender, Event value, MessageSender messageSender) { + if (value == null) { + sender.system("TEAM_EVENT_NO_EVENT"); + return false; + } else { + return true; + } + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + return Event.getComing().stream().map(Event::getEventName).toList(); + } + }; + } + + @Validator(value = "isNotInTeam", local = true) + public TypeValidator isNotInTeamValidator() { + return (sender, value, messageSender) -> { + if (sender.user().getTeam() != 0) { + messageSender.send("TEAM_IN_TEAM"); + return false; + } + return true; + }; + } + + @Validator(value = "isInTeam", local = true) + public TypeValidator isInTeamValidator() { + return (sender, value, messageSender) -> { + if (sender.user().getTeam() == 0) { + messageSender.send("TEAM_NOT_IN_TEAM"); + return false; + } + return true; + }; + } + + @Validator(value = "isLeader", local = true) + public TypeValidator isLeaderValidator() { + return (sender, value, messageSender) -> { + SteamwarUser user = sender.user(); + if (user.getTeam() == 0) { + messageSender.send("TEAM_NOT_IN_TEAM"); + return false; + } + if (!user.isLeader()) { + messageSender.send("TEAM_NOT_LEADER"); + return false; + } + return true; + }; + } + + @ClassMapper(Team.class) + @Cached(global = true, cacheDuration = 60) + public TypeMapper team() { + return new TypeMapper() { + @Override + public Team map(Chatter sender, PreviousArguments previousArguments, String s) { + return Team.get(s); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + return Team.getAll().stream() + .flatMap(team -> Stream.of(team.getTeamName(), team.getTeamKuerzel())) + .toList(); + } + }; + } + + private boolean checkTeamName(Chatter sender, Team team, String arg){ + Team t = Team.get(arg); + if(t != null && t.getTeamId() != team.getTeamId()){ + sender.system("TEAM_NAME_TAKEN"); + return true; + } + return false; + } + + private boolean checkTeamKuerzel(Chatter sender, Team team, String arg){ + Team t = Team.get(arg); + if(t != null && (team == null || t.getTeamId() != team.getTeamId())){ + sender.system("TEAM_KUERZEL_TAKEN"); + return true; + } + return false; + } + + private boolean notDuringEvent(Chatter sender){ + if(Event.get() != null){ + sender.system("TEAM_NOT_IN_EVENT"); + return true; + } + return false; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java new file mode 100644 index 00000000..6e9eb8e7 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TeamchatCommand.java @@ -0,0 +1,45 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.ChatterGroup; +import de.steamwar.sql.SteamwarUser; + +public class TeamchatCommand extends SWCommand { + + public TeamchatCommand() { + super("tc", "teamchat"); + } + + @Register(description = "TC_USAGE") + public void genericCommand(Chatter sender, @ErrorMessage(value = "TC_USAGE", allowEAs = false) String... args) { + SteamwarUser user = sender.user(); + + if(user.getTeam() == 0){ + sender.system("TC_NO_TEAM"); + return; + } + + ChatListener.sendChat(sender, new ChatterGroup(Chatter.allStream().filter(p -> p.user().getTeam() == user.getTeam())), "CHAT_TEAM", null, String.join(" ", args)); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TpCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TpCommand.java new file mode 100644 index 00000000..031e2baa --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TpCommand.java @@ -0,0 +1,169 @@ +/* + 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.velocitycore.*; +import de.steamwar.velocitycore.util.BauLock; +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeMapper; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.persistent.Bauserver; +import de.steamwar.persistent.Storage; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class TpCommand extends SWCommand { + + public TpCommand(){ + super("join", "tp", "teleport"); + } + + @Register + public void genericCommand(Chatter sender) { + sender.system(Event.get() == null ? "TP_USAGE" : "TP_USAGE_EVENT"); + } + + @Register + public void teleportCommand(PlayerChatter sender, @Mapper("to") String to, String... rest) { + RegisteredServer server = getTarget(to); + + //Give control of teleport command to server + Player p = sender.getPlayer(); + if (server == null || server.getPlayersConnected().contains(p)) { + if (rest.length == 0) { + p.spoofChatInput("/tp " + to); + } else { + p.spoofChatInput("/tp " + to + " " + String.join(" ", rest)); + } + return; + } + + teleport(sender, server); + } + + @Mapper("to") + @Cached(cacheDuration = 10) + public TypeMapper tabCompleter() { + return new TypeMapper<>() { + @Override + public String map(Chatter sender, PreviousArguments previousArguments, String s) { + return s; + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + List list = new ArrayList<>(); + for (Player player : VelocityCore.getProxy().getAllPlayers()) { + list.add(player.getUsername()); + } + if (Event.get() != null) { + EventStarter.getEventServer().keySet().forEach(teamId -> { + Team team = Team.get(teamId); + list.add(team.getTeamName()); + list.add(team.getTeamKuerzel()); + }); + } + if (Storage.teamServers.containsValue(sender.getPlayer().getCurrentServer().orElseThrow().getServerInfo())) { + Storage.directTabItems.getOrDefault(sender.getPlayer(), Collections.emptyMap()).forEach((uuid, item) -> list.add(item.getProfile().getName())); + } + return list; + } + }; + } + + public static void teleport(PlayerChatter sender, RegisteredServer server) { + if(CheckCommand.isChecking(sender.getPlayer())){ + sender.system("CHECK_CHECKING"); + return; + } + + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if(subserver == null) { + sender.getPlayer().createConnectionRequest(server).fireAndForget(); + return; + } + + switch (subserver.getType()) { + case ARENA: + if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.NoFightServer)) + return; + break; + + case BAUSERVER: + Bauserver bauserver = (Bauserver) subserver; + Player checker = VelocityCore.getProxy().getPlayer(bauserver.getOwner()).orElse(null); + if (checker != null && CheckCommand.isChecking(checker)) { + if (!sender.user().hasPerm(UserPerm.CHECK) && CheckCommand.getCheckingSchem(checker).getOwner() != sender.user().getId()) { + sender.system("JOIN_PLAYER_BLOCK"); + return; + } + } else if (BauLock.isLocked(SteamwarUser.get(bauserver.getOwner()), sender.user())) { + sender.system("BAU_LOCKED_NOALLOWED"); + Chatter.of(bauserver.getOwner()).system("BAU_LOCK_BLOCKED", sender); + return; + } else if (!bauserver.getOwner().equals(sender.user().getUUID()) && BauweltMember.getBauMember(bauserver.getOwner(), sender.user().getUUID()) == null) { + SubserverSystem.sendDeniedMessage(sender, bauserver.getOwner()); + sender.system("JOIN_PLAYER_BLOCK"); + return; + } + break; + + case BUILDER: + if(!sender.user().hasPerm(UserPerm.BUILD)) { + sender.system("JOIN_PLAYER_BLOCK"); + return; + } + break; + } + + SubserverSystem.sendPlayer(subserver, sender.getPlayer()); + } + + private static RegisteredServer getTarget(String arg) { + RegisteredServer server = null; + + //Get target player server + Player target = VelocityCore.getProxy().getPlayer(arg).orElse(null); + if(target != null) + server = target.getCurrentServer().map(ServerConnection::getServer).orElse(server); + + //Get target team event arena + if(server == null){ + Team team = Team.get(arg); + if(team != null){ + Subserver eventArena = EventStarter.getEventServer().get(team.getTeamId()); + if(eventArena != null && Subserver.getServerList().contains(eventArena)) + server = eventArena.getRegisteredServer(); + } + } + + return server; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TutorialCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TutorialCommand.java new file mode 100644 index 00000000..5b4e01fa --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TutorialCommand.java @@ -0,0 +1,163 @@ +/* + * 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.velocitycore.commands; + +import de.steamwar.velocitycore.*; +import de.steamwar.velocitycore.inventory.SWInventory; +import de.steamwar.velocitycore.inventory.SWItem; +import de.steamwar.velocitycore.inventory.SWListInv; +import de.steamwar.velocitycore.inventory.SWStreamInv; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeValidator; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.Tutorial; +import de.steamwar.sql.UserPerm; + +import java.io.File; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +public class TutorialCommand extends SWCommand { + + public TutorialCommand() { + super("tutorial"); + } + + @Register + public void genericCommand(PlayerChatter sender) { + openInventory(sender, true, false); + } + + @Register("rate") + public void rate(PlayerChatter sender) { + sender.getPlayer().spoofChatInput("/tutorial rate"); + } + + @Register("rate") + public void rate(PlayerChatter sender, int id) { + Tutorial tutorial = Tutorial.get(id); + if(tutorial == null) { + sender.getPlayer().spoofChatInput("/tutorial rate"); // Catch players manually entering numbers + return; + } + + rate(sender, tutorial); + } + + @Register(value = "create", description = "TUTORIAL_CREATE_HELP") + public void create(PlayerChatter sender, String material, String... name) { + create(sender, String.join(" ", name), material.toUpperCase()); + } + + @Register("own") + public void own(PlayerChatter sender) { + openInventory(sender, false, true); + } + + @Register("unreleased") + public void unreleased(@Validator("unreleased") PlayerChatter sender) { + openInventory(sender, false, false); + } + + @Validator("unreleased") + public TypeValidator unreleasedChecker() { + return (sender, value, messageSender) -> sender.user().hasPerm(UserPerm.TEAM); + } + + private void openInventory(PlayerChatter sender, boolean released, boolean own) { + SteamwarUser user = sender.user(); + + new SWStreamInv<>( + sender, + new Message("TUTORIAL_TITLE"), + (click, tutorial) -> { + if(!released && click.isShiftClick() && user.hasPerm(UserPerm.TEAM) && user.getId() != tutorial.getCreator()) { + tutorial.release(); + openInventory(sender, released, own); + return; + } else if(own && click.isShiftClick() && click.isRightClick()) { + tutorial.delete(); + SubserverSystem.deleteFolder(VelocityCore.local, world(tutorial).getPath()); + openInventory(sender, released, own); + return; + } + + new ServerStarter().tutorial(sender.getPlayer(), tutorial).start(); + }, + page -> (own ? Tutorial.getOwn(user.getId(), page, 45) : Tutorial.getPage(page, 45, released)).stream().map(tutorial -> new SWListInv.SWListEntry<>(getTutorialItem(tutorial, own), tutorial)).toList() + ).open(); + } + + private SWItem getTutorialItem(Tutorial tutorial, boolean personalHighlights) { + SWItem item = new SWItem(tutorial.getItem(), new Message("TUTORIAL_NAME", tutorial.getName())); + item.setHideAttributes(true); + + item.addLore(new Message("TUTORIAL_BY", SteamwarUser.get(tutorial.getCreator()).getUserName())); + item.addLore(new Message("TUTORIAL_STARS", String.format("%.1f", tutorial.getStars()))); + + if (personalHighlights) + item.addLore(new Message("TUTORIAL_DELETE")); + + if (personalHighlights && tutorial.isReleased()) + item.setEnchanted(true); + + return item; + } + + private void rate(PlayerChatter sender, Tutorial tutorial) { + int[] rates = new int[]{1, 2, 3, 4, 5}; + + new SWListInv<>(sender, new Message("TUTORIAL_RATE_TITLE"), Arrays.stream(rates).mapToObj(rate -> new SWListInv.SWListEntry<>(new SWItem("NETHER_STAR", new Message("TUTORIAL_RATE", rate)), rate)).toList(), (click, rate) -> { + tutorial.rate(sender.user().getId(), rate); + SWInventory.close(sender); + }).open(); + } + + private void create(PlayerChatter sender, String name, String item) { + Subserver subserver = Subserver.getSubserver(sender.getPlayer()); + SteamwarUser user = sender.user(); + File tempWorld = new File(ServerStarter.TEMP_WORLD_PATH, ServerStarter.serverToWorldName(ServerStarter.bauServerName(user))); + + if(subserver == null || !subserver.isStarted() || subserver.getType() != Servertype.BAUSERVER || !tempWorld.exists()) { + sender.system("TUTORIAL_CREATE_MISSING"); + return; + } + + subserver.execute("save-all"); + VelocityCore.schedule(() -> { + Tutorial tutorial = Tutorial.create(user.getId(), name, item); + File tutorialWorld = world(tutorial); + + if (tutorialWorld.exists()) + SubserverSystem.deleteFolder(VelocityCore.local, tutorialWorld.getPath()); + ServerStarter.copyWorld(VelocityCore.local, tempWorld.getPath(), tutorialWorld.getPath()); + sender.system("TUTORIAL_CREATED"); + }).delay(1, TimeUnit.SECONDS).schedule(); + } + + private File world(Tutorial tutorial) { + return new File(ServerStarter.TUTORIAL_PATH, String.valueOf(tutorial.getTutorialId())); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/TypeMappers.java b/VelocityCore/src/de/steamwar/velocitycore/commands/TypeMappers.java new file mode 100644 index 00000000..8036272f --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/TypeMappers.java @@ -0,0 +1,79 @@ +/* + * 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.velocitycore.commands; + +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommandUtils; +import de.steamwar.command.TypeMapper; +import de.steamwar.command.TypeValidator; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.sql.Punishment; +import lombok.experimental.UtilityClass; + +import java.util.Collection; + +@UtilityClass +public class TypeMappers { + + public static void init() { + SWCommandUtils.addValidator("arenaPlayer", arenaPlayer()); + SWCommandUtils.addMapper("nonHistoricArenaMode", arenaModeTypeMapper(false)); + SWCommandUtils.addMapper("historicArenaMode", arenaModeTypeMapper(true)); + SWCommandUtils.addMapper("arenaMap", arenaMapTypeMapper()); + } + + private static TypeValidator arenaPlayer() { + return (sender, player, messageSender) -> !PunishmentCommand.isPunishedWithMessage(player, Punishment.PunishmentType.NoFightServer); + } + + private static TypeMapper arenaModeTypeMapper(boolean historic) { + return new TypeMapper<>() { + @Override + public ArenaMode map(Chatter sender, PreviousArguments previousArguments, String s) { + return ArenaMode.getByChat(s); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + return ArenaMode.getAllChatNames(historic); + } + }; + } + + private static TypeMapper arenaMapTypeMapper() { + return new TypeMapper<>() { + @Override + public String map(Chatter sender, PreviousArguments previousArguments, String s) { + if (previousArguments.userArgs.length == 0) return null; + return ArenaMode.getByChat(previousArguments.userArgs[previousArguments.userArgs.length - 1]).convertToRealMapName(s); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + if (previousArguments.userArgs.length == 0) return null; + ArenaMode arenaMode = ArenaMode.getByChat(previousArguments.userArgs[previousArguments.userArgs.length - 1]); + if (arenaMode == null) return null; + return arenaMode.getMaps(); + } + }; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/UnIgnoreCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/UnIgnoreCommand.java new file mode 100644 index 00000000..7f1a5de0 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/UnIgnoreCommand.java @@ -0,0 +1,45 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.IgnoreSystem; +import de.steamwar.sql.SteamwarUser; + +public class UnIgnoreCommand extends SWCommand { + + public UnIgnoreCommand() { + super("unignore"); + } + + @Register(description = "UNIGNORE_USAGE") + public void genericCommand(Chatter sender, @ErrorMessage("UNIGNORE_NOT_PLAYER") SteamwarUser target) { + SteamwarUser user = sender.user(); + + if(!IgnoreSystem.isIgnored(user, target)){ + sender.system("UNIGNORE_NOT_IGNORED"); + return; + } + + IgnoreSystem.unIgnore(user, target); + sender.system("UNIGNORE_UNIGNORED", target.getUserName()); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/VerifyCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/VerifyCommand.java new file mode 100644 index 00000000..96324de2 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/VerifyCommand.java @@ -0,0 +1,60 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.discord.util.AuthManager; +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import net.dv8tion.jda.api.entities.User; + +import java.util.Base64; +import java.util.logging.Level; + +public class VerifyCommand extends SWCommand { + + public VerifyCommand() { + super("verify"); + } + + @Register(description = "VERIFY_USAGE") + public void genericCommand(Chatter sender, String code) { + byte[] bytes; + try { + bytes = Base64.getDecoder().decode(code); + } catch (IllegalArgumentException e) { + sender.system("VERIFY_INVALID"); + return; + } + + if(bytes.length != 16) { + sender.system("VERIFY_INVALID"); + return; + } + + User user = AuthManager.connectAuth(sender.user(), code); + if(user != null) { + VelocityCore.getLogger().log(Level.INFO,"%s Verified with Discorduser: %s".formatted(sender.user().getUserName(), user.getIdLong())); + sender.system("VERIFY_SUCCESS", user.getAsTag()); + } else { + sender.system("VERIFY_INVALID"); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java new file mode 100644 index 00000000..ff3370c8 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/WebpasswordCommand.java @@ -0,0 +1,72 @@ +/* + 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.velocitycore.commands; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +public class WebpasswordCommand extends SWCommand { + + public WebpasswordCommand() { + super("webpassword", "webpw", "web"); + } + + + @Register(description = "WEB_USAGE") + public void genericCommand(Chatter sender, String password) { + if(password.length() < 8) { + sender.system("WEB_PASSWORD_LENGTH"); + return; + } + + ProcessBuilder pb = new ProcessBuilder("php", "/var/www/register.php", sender.user().getUserName(), password); + pb.redirectErrorStream(true); + try { + Process regProcess = pb.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(regProcess.getInputStream())); + String errorLine; + if((errorLine = reader.readLine()) != null) { + if ("updated".equals(errorLine)) { + sender.system("WEB_UPDATED"); + return; + } else { + throw new SecurityException("Could not create webaccount " + errorLine); + } + } + + sender.system("WEB_CREATED"); + } catch (IOException e) { + throw new SecurityException("Could not create webaccount", e); + } + } + + public static void changeUsername(String oldUsername, String newUsername){ + ProcessBuilder pb = new ProcessBuilder("php", "/var/www/changename.php", oldUsername, newUsername); + try { + pb.start(); + } catch (IOException e) { + throw new SecurityException("Could not change username", e); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/WhoisCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/WhoisCommand.java new file mode 100644 index 00000000..6512c9c0 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/WhoisCommand.java @@ -0,0 +1,176 @@ +/* + 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.velocitycore.commands; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.persistent.Storage; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.mods.ModUtils; +import de.steamwar.command.PreviousArguments; +import de.steamwar.command.SWCommand; +import de.steamwar.command.TypeMapper; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.kyori.adventure.text.event.ClickEvent; + +import java.sql.Timestamp; +import java.text.DecimalFormat; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public class WhoisCommand extends SWCommand { + public WhoisCommand() { + super("whois"); + } + + @Register(description = "WHOIS_USAGE") + public void whois(Chatter sender, long id, WhoisParameterTypes... parameters) { + if(!sender.user().hasPerm(UserPerm.ADMINISTRATION)) { + sender.system("UNKNOWN_PLAYER"); + return; + } + + // SW ID or Discord ID + whois(sender, id < Integer.MAX_VALUE ? SteamwarUser.get((int) id) : SteamwarUser.get(id), parameters); + } + + @Register(description = "WHOIS_USAGE") + public void whois(Chatter sender, SteamwarUser user, WhoisParameterTypes... parameters) { + EnumSet parameterTypes = parameters.length == 0 ? EnumSet.noneOf(WhoisParameterTypes.class) : EnumSet.copyOf(Arrays.asList(parameters)); + + Team team = Team.get(user.getTeam()); + + sender.system("WHOIS_USERNAME", user.getUserName()); + sender.system("WHOIS_PREFIX", user.prefix().getColorCode() + user.prefix().getChatPrefix()); + sender.system("WHOIS_TEAM", new Message("WHOIS_TEAM_HOVER", team.getTeamName()), ClickEvent.runCommand("/team info " + team.getTeamKuerzel()), team.getTeamColor(), team.getTeamKuerzel(), team.getTeamName()); + + if (!sender.user().hasPerm(UserPerm.TEAM)) + return; + + if (sender.user().hasPerm(UserPerm.MODERATION)) { + sender.system("WHOIS_ID", user.getId()); + sender.system("WHOIS_UUID", new Message("WHOIS_UUID_HOVER"), ClickEvent.copyToClipboard(user.getUUID().toString()), user.getUUID().toString()); + if (user.getDiscordId() != null) + sender.system("WHOIS_DISCORD_ID", user.getDiscordId()); + + sender.system("WHOIS_PERMS", user.perms().stream().map(Enum::name).collect(Collectors.joining(", "))); + } + + Player target = VelocityCore.getProxy().getPlayer(user.getUUID()).orElse(null); + Timestamp firstJoin = user.getFirstjoin(); + double onlineTime = user.getOnlinetime(); + if(firstJoin == null && target != null) { + firstJoin = Storage.sessions.get(target); + } + + if(firstJoin != null) + sender.system("WHOIS_JOINED_FIRST", firstJoin.toString()); + sender.system("WHOIS_HOURS_PLAYED", new DecimalFormat("###.##").format(onlineTime / 3600d)); + + if(target != null) { + sender.system("WHOIS_CURRENT_PLAYED", new DecimalFormat("####.##").format((Instant.now().getEpochSecond() - Storage.sessions.get(target).toInstant().getEpochSecond()) / 60d)); + sender.system("WHOIS_CURRENT_SERVER", target.getCurrentServer().orElseThrow().getServerInfo().getName()); + sender.system("WHOIS_CURRENT_PROTOCOL", target.getProtocolVersion().getMostRecentSupportedVersion()); + + List mods = ModUtils.getPlayerModMap().get(user.getUUID()); + if(mods == null) + mods = Collections.emptyList(); + + sender.system("WHOIS_PLATFORM", mods.isEmpty() ? "Vanilla" : mods.get(0).getPlatform().toString()); + + if (parameterTypes.contains(WhoisParameterTypes.MOD)) { + if (!mods.isEmpty()) { + sender.system("WHOIS_ACTIVE_MODS", mods.size(), mods.stream().map(mod -> "§" + mod.getModType().getColorCode() + mod.getModName()).collect(Collectors.joining("§8, "))); + } else { + sender.system("WHOIS_NO_ACTIVE_MODS"); + } + } + } + + sender.system("WHOIS_PUNISHMENTS"); + List punishmentList = Punishment.getAllPunishmentsOfPlayer(user.getId()); + Set found = new HashSet<>(); + boolean isPunished = false; + boolean all = parameterTypes.contains(WhoisParameterTypes.ALL); + for (Punishment punishment : punishmentList) { + if (!all && !punishment.getType().isMulti() && !found.add(punishment.getType())) { + continue; + } + if (!all && !punishment.isCurrent()) { + continue; + } + sender.prefixless("WHOIS_PUNISHMENT", SteamwarUser.get(punishment.getPunisher()), punishment.getType().name(), duration(sender, punishment.getStartTime(), false), duration(sender, punishment.getEndTime(), punishment.isPerma()), punishment.getReason()); + isPunished = true; + } + if (!isPunished) { + sender.system(all ? "WHOIS_NO_ALL_PUNISHMENT" : "WHOIS_NO_PUNISHMENT"); + } + } + + private Message duration(Chatter sender, Timestamp timestamp, boolean perma) { + if(perma) + return new Message("PUNISHMENT_PERMA"); + else + return new Message("PLAIN_STRING", timestamp.toLocalDateTime().format(DateTimeFormatter.ofPattern(sender.parseToPlain("TIMEFORMAT")))); + } + + @ClassMapper(value = WhoisParameterTypes.class, local = true) + public TypeMapper argumentTypeMapper() { + WhoisParameterTypes[] values = WhoisParameterTypes.values(); + + return new TypeMapper() { + @Override + public WhoisParameterTypes map(Chatter sender, PreviousArguments previousArguments, String s) { + SteamwarUser user = sender.user(); + return Stream.of(values) + .filter(p -> user.hasPerm(p.perm)) + .filter(p -> p.getTabCompletes().contains(s)) + .findFirst() + .orElse(null); + } + + @Override + public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { + SteamwarUser user = sender.user(); + return Stream.of(values) + .filter(p -> user.hasPerm(p.perm)) + .flatMap(p -> p.getTabCompletes().stream()) + .collect(Collectors.toList()); + } + }; + } + + @AllArgsConstructor + public enum WhoisParameterTypes { + ALL(Arrays.asList("-a", "-all"), UserPerm.TEAM), + MOD(Arrays.asList("-m", "-mod", "-mods"), UserPerm.MODERATION); + + @Getter + private final List tabCompletes; + private final UserPerm perm; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java new file mode 100644 index 00000000..3928da7b --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordBot.java @@ -0,0 +1,218 @@ +/* + 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.velocitycore.discord; + +import de.steamwar.command.SWCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.Event; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.discord.channels.*; +import de.steamwar.velocitycore.discord.listeners.ChannelListener; +import de.steamwar.velocitycore.discord.listeners.DiscordSchemUpload; +import de.steamwar.velocitycore.discord.listeners.DiscordTeamEvent; +import de.steamwar.velocitycore.discord.listeners.DiscordTicketHandler; +import de.steamwar.velocitycore.discord.util.AuthManager; +import lombok.Getter; +import net.dv8tion.jda.api.*; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.Button; +import net.dv8tion.jda.api.requests.restaction.CommandListUpdateAction; +import net.dv8tion.jda.api.utils.MemberCachePolicy; + +import javax.security.auth.login.LoginException; +import java.awt.*; +import java.util.List; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class DiscordBot { + public static final String ARGUMENT_NAME = "arguments"; + + @Getter + private static DiscordBot instance; + + @Getter + private static final Map commands = new HashMap<>(); + + public static void withBot(Consumer consumer) { + if(instance != null) + consumer.accept(instance); + } + + public static Guild getGuild() { + return instance.jda.getGuildById(instance.config.getGuild()); + } + + @Getter + private DiscordChannel announcementChannel; + @Getter + private DiscordChatRoom ingameChat; + @Getter + private DiscordChatRoom serverTeamChat; + @Getter + private StaticMessageChannel eventChannel; + @Getter + private ChecklistChannel checklistChannel; + + @Getter + private final DiscordConfig config; + @Getter + private final JDA jda; + + public DiscordBot(DiscordConfig config) { + this.config = config; + + try { + jda = JDABuilder + .createDefault(config.getToken()) + .setStatus(OnlineStatus.ONLINE) + .setMemberCachePolicy(MemberCachePolicy.ONLINE) + .build(); + } catch (LoginException e) { + throw new SecurityException("Could not login", e); + } + + instance = this; + VelocityCore.schedule(this::asyncInit).schedule(); + } + + private void asyncInit() { + try { + jda.awaitReady(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + activity(); + new StaticMessageChannel(config.channel("roles"), () -> new MessageBuilder() + .setContent("**Rollenvergabe**\nKlicke um eine Rolle zu bekommen:") + .setActionRows(ActionRow.of(config.getRoles().values().stream().map(DiscordConfig.DiscordRole::toButton).toArray(Button[]::new))), event -> InteractionReply.reply(event, reply -> { + Member member = event.getMember(); + Guild guild = event.getGuild(); + Role role = guild.getRoleById(event.getComponentId()); + + if (member.getRoles().stream().anyMatch(role::equals)) { + guild.removeRoleFromMember(member, role).queue(); + reply.system("DC_ROLE_REMOVED", role.getAsMention()); + } else { + guild.addRoleToMember(member, role).queue(); + reply.system("DC_ROLE_ADDED", role.getAsMention()); + } + })); + new StaticMessageChannel(config.channel("rules"), () -> new MessageBuilder() + .setEmbeds(new EmbedBuilder() + .setDescription(String.join("\n", config.getRules())) + .setColor(Color.GRAY) + .setAuthor("SteamWar", "https://steamwar.de") + .setTitle("Regeln und Infos") + .build()) + .setActionRows( + ActionRow.of(Button.link("https://steamwar.de", "Website"), Button.link("https://steamwar.de/youtube", "YouTube")), + ActionRow.of(Button.primary("auth", Emoji.fromUnicode("U+2705")).withLabel("Minecraft verknüpfen")) + ), event -> { + if(event.getComponentId().equals("auth")) + event.reply("Gebe innerhalb der nächsten 10 Minuten ``/verify " + AuthManager.createDiscordAuthToken(event.getUser()) + "`` auf dem Minecraft Server ein").setEphemeral(true).queue(); + }); + new StaticMessageChannel(config.channel("ticket"), () -> new MessageBuilder() + .setEmbeds(new EmbedBuilder() + .setDescription("Hier kannst du Tickets öffnen, welche nur von dir und Teammitgliedern eingesehen werden können.") + .setTitle("SteamWar Tickets") + .setColor(Color.RED) + .build()) + .setActionRows(ActionRow.of(Arrays.stream(DiscordTicketType.values()).map(DiscordTicketType::toButton).toArray(Button[]::new))), DiscordTicketHandler::openTicket); + eventChannel = new StaticMessageChannel(config.channel("events"), EventChannel::get); + checklistChannel = new ChecklistChannel(config.channel("checklist")); + + announcementChannel = new DiscordChannel(config.channel("announcement")) { + @Override + public void received(GuildMessageReceivedEvent event) { + Chatter.broadcast().system("ALERT", event.getMessage().getContentDisplay()); + } + }; + ingameChat = new DiscordChatRoom(config.channel("ingame"), "CHAT_DISCORD_GLOBAL", Chatter::broadcast); + serverTeamChat = new DiscordChatRoom(config.channel("serverteam"), "CHAT_SERVERTEAM", Chatter::serverteam); + + VelocityCore.schedule(() -> { + try { + activity(); + eventChannel.update(); + checklistChannel.update(); + } catch (ErrorResponseException e) { + //ignored + } + }).repeat(30, TimeUnit.SECONDS).schedule(); + + jda.addEventListener( + new DiscordTicketHandler(), + new DiscordTeamEvent(), + new ChannelListener(), + new DiscordSchemUpload() + ); + + commandSetup(jda.retrieveCommands().complete(), jda.updateCommands()); + } + + private final OptionData commandArgument = new OptionData(OptionType.STRING, ARGUMENT_NAME, "Command arguments", false); + private void commandSetup(List existing, CommandListUpdateAction updateCommands) { + Set correctCommands = new HashSet<>(); + for(Command command : existing) { + if(!commands.containsKey(command.getName())) { + command.delete().complete(); + continue; + } + + List options = command.getOptions(); + if(options.size() != 1 || options.get(0).getType() != OptionType.STRING) + command.editCommand().clearOptions().addOptions(commandArgument).complete(); + + correctCommands.add(command.getName()); + } + + updateCommands + .addCommands(commands + .keySet().stream() + .filter(command -> !correctCommands.contains(command)) + .filter(command -> command.matches("^[\\w-]+$")) + .map(command -> new CommandData(command, command).addOptions(commandArgument)) + .toArray(CommandData[]::new)) + .queue(); + } + + private boolean activityToggle = false; + private void activity() { + if(activityToggle) { + Event event = Event.get(); + jda.getPresence().setActivity(event != null ? Activity.competing("dem Event " + event.getEventName()) : Activity.playing("auf SteamWar.de")); + } else { + int count = VelocityCore.getProxy().getPlayerCount(); + jda.getPresence().setActivity(Activity.playing(count == 1 ? "mit 1 Spieler" : ("mit " + count + " Spielern"))); + } + + activityToggle = !activityToggle; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordConfig.java b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordConfig.java new file mode 100644 index 00000000..967d74a3 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordConfig.java @@ -0,0 +1,70 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.discord; + +import de.steamwar.velocitycore.Config; +import de.steamwar.velocitycore.VelocityCore; +import lombok.Getter; +import lombok.NoArgsConstructor; +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.interactions.components.Button; +import net.dv8tion.jda.api.interactions.components.ButtonStyle; + +import java.io.File; +import java.util.List; +import java.util.Map; + +@Getter +public class DiscordConfig { + + public static DiscordConfig load() { + File file = new File(System.getProperty("user.home"), "discord.yml"); + if(!file.exists() || VelocityCore.get().getConfig().isEventmode()) + return null; + + return Config.load(DiscordConfig.class, file, description -> description.addPropertyParameters("roles", String.class, DiscordRole.class)); + } + + public String channel(String type) { + return channels.get(type); + } + + private String token; + private String guild; + + private Map channels; + private Map ranks; + private Map roles; + private List rules; + + private String ticketcategory; + + @NoArgsConstructor + public static class DiscordRole { + + private String emoji; + private String label; + private String roleId; + + public Button toButton() { + return Button.of(ButtonStyle.SECONDARY, roleId, label, Emoji.fromUnicode(emoji)); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordTicketType.java b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordTicketType.java new file mode 100644 index 00000000..5c6a2187 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/DiscordTicketType.java @@ -0,0 +1,46 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.discord; + +import lombok.AllArgsConstructor; +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.interactions.components.Button; +import net.dv8tion.jda.api.interactions.components.ButtonStyle; + +@AllArgsConstructor +public enum DiscordTicketType { + REPORT("U+1F46E", "Spieler melden", ButtonStyle.DANGER), + IDEA("U+1F4A1", "Feature vorschlagen", ButtonStyle.SUCCESS), + BUG("U+1F41B", "Bug melden", ButtonStyle.SECONDARY), + QUESTION("U+2753", "Fragen", ButtonStyle.PRIMARY), + APPEAL("U+1F528", "Entbannungsantrag", ButtonStyle.SECONDARY); + + private final String emoji; + private final String label; + private final ButtonStyle style; + + public Button toButton() { + return Button.of(style, name().toLowerCase(), Emoji.fromUnicode(emoji)).withLabel(label); + } + + public String introduction() { + return "DC_TICKETINTRO_" + name(); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/ChecklistChannel.java b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/ChecklistChannel.java new file mode 100644 index 00000000..2e6de441 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/ChecklistChannel.java @@ -0,0 +1,48 @@ +/* + * 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.velocitycore.discord.channels; + +import de.steamwar.sql.SchematicNode; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.velocitycore.commands.CheckCommand; + +import java.util.ArrayList; +import java.util.List; + +public class ChecklistChannel extends DiscordChannel { + + private final List lastSchematics = new ArrayList<>(); + + public ChecklistChannel(String channel) { + super(channel); + } + + public void update() { + List schems = CheckCommand.getSchemsToCheck(); + + lastSchematics.removeIf(id -> schems.stream().noneMatch(schem -> schem.getId() == id)); + schems.removeIf(schem -> lastSchematics.contains(schem.getId())); + + for(SchematicNode schem : schems) { + system("CHECK_LIST_TO_CHECK", CheckCommand.getWaitTime(schem), schem.getSchemtype().getKuerzel(), SteamwarUser.get(schem.getOwner()).getUserName(), schem.getName()); + lastSchematics.add(schem.getId()); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChannel.java b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChannel.java new file mode 100644 index 00000000..304e2d0f --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChannel.java @@ -0,0 +1,99 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.discord.channels; + +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.velocitycore.discord.listeners.ChannelListener; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.SteamwarUser; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.interaction.GenericComponentInteractionCreateEvent; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; + + +@AllArgsConstructor +public class DiscordChannel extends Chatter.PlayerlessChatter { + + public static SteamwarUser userOrPublic(User dcUser) { + SteamwarUser user = SteamwarUser.get(dcUser.getIdLong()); + return user != null ? user : SteamwarUser.get(0); + } + + private final SteamwarUser user; + @Getter + private final MessageChannel channel; + + public DiscordChannel(User user) { + this(userOrPublic(user), user.openPrivateChannel().complete()); + } + + public DiscordChannel(String channel) { + this(SteamwarUser.get(-1), DiscordBot.getGuild().getTextChannelById(channel)); + ChannelListener.getChannels().put(this.channel, this); + } + + public void send(String message) { + send(new MessageBuilder() + .append(message + .replace("&", "") + .replace("@everyone", "`@everyone`") + .replace("@here", "`@here`") + .replaceAll("<[@#]!?\\d+>", "`$0`"))); + } + + public void send(MessageBuilder builder) { + channel.sendMessage(builder.build()).queue(); + } + + public void received(GuildMessageReceivedEvent event) { + event.getMessage().delete().queue(); + } + + public void received(GenericComponentInteractionCreateEvent event) { + //ignored + } + + @Override + public SteamwarUser user() { + return user; + } + + @Override + public boolean chatShown() { + return true; + } + + @Override + public Component parse(boolean prefixed, Message message) { + return super.parse(false, message); + } + + @Override + public void sendMessage(Component msg) { + send(PlainTextComponentSerializer.plainText().serialize(msg)); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java new file mode 100644 index 00000000..2d5f0fb2 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/DiscordChatRoom.java @@ -0,0 +1,52 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.discord.channels; + +import de.steamwar.velocitycore.listeners.ChatListener; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.ChatterGroup; +import de.steamwar.sql.Punishment; +import de.steamwar.sql.SteamwarUser; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; + +import java.util.function.Supplier; + +public class DiscordChatRoom extends DiscordChannel { + + private final String format; + private final Supplier target; + + public DiscordChatRoom(String channel, String format, Supplier target) { + super(channel); + this.format = format; + this.target = target; + } + + @Override + public void received(GuildMessageReceivedEvent event) { + SteamwarUser user = SteamwarUser.get(event.getAuthor().getIdLong()); + if (user == null || event.getMessage().getContentRaw().length() > 250 || user.isPunished(Punishment.PunishmentType.Ban) || user.isPunished(Punishment.PunishmentType.Mute)) { + event.getMessage().delete().queue(); + return; + } + + ChatListener.sendChat(Chatter.of(user), target.get(), format, null, event.getMessage().getContentDisplay().replace('§', '&').replace('\n', ' ')); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/EventChannel.java b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/EventChannel.java new file mode 100644 index 00000000..b8d8b1bc --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/EventChannel.java @@ -0,0 +1,119 @@ +/* + * 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.velocitycore.discord.channels; + +import de.steamwar.sql.Event; +import de.steamwar.sql.EventFight; +import de.steamwar.sql.Team; +import de.steamwar.sql.TeamTeilnahme; +import lombok.experimental.UtilityClass; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.selections.SelectionMenu; + +import java.awt.*; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.stream.Collectors; + +@UtilityClass +public class EventChannel { + + public MessageBuilder get() { + if (Event.get() == null) + return updateComing(); + + return updateCurrent(); + } + + private MessageBuilder updateComing() { + EmbedBuilder embedBuilder = new EmbedBuilder() + .setColor(Color.GRAY) + .setTitle("Zukünftige Events") + .setAuthor("SteamWar", "https://www.steamwar.de"); + + + SelectionMenu.Builder menuBuilder = SelectionMenu.create("eventName") + .setPlaceholder("Wähle ein Event aus!") + .setMinValues(1) + .setMaxValues(1); + + Timestamp now = Timestamp.from(Instant.now()); + List events = Event.getComing(); + events.forEach(event -> { + StringBuilder st = new StringBuilder(); + if (event.getDeadline().after(now)) { + st.append("Deadline: \n"); + } + st.append("Start: "); + String teilname = TeamTeilnahme.getTeams(event.getEventID()).stream().map(Team::getTeamKuerzel).collect(Collectors.joining(", ")); + if (!teilname.isEmpty()) { + st.append("\nAngemeldete Teams: ").append(teilname); + } + embedBuilder.addField(event.getEventName(), st.toString(), false); + if(event.getDeadline().after(Timestamp.from(Instant.now()))) { + menuBuilder.addOption(event.getEventName(), event.getEventID() + "", "An " + event.getEventName() + " teilnehmen", Emoji.fromUnicode("U+1F4DD")); + } + }); + + MessageBuilder messageBuilder = new MessageBuilder() + .setEmbeds(embedBuilder.build()); + + if(events.stream().anyMatch(event -> event.getDeadline().after(Timestamp.from(Instant.now())))) { + messageBuilder.setActionRows(ActionRow.of(menuBuilder.build())); + } + return messageBuilder; + } + + private MessageBuilder updateCurrent() { + Event event = Event.get(); + EmbedBuilder embedBuilder = new EmbedBuilder() + .setColor(Color.GRAY) + .setTitle("Event: " + event.getEventName()) + .setAuthor("SteamWar", "https://www.steamwar.de"); + + Instant now = Instant.now(); + EventFight.getEvent(event.getEventID()).forEach(eventFight -> { + Team teamBlue = Team.get(eventFight.getTeamBlue()); + Team teamRed = Team.get(eventFight.getTeamRed()); + + StringBuilder st = new StringBuilder(); + st.append("Fightbeginn: "); + if(now.isAfter(eventFight.getStartTime().toInstant().plus(35, ChronoUnit.MINUTES))) { + st.append("\n"); + if (eventFight.getErgebnis() == 1) { + st.append("Sieg ").append(teamBlue.getTeamKuerzel()); + } else if (eventFight.getErgebnis() == 2) { + st.append("Sieg ").append(teamRed.getTeamKuerzel()); + } else { + st.append("Unentschieden"); + } + } + embedBuilder.addField(teamBlue.getTeamKuerzel() + " vs. " + teamRed.getTeamKuerzel(), st.toString(), true); + }); + + return StaticMessageChannel.toMessageBuilder(embedBuilder); + } + +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/InteractionReply.java b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/InteractionReply.java new file mode 100644 index 00000000..cbacf536 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/InteractionReply.java @@ -0,0 +1,65 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.discord.channels; + +import net.dv8tion.jda.api.interactions.Interaction; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class InteractionReply extends DiscordChannel { + + public static void reply(Interaction interaction, Consumer consumer) { + InteractionReply reply = new InteractionReply(interaction); + consumer.accept(reply); + reply.submit(); + } + + private final Interaction interaction; + + private boolean replied = false; + private final List messages = new ArrayList<>(); + + private InteractionReply(Interaction interaction) { + super(interaction.getUser()); + this.interaction = interaction; + } + + @Override + public void sendMessage(Component msg) { + if(replied) { + super.sendMessage(msg); + return; + } + + messages.add(PlainTextComponentSerializer.plainText().serialize(msg)); + } + + public void submit() { + (messages.isEmpty() ? + interaction.deferReply() : + interaction.reply(String.join("\n", messages)) + ).setEphemeral(true).queue(); + replied = true; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/channels/StaticMessageChannel.java b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/StaticMessageChannel.java new file mode 100644 index 00000000..2041583f --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/channels/StaticMessageChannel.java @@ -0,0 +1,69 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.discord.channels; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.events.interaction.GenericComponentInteractionCreateEvent; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class StaticMessageChannel extends DiscordChannel { + + public static MessageBuilder toMessageBuilder(EmbedBuilder embedBuilder) { + MessageBuilder messageBuilder = new MessageBuilder(); + messageBuilder.setEmbeds(embedBuilder.build()); + return messageBuilder; + } + + private Message message; + private final Supplier supplier; + private final Consumer interaction; + + public StaticMessageChannel(String channel, Supplier supplier) { + this(channel, supplier, event -> {}); + } + + public StaticMessageChannel(String channel, Supplier supplier, Consumer interaction) { + super(channel); + this.supplier = supplier; + this.interaction = interaction; + + if(getChannel().hasLatestMessage()) + message = getChannel().getIterableHistory().complete().stream().filter(m -> m.getAuthor().isBot()).findFirst().orElse(null); + + update(); + } + + public void update() { + if (message == null) { + getChannel().sendMessage(supplier.get().build()).queue(m -> message = m); + } else { + message.editMessage(supplier.get().build()).queue(); + } + } + + @Override + public void received(GenericComponentInteractionCreateEvent event) { + interaction.accept(event); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/ChannelListener.java b/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/ChannelListener.java new file mode 100644 index 00000000..7665c40f --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/ChannelListener.java @@ -0,0 +1,91 @@ +/* + * This file is a part of the SteamWar software. + *

+ * Copyright (C) 2021 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.velocitycore.discord.listeners; + +import de.steamwar.command.SWCommand; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.velocitycore.discord.channels.DiscordChannel; +import de.steamwar.velocitycore.discord.channels.InteractionReply; +import lombok.Getter; +import net.dv8tion.jda.api.entities.ChannelType; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.events.interaction.GenericComponentInteractionCreateEvent; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.InteractionType; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; + +public class ChannelListener extends ListenerAdapter { + @Getter + private static final Map channels = new HashMap<>(); + + @Override + public void onGuildMessageReceived(@NotNull GuildMessageReceivedEvent event) { + if(event.getAuthor().isBot()) + return; + + DiscordChannel channel = channels.get(event.getChannel()); + if(channel != null) + channel.received(event); + } + + @Override + public void onGenericComponentInteractionCreate(@NotNull GenericComponentInteractionCreateEvent event) { + if(event.getType() != InteractionType.COMPONENT) + return; + + DiscordChannel channel = channels.get(event.getChannel()); + if(channel != null) { + channel.received(event); + return; + } + + if(event.getChannelType() == ChannelType.PRIVATE && event.getComponentId().equals("tada")) + event.reply(":tada:").queue(); + } + + @Override + public void onSlashCommand(@NotNull SlashCommandEvent event) { + InteractionReply.reply(event, sender -> { + if(sender.user().getDiscordId() == null) + return; + + OptionMapping option = event.getOption(DiscordBot.ARGUMENT_NAME); + String args = ""; + if(option != null) + args = option.getAsString(); + + VelocityCore.getLogger().log(Level.INFO, "%s -> executed Discord command /%s %s".formatted(sender.user().getUserName(), event.getName(), args)); + SWCommand command = DiscordBot.getCommands().get(event.getName()); + UserPerm permission = command.getPermission(); + if(permission != null && !sender.user().perms().contains(permission)) + return; + + command.execute(sender, args.split(" ")); + }); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordSchemUpload.java b/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordSchemUpload.java new file mode 100644 index 00000000..e8969a7c --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordSchemUpload.java @@ -0,0 +1,90 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.discord.listeners; + +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.discord.channels.DiscordChannel; +import de.steamwar.sql.NodeData; +import de.steamwar.sql.Punishment; +import de.steamwar.sql.SchematicNode; +import de.steamwar.sql.SteamwarUser; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.events.message.priv.PrivateMessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; + +public class DiscordSchemUpload extends ListenerAdapter { + + private static final List SCHEM_FILE_ENDINGS = Arrays.asList(".schem", ".schematic"); + + @Override + public void onPrivateMessageReceived(PrivateMessageReceivedEvent event) { + Message message = event.getMessage(); + if(message.getAttachments().isEmpty()) + return; + + DiscordChannel sender = new DiscordChannel(event.getAuthor()); + SteamwarUser user = sender.user(); + if(user.getId() == 0) { + sender.system("DC_UNLINKED"); + return; + } + + if(user.isPunished(Punishment.PunishmentType.NoSchemReceiving)) { + sender.system("DC_SCHEMUPLOAD_NOPERM"); + return; + } + + for (Message.Attachment attachment : message.getAttachments()) { + String fileName = attachment.getFileName(); + int dot = fileName.lastIndexOf('.'); + if(dot == -1 || !SCHEM_FILE_ENDINGS.contains(fileName.substring(dot).toLowerCase())) { + sender.system("DC_SCHEMUPLOAD_IGNORED", attachment.getFileName()); + continue; + } + + String name = fileName.substring(0, dot); + if(SchematicNode.invalidSchemName(new String[] {name})) { + sender.system("DC_SCHEMUPLOAD_INVCHAR", name); + continue; + } + + SchematicNode node = SchematicNode.getSchematicNode(user.getId(), name, (Integer) null); + if(node == null) + node = SchematicNode.createSchematic(user.getId(), name, null); + + try (InputStream in = attachment.retrieveInputStream().get()) { + NodeData.get(node).saveFromStream(in, fileName.substring(dot).equalsIgnoreCase(".schem")); + sender.system("DC_SCHEMUPLOAD_SUCCESS", name); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException | IOException e) { + VelocityCore.getLogger().log(Level.SEVERE, "Could not upload schem \"%s\" for user \"%s\"".formatted(name, user.getUserName()), e); + sender.system("DC_SCHEMUPLOAD_ERROR", name); + } + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordTeamEvent.java b/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordTeamEvent.java new file mode 100644 index 00000000..d18de226 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordTeamEvent.java @@ -0,0 +1,67 @@ +/* + 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.velocitycore.discord.listeners; + +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.velocitycore.discord.channels.InteractionReply; +import de.steamwar.sql.Event; +import net.dv8tion.jda.api.events.interaction.SelectionMenuEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +public class DiscordTeamEvent extends ListenerAdapter { + + private final String eventsChannel = DiscordBot.getInstance().getConfig().channel("events"); + + @Override + public void onSelectionMenu(@NotNull SelectionMenuEvent event) { + if(!event.getChannel().getId().equals(eventsChannel)) + return; + + if(event.getSelectedOptions().isEmpty()) { + event.deferReply(true).queue(); + return; + } + + InteractionReply.reply(event, reply -> { + int eventId; + try { + eventId = Integer.parseInt(event.getSelectedOptions().get(0).getValue()); + } catch (NumberFormatException e) { + reply.system("UNKNOWN_EVENT"); + return; + } + + if(reply.user().getId() == 0) { + reply.system("DC_UNLINKED"); + return; + } + + Event tournament = Event.get(eventId); + if(tournament == null){ + reply.system("UNKNOWN_EVENT"); + return; + } + + VelocityCore.get().getTeamCommand().event(reply, tournament); + }); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordTicketHandler.java b/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordTicketHandler.java new file mode 100644 index 00000000..692d475a --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/listeners/DiscordTicketHandler.java @@ -0,0 +1,165 @@ +/* + 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.velocitycore.discord.listeners; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.ChatterGroup; +import de.steamwar.messages.Message; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.velocitycore.discord.DiscordTicketType; +import de.steamwar.velocitycore.discord.channels.DiscordChannel; +import de.steamwar.velocitycore.discord.channels.InteractionReply; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.entities.MessageChannel; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.interaction.GenericComponentInteractionCreateEvent; +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.Button; +import net.kyori.adventure.text.event.ClickEvent; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.time.Instant; +import java.util.LinkedList; +import java.util.stream.Collectors; + +public class DiscordTicketHandler extends ListenerAdapter { + + private static final String TICKET_CATEGORY = DiscordBot.getInstance().getConfig().getTicketcategory(); + private static final String TICKET_LOG = DiscordBot.getInstance().getConfig().channel("ticketlog"); + private static final String TICKET_CHANNEL = DiscordBot.getInstance().getConfig().channel("ticket"); + + public static void openTicket(GenericComponentInteractionCreateEvent event) { + DiscordTicketType ticketType = DiscordTicketType.valueOf(event.getComponentId().toUpperCase()); + SteamwarUser user = SteamwarUser.get(event.getUser().getIdLong()); + + TextChannel ticketChannel = event.getGuild().getCategoryById(TICKET_CATEGORY).createTextChannel((user == null ? event.getUser().getName() : user.getUserName()) + "-" + event.getComponentId() + "-" + System.currentTimeMillis() % 1000).complete(); + ticketChannel.createPermissionOverride(event.getMember()).setAllow( + Permission.VIEW_CHANNEL, + Permission.MESSAGE_WRITE, + Permission.MESSAGE_ATTACH_FILES, + Permission.MESSAGE_ADD_REACTION, + Permission.MESSAGE_READ, + Permission.MESSAGE_EMBED_LINKS, + Permission.MESSAGE_HISTORY).complete(); + ticketChannel.getManager().setTopic(event.getUser().getId()).complete(); + + DiscordChannel channel = new DiscordChannel(DiscordChannel.userOrPublic(event.getUser()), ticketChannel); + channel.send(new MessageBuilder() + .setEmbeds(new EmbedBuilder() + .setTitle(channel.parseToPlain("DC_TICKET_TITLE")) + .setDescription(channel.parseToPlain(ticketType.introduction())) + .setColor(Color.GREEN) + .build()) + .setActionRows(ActionRow.of(Button.danger("close-" + ticketChannel.getName(), channel.parseToPlain("DC_TICKET_CLOSE")).withEmoji(Emoji.fromUnicode("U+26A0"))))); + + InteractionReply.reply(event, reply -> reply.system("DC_TICKET_CREATED", ticketChannel.getAsMention())); + Chatter.serverteam().prefixless("DISCORD_TICKET_NEW", ticketChannel.getName()); + } + + @Override + public void onGenericComponentInteractionCreate(@NotNull GenericComponentInteractionCreateEvent event) { + MessageChannel messageChannel = event.getChannel(); + if(messageChannel instanceof TextChannel channel && channel.getParent() != null && channel.getParent().getId().equals(TICKET_CATEGORY) && event.getComponentId().startsWith("close-")) { + LinkedList messages = channel.getIterableHistory().complete().stream() + .filter(message -> !message.getAuthor().isSystem() && !message.getAuthor().isBot()) + .map(message -> { + StringBuilder stringBuilder = new StringBuilder() + .append(" ") + .append("**").append(message.getAuthor().getName()).append("**: ") + .append(message.getContentRaw()) + .append("\n"); + + if(!message.getAttachments().isEmpty()) { + message.getAttachments().forEach(attachment -> stringBuilder.append(attachment.getUrl())); + stringBuilder.append("\n"); + } + + return stringBuilder; + }) + .collect(Collectors.toCollection(LinkedList::new)); + + messages.addFirst(new StringBuilder().append(" **").append(event.getUser().getName()).append("**: Ticket closed")); + + LinkedList messageBuilders = new LinkedList<>(); + messageBuilders.add(new StringBuilder()); + messages.descendingIterator() + .forEachRemaining(stringBuilder -> { + if(stringBuilder.length() >= 4096) { + messageBuilders.getLast().append(stringBuilder.substring(0, 4090)); + messageBuilders.add(new StringBuilder(stringBuilder.substring(4090, stringBuilder.length() - 1))); + } else if (stringBuilder.length() + messageBuilders.getLast().length() >= 4096) { + messageBuilders.add(new StringBuilder(stringBuilder.toString())); + } else { + messageBuilders.getLast().append(stringBuilder); + } + }); + + EmbedBuilder embedBuilder = new EmbedBuilder() + .setColor(Color.GREEN) + .setTimestamp(Instant.now()) + .setTitle(event.getTextChannel().getName()); + + if(channel.getTopic() != null && !channel.getTopic().isEmpty()) { + User user = event.getJDA().retrieveUserById(channel.getTopic()).complete(); + embedBuilder.setAuthor(user.getName(), null, user.getAvatarUrl()); + } + + TextChannel logChannel = event.getGuild().getTextChannelById(TICKET_LOG); + messageBuilders.forEach(stringBuilder -> logChannel.sendMessage(new MessageBuilder().setEmbeds(embedBuilder.setDescription(stringBuilder.toString()).build()).build()).queue()); + + Chatter.serverteam().prefixless("DISCORD_TICKET_CLOSED", channel.getName()); + channel.delete().reason("Closed").queue(); + } + } + + @Override + public void onGuildMessageReceived(@NotNull GuildMessageReceivedEvent event) { + TextChannel channel = event.getChannel(); + if( + channel.getParent() != null && + channel.getParent().getId().equals(TICKET_CATEGORY) && + !channel.getId().equals(TICKET_CHANNEL) && + !channel.getId().equals(TICKET_LOG) + ) { + if(event.getAuthor().isBot()) + return; + + ChatterGroup receivers = new ChatterGroup(Chatter.allStream().filter(player -> player.user().hasPerm(UserPerm.TICKET_LOG))); + try { + SteamwarUser user = SteamwarUser.get(Long.parseLong(channel.getTopic())); + if(user != null && !user.perms().contains(UserPerm.TEAM)) + receivers = new ChatterGroup(receivers, Chatter.of(user)); + } catch(NumberFormatException e) { + //ignored + } + + receivers.prefixless("DISCORD_TICKET_MESSAGE", new Message("DISCORD_TICKET_HOVER"), ClickEvent.openUrl(event.getMessage().getJumpUrl()), event.getChannel().getName(), event.getAuthor().getName(), event.getMessage().getContentRaw()); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/util/AuthManager.java b/VelocityCore/src/de/steamwar/velocitycore/discord/util/AuthManager.java new file mode 100644 index 00000000..972f77f1 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/util/AuthManager.java @@ -0,0 +1,72 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.discord.util; + +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.discord.channels.DiscordChannel; +import de.steamwar.sql.SteamwarUser; +import lombok.experimental.UtilityClass; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.Button; + +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +@UtilityClass +public class AuthManager { + + private final Map TOKENS = new HashMap<>(); + private final Random rand = new Random(); + + public String createDiscordAuthToken(User user) { + TOKENS.values().removeIf(user::equals); + + byte[] randBytes = new byte[16]; + rand.nextBytes(randBytes); + String code = Base64.getEncoder().encodeToString(randBytes); + + TOKENS.put(code, user); + VelocityCore.getLogger().log(Level.INFO, "Created Discord Auth-Token: %s for: %s".formatted(code, user.getAsTag())); + VelocityCore.schedule(() -> TOKENS.remove(code)).delay(10, TimeUnit.MINUTES).schedule(); + return code; + } + + public User connectAuth(SteamwarUser user, String code) { + User dcUser = TOKENS.remove(code); + if(dcUser == null) + return null; + + user.setDiscordId(dcUser.getIdLong()); + + DiscordChannel channel = new DiscordChannel(dcUser); + channel.send(new MessageBuilder() + .setContent(channel.parseToPlain("DC_AUTH_SUCCESS", user)) + .setActionRows(ActionRow.of(Button.success("tada", Emoji.fromUnicode("U+1F389"))))); + + return dcUser; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/util/DiscordAlert.java b/VelocityCore/src/de/steamwar/velocitycore/discord/util/DiscordAlert.java new file mode 100644 index 00000000..0b115d41 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/util/DiscordAlert.java @@ -0,0 +1,64 @@ +/* + 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.velocitycore.discord.util; + +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.velocitycore.discord.channels.DiscordChannel; +import lombok.experimental.UtilityClass; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.Button; + +import java.awt.*; +import java.time.Instant; + +@UtilityClass +public class DiscordAlert { + + public static void send(Chatter player, Color color, Message title, Message description, boolean success) { + DiscordBot.withBot(bot -> { + Long discordId = player.user().getDiscordId(); + if(discordId == null) + return; + + User user = DiscordBot.getInstance().getJda().getUserById(discordId); + if(user == null) + return; + + MessageBuilder builder = new MessageBuilder() + .setEmbeds(new EmbedBuilder() + .setAuthor("SteamWar", "https://steamwar.de", "https://cdn.discordapp.com/app-icons/869606970099904562/60c884000407c02671d91d8e7182b8a1.png") + .setColor(color) + .setTitle(player.parseToPlain(title)) + .setDescription(player.parseToPlain(description)) + .setTimestamp(Instant.now()) + .build()); + if(success) + builder.setActionRows(ActionRow.of(Button.success("tada", Emoji.fromUnicode("U+1F389")))); + + new DiscordChannel(user).send(builder); + }); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/discord/util/DiscordRanks.java b/VelocityCore/src/de/steamwar/velocitycore/discord/util/DiscordRanks.java new file mode 100644 index 00000000..d96e9fe8 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/discord/util/DiscordRanks.java @@ -0,0 +1,59 @@ +/* + * 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.velocitycore.discord.util; + +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import lombok.experimental.UtilityClass; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; + +import java.util.*; +import java.util.stream.Collectors; + + +@UtilityClass +public class DiscordRanks { + + private final Map prefixToPermName = UserPerm.prefixes.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, entry -> entry.getKey().name().toLowerCase())); + + public void update(SteamwarUser user) { + if (user.getDiscordId() == null) + return; + + Set swRoles = new HashSet<>(DiscordBot.getInstance().getConfig().getRanks().values()); + + Guild guild = DiscordBot.getGuild(); + guild.retrieveMemberById(user.getDiscordId()).queue(member -> { + String prefixRole = DiscordBot.getInstance().getConfig().getRanks().get(prefixToPermName.get(user.prefix())); + member.getRoles().stream() + .filter(role -> swRoles.contains(role.getId())) + .filter(role -> !role.getId().equals(prefixRole)) + .forEach(role -> guild.removeRoleFromMember(member, role).queue()); + + if (prefixRole != null && member.getRoles().stream().noneMatch(role -> role.getId().equals(prefixRole))) + guild.addRoleToMember(member, guild.getRoleById(prefixRole)).queue(); + }, e -> { + if(e instanceof ErrorResponseException err && err.getErrorCode() == 10007) + user.setDiscordId(null); + }); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/inventory/InvCallback.java b/VelocityCore/src/de/steamwar/velocitycore/inventory/InvCallback.java new file mode 100644 index 00000000..1e034313 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/inventory/InvCallback.java @@ -0,0 +1,61 @@ +/* + 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.velocitycore.inventory; + +public interface InvCallback { + + void clicked(ClickType click); + + enum ClickType { + LEFT, + SHIFT_LEFT, + RIGHT, + SHIFT_RIGHT, + WINDOW_BORDER_LEFT, + WINDOW_BORDER_RIGHT, + MIDDLE, + NUMBER_KEY, + DOUBLE_CLICK, + DROP, + CONTROL_DROP, + CREATIVE, + UNKNOWN; + + public boolean isKeyboardClick() { + return this == NUMBER_KEY || this == DROP || this == CONTROL_DROP; + } + + public boolean isCreativeAction() { + return this == MIDDLE || this == CREATIVE; + } + + public boolean isRightClick() { + return this == RIGHT || this == SHIFT_RIGHT; + } + + public boolean isLeftClick() { + return this == LEFT || this == SHIFT_LEFT || this == DOUBLE_CLICK || this == CREATIVE; + } + + public boolean isShiftClick() { + return this == SHIFT_LEFT || this == SHIFT_RIGHT || this == CONTROL_DROP; + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/inventory/SWInventory.java b/VelocityCore/src/de/steamwar/velocitycore/inventory/SWInventory.java new file mode 100644 index 00000000..36034011 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/inventory/SWInventory.java @@ -0,0 +1,105 @@ +/* + 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.velocitycore.inventory; + +import de.steamwar.messages.Message; +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.velocitycore.network.handlers.InventoryCallbackHandler; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.network.packets.server.CloseInventoryPacket; +import de.steamwar.network.packets.server.InventoryPacket; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; + +public class SWInventory { + + private final Map itemMap = new HashMap<>(); + private final Chatter player; + private final int size; + private final Message title; + @Setter + private InvCallback close; + @Getter + @Setter + private boolean next = false; + + private final AtomicBoolean processingClick = new AtomicBoolean(); + + public SWInventory(PlayerChatter player, int size, Message title) { + InventoryCallbackHandler.inventoryHashMap.put(player.user().getId(), this); + this.player = player; + this.size = size; + this.title = title; + } + + public void addItem(int pos, SWItem item, InvCallback callback) { + item.setCallback(callback); + addItem(pos, item); + } + + public void addItem(int pos, SWItem item) { + itemMap.put(pos, item); + } + + public void setCallback(int pos, InvCallback callback) { + itemMap.get(pos).setCallback(callback); + } + + public void handleCallback(InvCallback.ClickType type, int pos) { + if(processingClick.compareAndSet(false, true)) { + itemMap.get(pos).getCallback().clicked(type); + processingClick.set(false); + } + } + + public void handleClose() { + if(processingClick.compareAndSet(false, true)) { + InventoryCallbackHandler.inventoryHashMap.remove(player.user().getId(), this); + if(close != null) + close.clicked(null); + processingClick.set(false); + } + } + + public void open() { + InventoryPacket inv = new InventoryPacket(player.parseToLegacy(title), player.user().getId(), size, map(itemMap, (integer, swItem) -> swItem.writeToString(player, integer).toString())); + player.withPlayer(p -> NetworkSender.send(p, inv)); + } + + private static Map map(Map map, BiFunction function) { + Map result = new HashMap<>(); + map.forEach((key, value) -> result.put(key, function.apply(key, value))); + return result; + } + + public void close() { + close(player); + } + + public static void close(Chatter player) { + player.withPlayer(p -> NetworkSender.send(p, new CloseInventoryPacket(player.user().getId()))); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/inventory/SWItem.java b/VelocityCore/src/de/steamwar/velocitycore/inventory/SWItem.java new file mode 100644 index 00000000..c2db126f --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/inventory/SWItem.java @@ -0,0 +1,90 @@ +/* + 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.velocitycore.inventory; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import de.steamwar.messages.Message; +import de.steamwar.messages.Chatter; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +@Setter +public class SWItem { + + @Getter + private String material = "DYE"; + private Message title; + private String skullOwner; + private boolean enchanted; + private boolean hideAttributes; + private List lore = new ArrayList<>(); + @Getter + private InvCallback callback; + private int color = 0; + + public SWItem(String material, Message title) { + this.material = material.toUpperCase(); + this.title = title; + } + + public SWItem(Message title, int color) { + this.title = title; + this.color = color; + } + + public static SWItem getSkull(String skullOwner) { + SWItem item = new SWItem("SKULL", new Message("PLAIN_STRING", skullOwner)); + item.setSkullOwner(skullOwner); + return item; + } + + public SWItem addLore(Message lore) { + this.lore.add(lore); + return this; + } + + public JsonObject writeToString(Chatter player, int position) { + JsonObject object = new JsonObject(); + object.addProperty("material", material); + object.addProperty("position", position); + object.addProperty("title", player.parseToLegacy(title)); + if(skullOwner != null) + object.addProperty("skullOwner", skullOwner); + if(enchanted) + object.addProperty("enchanted", true); + if(hideAttributes) + object.addProperty("hideAttributes", true); + if(color != 0) + object.addProperty("color", color); + if(lore != null) { + JsonArray array = new JsonArray(); + for (Message lores : lore) { + array.add(player.parseToLegacy(lores)); + } + object.add("lore", array); + } + + return object; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/inventory/SWListInv.java b/VelocityCore/src/de/steamwar/velocitycore/inventory/SWListInv.java new file mode 100644 index 00000000..44f3deb6 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/inventory/SWListInv.java @@ -0,0 +1,88 @@ +/* + 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.velocitycore.inventory; + +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +public class SWListInv extends SWInventory { + @Setter + private ListCallback callback; + private final List> elements; + private int page; + + public SWListInv(PlayerChatter p, Message t, List> l, ListCallback c){ + super(p, (l.size()>45) ? 54 : (l.size() + 9-l.size()%9), t); + callback = c; + elements = l; + page = 0; + } + + @Override + public void open(){ + if(elements.size() > 54){ + if(page != 0) + addItem(45, new SWItem(new Message("INV_PAGE_BACK", "e"), 10), (InvCallback.ClickType click) -> { + page--; + open(); + }); + else + addItem(45, new SWItem(new Message("INV_PAGE_BACK", "7"), 8), (InvCallback.ClickType click) -> {}); + if(page < elements.size()/45) + addItem(53, new SWItem(new Message("INV_PAGE_NEXT", "e"), 10), (InvCallback.ClickType click) -> { + page++; + open(); + }); + else + addItem(53, new SWItem(new Message("INV_PAGE_NEXT", "7"), 8), (InvCallback.ClickType click) -> {}); + } + + int ipageLimit = elements.size() - page*45; + if(ipageLimit > 45 && elements.size() > 54){ + ipageLimit = 45; + } + int i = page*45; + for(int ipage=0; ipage < ipageLimit; ipage++ ){ + SWItem e = elements.get(i).getItem(); + + final int pos = i; + addItem(ipage, e); + setCallback(ipage, (InvCallback.ClickType click) -> callback.clicked(click, elements.get(pos).getObject())); + i++; + } + super.open(); + } + + public interface ListCallback{ + void clicked(InvCallback.ClickType click, T element); + } + + @Getter + @AllArgsConstructor + public static class SWListEntry { + final SWItem item; + final T object; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/inventory/SWStreamInv.java b/VelocityCore/src/de/steamwar/velocitycore/inventory/SWStreamInv.java new file mode 100644 index 00000000..2d82e352 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/inventory/SWStreamInv.java @@ -0,0 +1,71 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.inventory; + +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; + +import java.util.List; +import java.util.function.Function; + +public class SWStreamInv extends SWInventory { + private final SWListInv.ListCallback callback; + private final Function>> constructor; + private int page; + + public SWStreamInv(PlayerChatter chatter, Message title, SWListInv.ListCallback callback, Function>> constructor) { + super(chatter, 54, title); + this.callback = callback; + this.constructor = constructor; + page = 0; + } + + @Override + public void open(){ + List> entries = constructor.apply(page); + + if(page != 0) + addItem(45, new SWItem(new Message("INV_PAGE_BACK", "e"), 10), (InvCallback.ClickType click) -> { + page--; + open(); + }); + else + addItem(45, new SWItem(new Message("INV_PAGE_BACK", "7"), 8), (InvCallback.ClickType click) -> {}); + + if(entries.size() == 45) + addItem(53, new SWItem(new Message("INV_PAGE_NEXT", "e"), 10), (InvCallback.ClickType click) -> { + page++; + open(); + }); + else + addItem(53, new SWItem(new Message("INV_PAGE_NEXT", "7"), 8), (InvCallback.ClickType click) -> {}); + + for(int i = 0; i < entries.size(); i++) { + SWListInv.SWListEntry item = entries.get(i); + addItem(i, item.getItem()); + setCallback(i, (InvCallback.ClickType click) -> { + close(); + callback.clicked(click, item.getObject()); + }); + } + + super.open(); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/BanListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/BanListener.java new file mode 100644 index 00000000..1d205cc8 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/BanListener.java @@ -0,0 +1,85 @@ +/* + 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.velocitycore.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.LoginEvent; +import com.velocitypowered.api.proxy.Player; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.BannedUserIPs; +import de.steamwar.sql.Punishment; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.velocitycore.commands.PunishmentCommand; +import net.kyori.adventure.text.event.ClickEvent; + +import java.sql.Timestamp; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class BanListener extends BasicListener { + + @Subscribe + public void onLogin(LoginEvent event) { + Player player = event.getPlayer(); + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + String ip = IPSanitizer.getTrueAddress(player).getHostAddress(); + if (user.isPunished(Punishment.PunishmentType.Ban)) { + BannedUserIPs.banIP(user.getId(), ip); + Chatter.of(event).system(PunishmentCommand.punishmentMessage(user, Punishment.PunishmentType.Ban)); + return; + } + + List ips = BannedUserIPs.get(ip); + if(!ips.isEmpty()){ + Timestamp highestBan = ips.get(0).getTimestamp(); + boolean perma = false; + for(BannedUserIPs banned : ips) { + SteamwarUser bannedUser = SteamwarUser.get(banned.getUserID()); + if (bannedUser.isPunished(Punishment.PunishmentType.Ban)) { + Punishment ban = bannedUser.getPunishment(Punishment.PunishmentType.Ban); + if(ban.isPerma()) { + perma = true; + break; + } + if(ban.getEndTime().after(highestBan)) + highestBan = ban.getEndTime(); + } + } + ClickEvent clickEvent = ClickEvent.runCommand("/ban " + user.getUserName() + " " + + (perma ? "perma" : highestBan.toLocalDateTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy_HH:mm"))) + + " Ban Evasion - Bannumgehung"); + + Chatter.serverteam().system( + "BAN_AVOIDING_ALERT", + new Message("BAN_AVOIDING_BAN_HOVER"), + clickEvent, + user.getUserName(), + (Function) ((Chatter sender) -> ips.stream().map(banned -> { + SteamwarUser bannedUser = SteamwarUser.get(banned.getUserID()); + return sender.parseToLegacy("BAN_AVOIDING_LIST", bannedUser.getUserName(), + banned.getTimestamp().toLocalDateTime().format(DateTimeFormatter.ofPattern(sender.parseToPlain("TIMEFORMAT")))); + }).collect(Collectors.joining(" "))) + ); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/BasicListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/BasicListener.java new file mode 100644 index 00000000..34e2c766 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/BasicListener.java @@ -0,0 +1,29 @@ +/* + 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.velocitycore.listeners; + +import de.steamwar.velocitycore.VelocityCore; + +public abstract class BasicListener { + + protected BasicListener() { + VelocityCore.getProxy().getEventManager().register(VelocityCore.get(), this); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java new file mode 100644 index 00000000..74429c13 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/ChatListener.java @@ -0,0 +1,265 @@ +/* + 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.velocitycore.listeners; + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.event.PostOrder; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.command.CommandExecuteEvent; +import com.velocitypowered.api.event.player.PlayerChatEvent; +import com.velocitypowered.api.event.player.TabCompleteEvent; +import com.velocitypowered.api.proxy.ConsoleCommandSource; +import com.velocitypowered.api.proxy.Player; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.ChatterGroup; +import de.steamwar.messages.Message; +import de.steamwar.messages.PlayerChatter; +import de.steamwar.network.packets.server.PingPacket; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.*; +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.commands.PunishmentCommand; +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.velocitycore.network.NetworkSender; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class ChatListener extends BasicListener { + + private static final Logger cmdLogger = Logger.getLogger("Command logger"); + + private static final List rankedModes = ArenaMode.getAllModes().stream().filter(ArenaMode::isRanked).map(ArenaMode::getSchemType).toList(); + + @Subscribe(order = PostOrder.FIRST) + public void fixCommands(CommandExecuteEvent e) { + String command = e.getCommand(); + if(command.startsWith("7")) { + command = "/" + command.substring(1); + + CommandExecuteEvent.CommandResult result = e.getResult(); + if(result.isForwardToServer()) + result = CommandExecuteEvent.CommandResult.forwardToServer(command); + else if(result.isAllowed()) + result = CommandExecuteEvent.CommandResult.command(command); + + e.setResult(result); + } + } + + @Subscribe(order = PostOrder.LAST) + public void logCommands(CommandExecuteEvent e) { + String command = e.getCommand(); + int space = command.indexOf(' '); + if(VelocityCore.getProxy().getCommandManager().hasCommand(space != -1 ? command.substring(0, space) : command)) { + CommandSource source = e.getCommandSource(); + String name; + if(source instanceof Player player) + name = player.getUsername(); + else if(source instanceof ConsoleCommandSource) + name = "«CONSOLE»"; + else + name = source.toString(); + + cmdLogger.log(Level.INFO, "%s -> executed command /%s".formatted(name, command)); + } else if (e.getCommandSource() instanceof Player player) { + player.spoofChatInput("/" + command); + e.setResult(CommandExecuteEvent.CommandResult.denied()); + } + } + + @Subscribe + public void onChatEvent(PlayerChatEvent e) { + Player player = e.getPlayer(); + String message = e.getMessage(); + + e.setResult(PlayerChatEvent.ChatResult.denied()); + + if (message.contains("jndi:ldap")) { + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + PunishmentCommand.ban(user, Punishment.PERMA_TIME, "Versuchte Exploit-Ausnutzung", SteamwarUser.get(-1), true); + VelocityCore.getLogger().log(Level.SEVERE, "%s %s wurde automatisch wegen jndi:ldap gebannt.".formatted(user.getUserName(), user.getId())); + return; + } + + if (isMistypedCommand(player, message)) + return; + + Subserver subserver = Subserver.getSubserver(player); + if(subserver != null && subserver.getType() == Servertype.ARENA && subserver.getServer() == player.getCurrentServer().orElseThrow().getServerInfo()) { + localChat(Chatter.of(player), message); + } else if (message.startsWith("+")) { + localChat(Chatter.of(player), message.substring(1)); + } else { + sendChat(Chatter.of(player), Chatter.globalChat(), "CHAT_GLOBAL", null, message); + } + } + + private static boolean isMistypedCommand(Player player, String message) { + String command = message.substring(1); + boolean isCommand = message.startsWith("7") && command.matches("^[7/]?[A-Za-z]+"); + if(isCommand && Boolean.FALSE.equals(VelocityCore.getProxy().getCommandManager().executeAsync(player, command).join())) { + if(command.startsWith("7")) + command = "/" + command.substring(1); + message = "/" + command; + + if(filteredCommand(Chatter.of(player), message)) + return true; + + player.spoofChatInput(message); + } + + return isCommand; + } + + public static void sendChat(Chatter sender, ChatterGroup receivers, String format, Chatter msgReceiver, String message) { + SteamwarUser user = sender.user(); + final String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message; + if(chatFilter(sender, coloredMessage)) + return; + + boolean noReceiver = true; + for(Chatter player : receivers.getChatters()) { + if(player.chatShown()) { + chatToReciever(player, msgReceiver, user, format, coloredMessage); + if(sender.user().getId() != player.user().getId()) + noReceiver = false; + } + + } + + if(format.equals("CHAT_GLOBAL")) { + DiscordBot.withBot(bot -> chatToReciever(bot.getIngameChat(), msgReceiver, user, format, coloredMessage)); + } else if (format.equals("CHAT_SERVERTEAM")) { + DiscordBot.withBot(bot -> chatToReciever(bot.getServerTeamChat(), msgReceiver, user, format, coloredMessage)); + } else if (noReceiver) { + sender.system("CHAT_NO_RECEIVER"); + } + } + + public static void localChat(PlayerChatter sender, String message) { + if(message.isEmpty()){ + sender.system("CHAT_BC_USAGE"); + return; + } + + if(ChatListener.filteredCommand(sender, message)) + return; + + if(!message.startsWith("/") && chatFilter(sender, message)) + return; + + sender.getPlayer().spoofChatInput(message); + } + + private static boolean chatFilter(Chatter sender, String message) { + if(!sender.chatShown()) { + sender.system("CHAT_RECEIVE"); + return true; + } + + if(message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) { + sender.system("CHAT_EMPTY"); + return true; + } + + SteamwarUser user = sender.user(); + if(!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))){ + sender.system("CHAT_NO_LINKS"); + return true; + } + + if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) + return true; + + if (message.contains("LIXFEL")) + specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15); + if (message.contains("YOYONOW")) + specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12); + if (message.contains("CHAOSCAOT")) + specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17); + + return false; + } + + private static void chatToReciever(Chatter receiver, Chatter msgReceiver, SteamwarUser sender, String format, String message) { + UserPerm.Prefix prefix = sender.prefix(); + String chatColorCode = sender.hasPerm(UserPerm.TEAM) ? "§f" : "§7"; + receiver.prefixless(format, + sender, + msgReceiver == null ? receiver : msgReceiver, + highlightMentions(message, chatColorCode, receiver), + sender.getTeam() == 0 ? "" : "§" + Team.get(sender.getTeam()).getTeamColor() + Team.get(sender.getTeam()).getTeamKuerzel() + " ", + UserElo.getEmblem(sender, rankedModes), + prefix.getColorCode(), + prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ", + chatColorCode); + } + + private static boolean filteredCommand(Chatter sender, String message) { + String command = message.split(" ", 2)[0]; + if(command.startsWith("/") && command.contains(":")) { + sender.system("UNKNOWN_COMMAND"); + return true; + } + return false; + } + + private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) { + sender.system("CHAT_LIXFEL_ACTION_BAR"); + for(int i = 0; i < delay.length; i++) { + int finalI = i; + VelocityCore.schedule( () -> sender.prefixless("CHAT_MSG", name, sender.user(), new Message(baseMessage + (finalI+1)))).delay(delay[i], TimeUnit.SECONDS).schedule(); + } + } + + private static String highlightMentions(String message, String returnColor, Chatter player) { + if(!message.contains("@")) + return message; + + String mark = "@" + player.user().getUserName(); + return Arrays.stream(message.split(" ")).map(cur -> { + if(cur.equalsIgnoreCase(mark) && player.getPlayer() != null) { + NetworkSender.send(player.getPlayer(), new PingPacket(player.user().getId())); + return "§e" + cur + returnColor; + } + return cur; + }).collect(Collectors.joining(" ")); + } + + @Subscribe + public void onTabCompleteResponseEvent(TabCompleteEvent e){ + List suggestions = e.getSuggestions(); + int i = 0; + while (i < suggestions.size()) { + String suggestion = suggestions.get(i); + if(suggestion.startsWith("/") && suggestion.contains(":")) + suggestions.remove(i); + else + i++; + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/CheckListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/CheckListener.java new file mode 100644 index 00000000..46668160 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/CheckListener.java @@ -0,0 +1,74 @@ +/* + 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.velocitycore.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.proxy.Player; +import de.steamwar.persistent.Bauserver; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import de.steamwar.velocitycore.commands.CheckCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.SchematicNode; +import de.steamwar.sql.SchematicType; + +import java.util.ArrayList; +import java.util.List; + +public class CheckListener extends BasicListener { + + @Subscribe + public void onPlayerJoin(PostLoginEvent e){ + Chatter sender = Chatter.of(e.getPlayer()); + + List uncheckedSchematics = new ArrayList<>(); + for(SchematicType type : SchematicType.values()){ + if(type.check()) + uncheckedSchematics.addAll(SchematicNode.getAllSchematicsOfType(sender.user().getId(), type.toDB())); + } + + if(!uncheckedSchematics.isEmpty()) + sender.system("CHECK_UNCHECKED", uncheckedSchematics.size()); + } + + @Subscribe + public void onServerSwitch(ServerConnectedEvent e){ + Player player = e.getPlayer(); + if(CheckCommand.isChecking(player)){ + Subserver server = Subserver.getSubserver(e.getServer().getServerInfo()); + if( + server == null || + server.getType() != Servertype.BAUSERVER || + ((Bauserver)server).getOwner() != player.getUniqueId() + ) + CheckCommand.abort(player); + } + } + + @Subscribe + public void onPlayerDisconnect(DisconnectEvent e){ + Player player = e.getPlayer(); + if(CheckCommand.isChecking(player)) + CheckCommand.abort(player); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/ConnectionListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/ConnectionListener.java new file mode 100644 index 00000000..4fb3d0df --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/ConnectionListener.java @@ -0,0 +1,100 @@ +/* + 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.velocitycore.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.event.permission.PermissionsSetupEvent; +import com.velocitypowered.api.permission.Tristate; +import com.velocitypowered.api.proxy.Player; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import de.steamwar.velocitycore.commands.*; +import de.steamwar.velocitycore.discord.DiscordBot; +import de.steamwar.velocitycore.discord.util.DiscordRanks; +import de.steamwar.velocitycore.mods.ModUtils; +import net.kyori.adventure.text.event.ClickEvent; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class ConnectionListener extends BasicListener { + + private static final Set newPlayers = new HashSet<>(); + + public static void newPlayer(UUID player){ + newPlayers.add(player); + } + + private static final Set TEAM_PERMISSIONS = Set.of("velocity.command.send", "velocity.command.glist"); + + @Subscribe + public void onPermissionSetup(PermissionsSetupEvent event) { + event.setProvider(subject -> { + if(!(subject instanceof Player player)) + return perm -> Tristate.TRUE; + + Set perms = SteamwarUser.getOrCreate(player.getUniqueId(), player.getUsername(), ConnectionListener::newPlayer, WebpasswordCommand::changeUsername).perms(); + if(perms.contains(UserPerm.ADMINISTRATION)) + return perm -> Tristate.TRUE; + else if(perms.contains(UserPerm.TEAM)) + return perm -> Tristate.fromBoolean(TEAM_PERMISSIONS.contains(perm)); + else + return perm -> Tristate.FALSE; + }); + } + + @Subscribe + public void onPostLogin(PostLoginEvent event) { + Player player = event.getPlayer(); + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + Chatter chatter = Chatter.of(player); + + if(user.hasPerm(UserPerm.CHECK)) + CheckCommand.sendReminder(chatter); + + for(Subserver subserver : Subserver.getServerList()) { + if(subserver.getType() == Servertype.ARENA) { + chatter.system("JOIN_ARENA", new Message("JOIN_ARENA_HOVER"), ClickEvent.runCommand("/arena " + subserver.getServer().getName()), subserver.getServer().getName()); + } + } + + if(newPlayers.contains(player.getUniqueId())){ + Chatter.broadcast().system("JOIN_FIRST", player); + newPlayers.remove(player.getUniqueId()); + } + + DiscordBot.withBot(bot -> DiscordRanks.update(user)); + } + + @Subscribe + public void onDisconnect(DisconnectEvent e){ + ChallengeCommand.remove(e.getPlayer()); + MsgCommand.remove(e.getPlayer()); + ModUtils.getPlayerModMap().remove(e.getPlayer().getUniqueId()); + ModCommand.playerFilterType.remove(e.getPlayer().getUniqueId()); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/EventModeListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/EventModeListener.java new file mode 100644 index 00000000..5cc55757 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/EventModeListener.java @@ -0,0 +1,49 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.Event; +import de.steamwar.sql.Referee; +import de.steamwar.sql.TeamTeilnahme; + +public class EventModeListener extends BasicListener { + + @Subscribe + public void onPostLogin(PostLoginEvent e) { + Chatter sender = Chatter.disconnect(e.getPlayer()); + + Event event = Event.get(); + if(event == null) { + sender.system("EVENTMODE_KICK"); + return; + } + + if(TeamTeilnahme.nimmtTeil(sender.user().getTeam(), event.getEventID())) + return; + + if(Referee.get(event.getEventID()).contains(sender.user().getId())) + return; + + sender.system("EVENTMODE_KICK"); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/IPSanitizer.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/IPSanitizer.java new file mode 100644 index 00000000..7b0876a5 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/IPSanitizer.java @@ -0,0 +1,52 @@ +/* + * 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.velocitycore.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PreLoginEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.connection.client.LoginInboundConnection; +import de.steamwar.persistent.Reflection; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.mods.Hostname; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.logging.Level; + +public class IPSanitizer extends BasicListener { + + private static final Reflection.Field remoteAddress = new Reflection.Field<>(MinecraftConnection.class, "remoteAddress"); + + public static InetAddress getTrueAddress(Player player) { + return ((InetSocketAddress) ((ConnectedPlayer)player).getConnection().getChannel().remoteAddress()).getAddress(); + } + + private final InetSocketAddress sanitized = new InetSocketAddress("127.127.127.127", 25565); + + @Subscribe + public void loginEvent(PreLoginEvent e) { + VelocityCore.getLogger().log(Level.INFO, "%s has logged in with user name %s".formatted(e.getConnection().getRemoteAddress(), e.getUsername())); + remoteAddress.set(Hostname.getInitialInboundConnection((LoginInboundConnection) e.getConnection()).getConnection(), sanitized); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/PluginMessage.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/PluginMessage.java new file mode 100644 index 00000000..cdb36137 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/PluginMessage.java @@ -0,0 +1,507 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.listeners; + +import com.lunarclient.apollo.ApolloManager; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.messages.ChannelMessageSource; +import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.commands.TeamCommand; +import de.steamwar.velocitycore.mods.*; +import de.steamwar.velocitycore.network.ServerMetaInfo; +import de.steamwar.messages.Chatter; +import de.steamwar.network.packets.NetworkPacket; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.kyori.adventure.text.Component; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.*; +import java.util.function.Consumer; +import java.util.logging.Level; + +public class PluginMessage extends BasicListener { + + public static void send(Player player, String legacyChannel, String channel, byte[] data) { + // 1.12 format change + send(player, player.getProtocolVersion().greaterThan(ProtocolVersion.MINECRAFT_1_12_2) ? channel : legacyChannel, data); + } + + public static void send(Player player, String channel, byte[] data) { + //TODO only if player has registered channel + player.sendPluginMessage(channel.indexOf(':') != -1 ? MinecraftChannelIdentifier.from(channel) : new LegacyChannelIdentifier(channel), data); + } + + public static byte[] genBufPacket(Consumer generator) { + ByteBuf buf = Unpooled.buffer(); + generator.accept(buf); + + byte[] packet = new byte[buf.readableBytes()]; + buf.readBytes(packet); + return packet; + } + + public static byte[] genStreamPacket(StreamConsumer generator) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(stream); + + try { + generator.accept(out); + } catch (IOException e) { + throw new SecurityException("Could not create PluginMessage packet", e); + } + + return stream.toByteArray(); + } + + public interface StreamConsumer { + void accept(DataOutputStream out) throws IOException; + } + + private static final Parser UNKNOWN = event -> VelocityCore.getLogger().log(Level.WARNING, () -> "Undefined PluginMessage on channel " + event.getIdentifier() + " from " + event.getSource() + " received.\n" + new String(event.getData()) + "\n" + Arrays.toString(event.getData())); + private static final Parser PASS_THROUGH = event -> event.setResult(PluginMessageEvent.ForwardResult.forward()); + private static final Parser DROP = event -> {}; + + private final Lunar lunar = new Lunar(); + private final Badlion badlion = new Badlion(); + + private final Set knownBrands = new HashSet<>(); + private final Map> channelRegisterHandlers = new HashMap<>(); + private final Map handlers = new HashMap<>(); + + public PluginMessage() { + LabyMod labyMod = new LabyMod(); + WorldDownloader wdl = new WorldDownloader(); + + knownBrands.addAll(Arrays.asList("vanilla", "badlion", "fabric", "quilt", "forge", "optifine", "Geyser", "labymod", "Feather Fabric")); + + for(String channel : Arrays.asList( + "fabric:container/open", "fabric:registry/sync/direct", "fabric:registry/sync", + "fabric-screen-handler-api-v1:open_screen", + + FML.CHANNEL, "FML|MP", "FML", "FORGE", + "fml:loginwrapper", "fml:handshake", "fml:play", + "forge:tier_sorting", "forge:split", "forge:login", "forge:handshake", + + "labymod3:main", "labymod:neo", + "feather:client/frag", + Alpine.HANDSHAKE, Alpine.PLAY, + + "sw:hotkeys", + "floodgate:form", "floodgate:transfer", "floodgate:packet", + + "Replay|Restrict", "replaymod:restrict", + "WDL|CONTROL", "wdl:control", + "tpshud:handshake", "tpshud:tps", //https://github.com/mooziii/tpshud-fabric/tree/main + "methane_server:statepacket", //https://modrinth.com/mod/methane + "servux:structures", //https://modrinth.com/mod/servux + "architectury:spawn_entity_packet", //https://modrinth.com/mod/architectury-api + "jei:channel", "jei:cheat_permission", //https://modrinth.com/mod/jei + "owo:local_packet", "owo:sync_screen_handler_properties", //https://modrinth.com/mod/owo-lib + "minecraft:intave", //https://intave.ac seems to be a client side integration of intave with labymod 4 + "midnightcontrols:feature", "midnightcontrols:controls_mode", //https://modrinth.com/mod/midnightcontrols + "controlify:vibrate_from_origin", "controlify:vibration", "controlify:vibrate_from_entity", + "carpet:structures", //https://modrinth.com/mod/carpet + "plasmo:voice/v2/installed", "plasmo:voice/v2", //https://modrinth.com/plugin/plasmo-voice (Voice chat) + "inventorysorter:sync_blacklist_packet", //https://github.com/cpw/inventorysorter (needs server side component to work) + "emi:ping", "emi:command", "emi:chess", //https://github.com/emilyploszaj/emi/ + "fancymenu:execute_command", "fancymenu:packet_bridge", //https://github.com/Keksuccino/FancyMenu (Custom menus) + "itemswapper:enableshulker", "itemswapper:enablerefill", //https://github.com/tr7zw/ItemSwapper/tree/main (Easier inventory item swapping) + "jade:show_overlay", "jade:receive_data", "jade:server_ping", //https://github.com/Snownee/Jade (Information over block/entity under crosshair) + "bclib:hello_client", "bclib:request_files", "bclib:send_files", "bclib:chunker", //https://github.com/quiqueck/BCLib (Library for additional dimensions) + "roughlyenoughitems:ci_msg", "roughlyenoughitems:request_tags_s2c", "roughlyenoughitems:og_not_enough", //https://github.com/shedaniel/RoughlyEnoughItems (Crafting recipe helper) + "essentialclient:chunkdebug", "essentialclient:clientscript", "essentialclient:gamerule", //https://github.com/senseiwells/EssentialClient (Carpet mod extension) + "couplings:server_config", //https://github.com/ChloeDawn/Couplings (Opens/closes double doors/gates simultaneously) + "yigd:grave_overview_s2c", "yigd:grave_selection_s2c", "yigd:player_selection_s2c", //https://github.com/B1n-ry/Youre-in-grave-danger (Adds new block - graves) + "bagofholding:1/0", //https://github.com/Fuzss/bagofholding (Adds new item - bag) + "betterend:ritual_update", //https://github.com/quiqueck/BetterEnd (Dimension improvement) + "calio:sync_data_object_registry", //https://github.com/apace100/calio (Data serialization library) + "chunk_debug:batched_chunk_info", "chunk_debug:hello", //https://github.com/senseiwells/ChunkDebug (Serverside extension for EssentialClient) + "chunky:border", //https://github.com/pop4959/Chunky (Serverside chunk generator) + "commandblockide:edit_function", "commandblockide:update_function_command", //https://github.com/arm32x/command-block-ide (Command block interface) + "configured:session_data", //https://github.com/MrCrayfish/Configured (Confiuration system) + "corpse:default", //https://github.com/henkelmax/corpse/tree/master (Adds new entity - corpse) + "craftingtweaks:hello", "craftingtweaks:sync_config", //https://github.com/TwelveIterationMods/CraftingTweaks (Additional Crafting UI) + "create:main", //https://github.com/Creators-of-Create/Create (Additional redstone blocks mod) + "dummmmmmy:0", "dummmmmmy:1", "dummmmmmy:2", //https://github.com/MehVahdJukaar/DuMmmMmmy (Adding dummy target item) + "easyanvils:1/0", "easyanvils:1/2", //https://github.com/Fuzss/easyanvils (Anvil handling improvements) + "easymagic:1/0", "easymagic.main:0", //https://github.com/Fuzss/easymagic (Enchantment table handling improvements) + "enhancedvisuals:main0", "enhancedvisuals:main1", "enhancedvisuals:main2", //https://github.com/CreativeMD/EnhancedVisuals (Visual effects) + "fallingtree:configuration-packet", //https://github.com/RakambdaOrg/FallingTree (Serverside tree cutting enhancements) + "forgeconfigscreens:play/0", "forgeconfigscreens:play/1", //https://github.com/Fuzss/forgeconfigscreens (Config GUI) + "fwaystones:void_totem_revive", "fwaystones:sync_player", "fwaystones:waystone_packet", //https://github.com/LordDeatHunter/FabricWaystones (Adds new block - waystone) + "fzzy_config:sync_config_packet", //https://github.com/fzzyhmstrs/fconfig (Synchronizing configuation library) + "graveyard:spawn_entity", //https://github.com/finallion/The-Graveyard-FORGE (Adding graveyard themed blocks, items and mobs) + "immersive_weathering:0", //https://github.com/AstralOrdana/Immersive-Weathering (Adds additional random block transitions) + "kiwi:sync_cosmetic", //github.com/Snownee/Kiwi (General purpose library) + "libgui:screen_messag", "libgui:screen_message_s2c", //https://github.com/cottonmc/libgui (Ingame GUI library) + "libjf-config-network", "libjf-config-network-v0:request", "libjf-config-network-v0:response", //https://git.frohnmeyer-wds.de/JfMods/LibJF (General purpose library) + "libz:set_mouse_position", "libz:sync_config", //https://github.com/Globox1997/LibZ (General purpose library) + "moonlight:0", "moonlight:1", "moonlight:2", "moonlight:3", "moonlight:4", "moonlight:5", //https://github.com/MehVahdJukaar/Moonlight (General purpose library) + "nochatreports:sync", //https://github.com/Aizistral-Studios/No-Chat-Reports (Unsigns chat messages) + "omegaconfig:sync", //https://github.com/Draylar/omega-config (Config library) + "openpartiesandclaims:main", //https://github.com/thexaero/open-parties-and-claims (Chunk loading tool, needs server cooperation) + "owo:config_sync", //https://github.com/wisp-forest/owo-lib (GUI and config library) + "patchouli:open_book", "patchouli:reload_books", //https://github.com/VazkiiMods/Patchouli/ (Ingame user guide books) + "pickupnotifier:1/0", "pickupnotifier:1/1", //https://modrinth.com/mod/pick-up-notifier + "porting_lib:extra_entity_spawn_data", "porting_lib:open_screen", "port_lib:tier_sorting", //https://github.com/Fabricators-of-Create/Porting-Lib (Forge fabric porting library) + "showmeyourskin:config_sync", //https://github.com/enjarai/show-me-your-skin (Configurable armor rendering) + "simplerpc:serverconfig", //https://modrinth.com/mod/simple-discord-rpc (Customizable Discord rich presence) + "skinshuffle:handshake", //https://github.com/IMB11/SkinShuffle (Ingame realtime skin editor) + "toms_storage:data_s2c", //https://github.com/tom5454/Toms-Storage (Additional storage blocks) + "travelersbackpack:sync_backpack", "travelersbackpack:update_config", //https://github.com/Tiviacz1337/Travelers-Backpack (Additional backpack items) + "trinkets:break", "trinkets:sync_inventory", "trinkets:sync_slots", //https://github.com/emilyploszaj/trinkets (Additional armor slots) + "tweed4:sync_config", //https://github.com/Siphalor/tweed-api (Config library) + "umu_backpack:load", "umu_backpack:unload", "umu_config:sync_config", //https://github.com/Zemelua/UMU-Backpack (Additional backpack item) + "visualoverhaul:brewingstand", "visualoverhaul:furnace", "visualoverhaul:record", //https://github.com/TeamMidnightDust/VisualOverhaul (Graphical overhaul for certain blocks) + "walkietalkie:buttonpressedresponse", //https://github.com/Flaton1/walkie-talkie-mod (Simple voice chat walkietalkie addon) + "whereisit:s2c_founditem", "whereisit:found_item_s2c", //https://modrinth.com/mod/where-is-it (needs server side component to work) + "wildfire_gender:hurt", "wildfire_gender:sync", //https://github.com/WildfireRomeo/WildfireFemaleGenderMod (Female player model) + + //https://github.com/ZsoltMolnarrr/SpellEngine (Magic library) + "spell_engine:config_sync", "spell_engine:particle_effects", "spell_engine:spell_animation", + "spell_engine:spell_cooldown", "spell_engine:spell_registry_sync", + + //https://modrinth.com/mod/shulkerboxtooltip + "shulkerboxtooltip:s2c_handshake", "shulkerboxtooltip:ec_update", "shulkerboxtooltip:ec", + "shulkerboxtooltip:s2", + + //https://github.com/QuiltMC/quilt-standard-libraries (Quilt general purpose library) + "qsl:registry_sync/end", "qsl:registry_sync/error_style", "qsl:registry_sync/handshake", + "qsl:registry_sync/mod_protocol", "qsl:registry_sync/registry_apply", "qsl:registry_sync/registry_data", + "qsl:registry_sync/registry_restore", "qsl:registry_sync/registry_start", + "qsl:registry_sync/validate/block_states", "qsl:registry_sync/validate/fluid_states", + "quilt:extended_entity_spawn_packet", "quilt_registry_entry_attachments:sync", + + //https://github.com/apace100/origins-fabric (Minecraft player special powers mod) + "origins:badge_list", "origins:confirm_origin", "origins:layer_list", "origins:open_origin_screen", + "origins:origin_list", + + //https://github.com/skyecodes/IBE-Editor (Item data editor) + "ibeeditor:network/block_editor_response", "ibeeditor:network/block_inventory_item_editor_response", + "ibeeditor:network/editor_command", "ibeeditor:network/player_inventory_item_editor_response", + "ibeeditor:network/entity_inventory_item_editor_response", "ibeeditor:network/server_notification", + "ibeeditor:network/main_hand_item_editor_response", "ibeeditor:network/entity_editor_response", + + //https://github.com/enjarai/do-a-barrel-roll (Elytra flight visuals changes) + "do_a_barrel_roll:config_sync", "do_a_barrel_roll:handshake", "do_a_barrel_roll:player_roll", + "do_a_barrel_roll:server_config_update", + + //https://github.com/CreativeMD/CreativeCore (General purpose library) + "creativecore:main0", "creativecore:main1", "creativecore:main2", "creativecore:main3", + "creativecore:main4", "creativecore:main5", "creativecore:main6", "creativecore:main7", + + //https://github.com/Ladysnake/Cardinal-Components-API (General purpose library) + "cardinal-components:block_entity_sync", "cardinal-components:chunk_sync", + "cardinal-components:entity_sync", "cardinal-components:level_sync", "cardinal-components:world_sync", + "cardinal-components:scoreboard_sync", "cardinal-components:team_sync", + + //https://github.com/RedLime/SpeedRunIGT (Speedrunning overlay) + "speedrunigt:achieve_advancement", "speedrunigt:achieve_criteria", "speedrunigt:condition_custom", + "speedrunigt:condition_data", "speedrunigt:timer_category", "speedrunigt:timer_complete", + "speedrunigt:timer_init", "speedrunigt:timer_start", "speedrunigt:timer_timeline", + "speedrunigt:timer_uncompleted", + + //https://github.com/miyo6032/bosses-of-mass-destruction (Adds new entities) + "bosses_of_mass_destruction:change_hitbox", "bosses_of_mass_destruction:charged_ender_pearl_impact", + "bosses_of_mass_destruction:client_vec3d", "bosses_of_mass_destruction:gauntlet_blindness", + "bosses_of_mass_destruction:obsidilith_revive", "bosses_of_mass_destruction:player_velocity", + "bosses_of_mass_destruction:spawn_entity", "bosses_of_mass_destruction:void_blossom_head", + "bosses_of_mass_destruction:void_blossom_place", "bosses_of_mass_destruction:void_blossom_revive", + "bosses_of_mass_destruction:void_blossom_spikes", "bosses_of_mass_destruction:void_lily_pollen", + + //https://github.com/AzureDoom/AzureLib (Animation library) + "azurelib:anim_data_sync", "azurelib:anim_trigger_sync", "azurelibarmor:anim_data_sync", + "azurelibarmor:anim_trigger_sync", "azurelib:block_entity_anim_data_sync", + "azurelib:block_entity_anim_trigger_sync", "azurelib:entity_anim_data_sync", + "azurelib:entity_anim_trigger_sync", "azurelib:s2c_send_config_data", "azurelib:spawn_entity", + + //https://github.com/apace100/apoli (Entity power library) + "apoli:player_dismount", "apoli:player_mount", "apoli:power_list", "apoli:set_attacker", + "apoli:sync_power", "apoli:sync_status_effect", + + //https://essential.gg/ + "essential:", "essential:game_rule_hello", "essential:game_rule_permissions", + "essential:game_rules_changed", + + //https://github.com/MehVahdJukaar/Supplementaries (Additional blocks) + "supplementaries:0", "supplementaries:1", "supplementaries:2", "supplementaries:3", "supplementaries:4", + "supplementaries:5", "supplementaries:6", "supplementaries:7", "supplementaries:8", "supplementaries:9", + "supplementaries:10", "supplementaries:11", "supplementaries:12", "supplementaries:13", + "supplementaries:14", "supplementaries:24", "supplementaries:25", + + //https://www.curseforge.com/minecraft/mc-mods/waila (Information over block/entity under crosshair) + "waila:data", "waila:blacklist", "waila:config", "waila:version", "waila:generate_client_dump", + + //https://modrinth.com/plugin/simple-voice-chat (Voice chat) + "voicechat:secret", "voicechat:joined_group", "voicechat:player_states", "voicechat:player_state", + "voicechat:remove_category", "voicechat:remove_group", "voicechat:add_category", + "voicechat:leave_group", "voicechat:create_group", "voicechat:request_secret", "voicechat:set_group", + "voicechat:update_state", "voicechat:add_group", + + //https://github.com/mim1q/MineCells (Additional dimensions and gamemodes) + "minecells:obelisk_activation", "minecells:connect", "minecells:crit", "minecells:explosion", + "minecells:elevator_destroyed", "minecells:spawn_rune_particles", "minecells:sync_minecells_data", + + //https://modrinth.com/mod/appleskin (Additional food bar information) + "appleskin:exhaustion_sync", "appleskin:saturation_sync", + "appleskin:saturation", "appleskin:exhaustion", + + //https://modrinth.com/mod/puzzles-lib (General purpose library) + "puzzleslib:1/0", "puzzleslib:1/1", "puzzleslib:1/2", + "puzzleslib.main:0", "puzzleslib.main:1", "puzzleslib:play/0", + //https://github.com/Fuzss/puzzlesapi (General purpose library extension) + "puzzlesapi:1/1", "puzzlesapi:1/2", "puzzlesapi:1/5", "puzzlesapi:2/1", "puzzlesapi:2/2", + + //https://github.com/bernie-g/geckolib + "geckolib:block_entity_anim_trigger_sync", "geckolib:entity_anim_trigger_sync", + "geckolib:block_entity_anim_data_sync", "geckolib:anim_data_sync", + "geckolib:entity_anim_data_sync", "geckolib:anim_trigger_sync", + + //https://github.com/Noxcrew/noxesium (MC Championship helper) + "noxesium:server_rules", + "noxesium-v1:reset", "noxesium-v1:change_server_rules", "noxesium-v1:server_info", + "noxesium-v1:mcc_server", "noxesium-v1:mcc_game_state", "noxesium-v1:reset_server_rules", + "noxesium-v1:stop_sound", "noxesium-v1:start_sound", "noxesium-v1:modify_sound", + "noxesium-v2:reset", "noxesium-v2:change_server_rules", "noxesium-v2:server_info", + "noxesium-v2:mcc_server", "noxesium-v2:mcc_game_state", "noxesium-v2:reset_server_rules", + "noxesium-v2:stop_sound", "noxesium-v2:start_sound", "noxesium-v2:modify_sound" + )) + channelRegisterHandlers.put(channel, player -> {}); + + channelRegisterHandlers.put(ApolloManager.PLUGIN_MESSAGE_CHANNEL, lunar::sendRestrictions); + channelRegisterHandlers.put(Feather.CHANNEL, new Feather()::sendRestrictions); + channelRegisterHandlers.put("openboatutils:settings", player -> send(player, "openboatutils:settings", new byte[] { 0, 0 })); //https://github.com/o7Moon/OpenBoatUtils/wiki/Packets (Reset packet) + channelRegisterHandlers.put("itemswapper:disable", player -> send(player, "itemswapper:disable", new byte[]{ 0 })); //https://github.com/tr7zw/ItemSwapper/blob/main/src/main/java/dev/tr7zw/itemswapper/packets/DisableModPayload.java + channelRegisterHandlers.put("xaerominimap:main", player -> player.sendMessage(Component.text("§n§o§m§i§n§i§m§a§p"))); //https://www.curseforge.com/minecraft/mc-mods/xaeros-minimap + channelRegisterHandlers.put("litemoretica:init_easy_place", player -> Chatter.disconnect(player).prefixless("MOD_YELLOW_SING", "litematica")); //https://github.com/Earthcomputer/litemoretica/tree/master + channelRegisterHandlers.put("litemoretica:init_ea", player -> Chatter.disconnect(player).prefixless("MOD_YELLOW_SING", "litematica")); //https://github.com/Earthcomputer/litemoretica/tree/master + channelRegisterHandlers.put("voxelmap:settings", player -> Chatter.disconnect(player).prefixless("MOD_YELLOW_SING", "voxelmap")); //https://modrinth.com/mod/voxelmap-updated undocumented + channelRegisterHandlers.put(Controlify.CHANNEL, new Controlify()::onRegister); + + for(String channel : Arrays.asList( + "worldinfo:world_id", // JourneyMap and VoxelMap + "journeymap:version", "journeymap:admin_req", "journeymap:mp_options_req", "journeymap:waypoint", + "journeymap:player_loc", "journeymap:admin_save", "journeymap:teleport_req", "journeymap:common", + "journeymap:perm_req" + )) + channelRegisterHandlers.put(channel, player -> Chatter.disconnect(player).prefixless("MOD_YELLOW_SING", "minimap")); + + for(String channel : Arrays.asList("bedrockify:cauldron_particles", "bedrockify:eat-particles")) //https://github.com/juancarloscp52/BedrockIfy (Bedrock features on Java, banned for reach-around block placement) + channelRegisterHandlers.put(channel, player -> Chatter.disconnect(player).prefixless("MOD_YELLOW_SING", "bedrockify")); + + registerBiDirPassthrough("WECUI", "worldedit:cui", "worldedit:internal", "minecraft:wecui"); + + registerPassthroughToClient( + "axiom:enable", "axiom:initialize_hotbars", + "axiom:response_chunk_data", "axiom:register_world_properties", "axiom:set_world_property", + "axiom:ack_world_properties", "axiom:restrictions", "axiom:marker_data", "axiom:marker_nbt_response", + "axiom:custom_blocks", "axiom:editor_warning", "axiom:blueprint_manifest", "axiom:response_blueprint" + ); + registerBiDirPassthrough("axiom:handle_big_payload", "axiom:set_editor_views"); + for(String channel : Arrays.asList( + "axiom:hello", "axiom:set_gamemode", "axiom:set_fly_speed", "axiom:set_world_time", + "axiom:set_world_property", "axiom:set_block", "axiom:set_hotbar_slot", "axiom:switch_active_hotbar", + "axiom:teleport", "axiom:request_chunk_data", "axiom:spawn_entity", + "axiom:manipulate_entity", "axiom:delete_entity", "axiom:marker_nbt_request", "axiom:set_buffer" + )) { + channelRegisterHandlers.put(channel, player -> {}); + registerPassthroughToServer(channel); + } + + for(String channel : Arrays.asList( + "floodgate:skin", + "watut:nbt", //https://github.com/Corosauce/WATUT + "bclib:hello_server", + "vivecraft:data", //https://github.com/Vivecraft/VivecraftMod https://github.com/jrbudda/Vivecraft_Spigot_Extensions https://github.com/Techjar/Vivecraft_BungeeCord_Extensions (VR support) + "badpackets:channel_sync" //https://github.com/badasintended/badpackets (Forge fabric translation layer) + )) { + channelRegisterHandlers.put(channel, player -> {}); + register(channel, false, directional(UNKNOWN, DROP)); + } + + for(String channel : Arrays.asList( + "UNREGISTER", "minecraft:unregister", // used by carpet and servux + "WDL|REQUEST", "wdl:request", + "minecraft:intave", //undocumented, byte stringlength, clientconfig, byte length, json {"legacySneakHeight":false,"legacyOldRange":false,"legacyOldSlowdown":false} + "waila:entity", "waila:block", + "lambdacontrols:hello", + "midnightcontrols:controls_mode", + "inventorysorter:sync_settings_packet", + "voicechat:request_secret", "voicechat:update_state", + "shulkerboxtooltip:c2s_handshake", + "noxesium:client_information", "noxesium:client_settings", + "polymer:handshake", //https://github.com/Patbox/polymer + "minecells:request_sync_minecells_data", + "puzzlesapi:1/4", + "fancymenu:variable_cmd_sugg", "fancymenu:packet_bridge", + "jade:request_entity", "jade:request_tile", + "tiscm:network/v1", //https://github.com/gnembon/fabric-carpet + "couplings:client_config", + "yigd:config_update_c2s", + "kiwi:set_cosmetic", + "fwaystones:request_player_waystone_update", + "openboatutils:settings", //https://github.com/o7Moon/OpenBoatUtils + "block-event-separator:handshake", //https://github.com/SpaceWalkerRS/block-event-separator (Separating block events) + "oth3r-sit:settings_v1.1" //https://github.com/Oth3r/Sit (Sitting mod) + )) + register(channel, false, directional(UNKNOWN, DROP)); + + register("REGISTER", false, directional(this::serverRegistersChannel, this::clientRegistersChannel)); + register("minecraft:register", false, directional(this::serverRegistersChannel, this::clientRegistersChannel)); + + register("MC|Brand", false, directional(this::steamWarBrand, this::userBrand)); + register("minecraft:brand", false, directional(this::steamWarBrand, this::userBrand)); + + //Needs to be registered cause paper refuses to send PluginMessages on unregistered channels... + register("sw:bridge", true, directional(onlySWSource(async(event -> NetworkPacket.handle(new ServerMetaInfo((ServerConnection) event.getSource()), event.getData()))), UNKNOWN)); + registerPassthroughToServer("sw:hotkeys"); + register("fabricmodsender:mods", true, directional(UNKNOWN, async(new FabricModSender()::handlePluginMessage))); + + register("WDL|INIT", true, directional(UNKNOWN, wdl::handlePluginMessage)); + register("wdl:init", true, directional(UNKNOWN, wdl::handlePluginMessage)); + + register(ApolloManager.PLUGIN_MESSAGE_CHANNEL, true, async(lunar::handlePluginMessage)); + register(Alpine.HANDSHAKE, false, directional(UNKNOWN, new Alpine()::handleHandshakeMessage)); + register("LMC", true, directional(UNKNOWN, async(labyMod::handlePluginMessage))); + register("labymod3:main", true, directional(UNKNOWN, async(labyMod::handlePluginMessage))); + register("labymod:neo", false, directional(UNKNOWN, DROP)); //undocumented, JSON format "0" byte, packetlängen byte, {"version":"4.1.25"} + register(FML.CHANNEL, true, directional(UNKNOWN, async(new FML()::handlePluginMessage))); + + //vanilla does not register any channels (sends only one minecraft:brand vanilla, nothing else (potential spoofed client detection)) + //Forge interestingly registers all channels the server registers + //meteor https://github.com/MeteorDevelopment/meteor-client/blob/master/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/ServerSpoof.java https://github.com/MeteorDevelopment/meteor-client/blob/master/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/DiscordPresence.java + //litematica/malilib https://github.com/maruohon/litematica/issues/75 https://github.com/maruohon/malilib/blob/liteloader_1.12.2/src/main/java/malilib/network/message/ConfigLockPacketHandler.java#L65 + // Hackclientlike modsuppressor for labymod: https://github.com/Neocraftr/LabyMod-NeoEssentials (Potentially recognizable from NO Addons/NO Mods?) https://github.com/Neocraftr/LabyMod-NeoEssentials/blob/master/src/main/java/de/neocraftr/neoessentials/utils/BytecodeMethods.java + } + + @Subscribe + public void onPluginMessage(PluginMessageEvent event) { + event.setResult(PluginMessageEvent.ForwardResult.handled()); + + try { + handlers.getOrDefault(event.getIdentifier().getId(), UNKNOWN).handle(event); + } catch (Exception e) { + throw new SecurityException("PluginMessage handling exception: " + event + "\n" + Arrays.toString(event.getData()), e); + } + } + + private void registerPassthroughToClient(String... channels) { + for(String channel : channels) { + channelRegisterHandlers.put(channel, player -> {}); + register(channel, false, directional(PASS_THROUGH, UNKNOWN)); + } + } + + private void registerPassthroughToServer(String... channels) { + for(String channel : channels) + register(channel, false, directional(UNKNOWN, PASS_THROUGH)); + } + + private void registerBiDirPassthrough(String... channels) { + for(String channel : channels) { + channelRegisterHandlers.put(channel, player -> {}); + register(channel, false, PASS_THROUGH); + } + } + + private void register(String channel, boolean clientSideRegister, Parser handler) { + handlers.put(channel, handler); + if(clientSideRegister) + VelocityCore.getProxy().getChannelRegistrar().register(channel.indexOf(':') != -1 ? MinecraftChannelIdentifier.from(channel) : new LegacyChannelIdentifier(channel)); + } + + private void clientRegistersChannel(PluginMessageEvent event) { + Player player = (Player) event.getSource(); + + for(String channel : new String(event.getData()).split("\0")) { + channelRegisterHandlers.getOrDefault(channel, p -> VelocityCore.getLogger().log(Level.WARNING, () -> p.getUsername() + " registered unknown channel " + channel)).accept(player); + } + + PASS_THROUGH.handle(event); + } + + private void serverRegistersChannel(PluginMessageEvent event) { + Player player = (Player) event.getTarget(); + + List channels = new ArrayList<>(Arrays.asList(new String(event.getData()).split("\0"))); + channels.removeIf(channel -> channel.equals("sw:bridge")); + send(player, "REGISTER", "minecraft:register", String.join("\0", channels).getBytes()); + } + + private void userBrand(PluginMessageEvent event) { + Player player = (Player) event.getSource(); + + ByteBuf buf = Unpooled.wrappedBuffer(event.getData()); + String brand = ProtocolUtils.readString(buf); + boolean lunarclient = brand.startsWith("lunarclient:"); + + VelocityCore.getLogger().log(knownBrands.contains(brand) || lunarclient ? Level.INFO : Level.WARNING, () -> player.getUsername() + " joins with brand: " + brand); + if(lunarclient) + lunar.sendRestrictions(player); + if(brand.equals("badlion")) + badlion.sendRestrictions(player); + + PASS_THROUGH.handle(event); + } + + private void steamWarBrand(PluginMessageEvent event) { + Player player = (Player) event.getTarget(); + String brand = Chatter.of(player).parseToLegacy("STEAMWAR_BRAND", "Velocity", player.getCurrentServer().map(serverConnection -> serverConnection.getServerInfo().getName()).orElse(""), new String(event.getData(), 1, event.getData().length - 1)); + player.sendPluginMessage(event.getIdentifier(), genBufPacket(buf -> ProtocolUtils.writeString(buf, brand))); + } + + + private Parser directional(Parser fromServer, Parser fromPlayer) { + return event -> { + if(event.getSource() instanceof Player) + fromPlayer.handle(event); + else + fromServer.handle(event); + }; + } + + private Parser onlySWSource(Parser parser) { + return event -> { + ChannelMessageSource sender = event.getSource(); + if(TeamCommand.isLocalhost(sender instanceof Player player ? IPSanitizer.getTrueAddress(player) : ((ServerConnection) sender).getServerInfo().getAddress().getAddress())) + parser.handle(event); + else + UNKNOWN.handle(event); + }; + } + + private Parser async(Parser parser) { + return event -> VelocityCore.schedule(() -> parser.handle(event)).schedule(); + } + + private interface Parser { + void handle(PluginMessageEvent event); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/PollSystem.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/PollSystem.java new file mode 100644 index 00000000..c0278699 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/PollSystem.java @@ -0,0 +1,91 @@ +/* + 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.velocitycore.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import de.steamwar.velocitycore.Config; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.command.TypeValidator; +import de.steamwar.messages.Chatter; +import de.steamwar.messages.Message; +import de.steamwar.sql.PollAnswer; +import net.kyori.adventure.text.event.ClickEvent; + +public class PollSystem extends BasicListener { + + public static void init() { + poll = VelocityCore.get().getConfig().getPoll(); + if(poll == null) + return; + + if(noCurrentPoll()) + return; + + PollAnswer.setCurrentPoll(poll.getQuestion()); + new PollSystem(); + } + + private static Config.Poll poll = null; + + + @Subscribe + public void onPostLogin(PostLoginEvent event){ + Chatter player = Chatter.of(event.getPlayer()); + + PollAnswer answer = PollAnswer.get(player.user().getId()); + if(answer.hasAnswered()) + return; + + sendPoll(player); + } + + public static void sendPoll(Chatter player) { + player.system("POLL_HEADER"); + player.prefixless("POLL_HEADER2"); + player.prefixless("POLL_QUESTION", poll.getQuestion()); + + for(int i = 1; i <= poll.getAnswers().size(); i++) { + player.prefixless("POLL_ANSWER", new Message("POLL_ANSWER_HOVER", poll.getAnswers().get(i-1)), ClickEvent.runCommand("/poll " + i), poll.getAnswers().get(i-1)); + } + } + + private static boolean noCurrentPoll(){ + return poll == null; + } + + public static TypeValidator noPoll() { + return (sender, value, messageSender) -> { + if (PollSystem.noCurrentPoll()) { + messageSender.send("POLL_NO_POLL"); + return false; + } + return true; + }; + } + + public static int answers(){ + return poll.getAnswers().size(); + } + + public static String getAnswer(int i) { + return poll.getAnswers().get(i); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/SessionManager.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/SessionManager.java new file mode 100644 index 00000000..7caeac14 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/SessionManager.java @@ -0,0 +1,48 @@ +/* + 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.velocitycore.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.sql.Session; +import de.steamwar.sql.SteamwarUser; + +import java.sql.Timestamp; +import java.time.Instant; + +import static de.steamwar.persistent.Storage.sessions; + +public class SessionManager extends BasicListener { + + @Subscribe + public void onPostLogin(PostLoginEvent event){ + sessions.put(event.getPlayer(), Timestamp.from(Instant.now())); + } + + @Subscribe + public void onDisconnect(DisconnectEvent e){ + Timestamp timestamp = sessions.remove(e.getPlayer()); + if(timestamp != null) { + VelocityCore.schedule(() -> Session.insertSession(SteamwarUser.get(e.getPlayer().getUniqueId()).getId(), timestamp)).schedule(); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/SettingsChangedListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/SettingsChangedListener.java new file mode 100644 index 00000000..59693af5 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/SettingsChangedListener.java @@ -0,0 +1,41 @@ +/* + * 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.velocitycore.listeners; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent; +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.network.packets.server.LocaleInvalidationPacket; + +public class SettingsChangedListener extends BasicListener { + + @Subscribe + public void onSettingsChanged(PlayerSettingsChangedEvent event) { + VelocityCore.schedule(() -> { + Player player = event.getPlayer(); + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + user.setLocale(event.getPlayerSettings().getLocale(), false); + NetworkSender.send(player, new LocaleInvalidationPacket(user.getId())); + }).schedule(); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/Alpine.java b/VelocityCore/src/de/steamwar/velocitycore/mods/Alpine.java new file mode 100644 index 00000000..82271dc9 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/Alpine.java @@ -0,0 +1,71 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.listeners.PluginMessage; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessagePacker; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class Alpine { + //https://github.com/alpine-client/alpine-client-api + + public static final String HANDSHAKE = "ac:handshake"; + public static final String PLAY = "ac:play"; + + private final byte[] magicHandshakeResponse = new byte[] {0x3A, 0x3D}; //https://github.com/alpine-client/alpine-client-api/blob/master/src/main/java/com/alpineclient/plugin/listener/plugin/HandshakeListener.java#L19 + private final byte[] modulesPacket; + + public Alpine() { + //https://github.com/alpine-client/alpine-client-api/blob/master/MODULES.md + String[] disabledModules = new String[] { + "armor_status", "cannon_playback", "cannon_view", "clear_water", "explosion_boxes", "fullbright", + "hit_color", "inventory_tweaks", "low_hp_tint", "minimap", "perspective_mod", "potion_status", + "reach_display", "schematica", "tnt_timer", "toggle_sneak_sprint" + }; + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try (MessagePacker packer = MessagePack.newDefaultPacker(stream)) { + //https://github.com/alpine-client/alpine-client-api/blob/master/src/main/java/com/alpineclient/plugin/network/Packet.java#L103 + packer.packInt(0xB0); + + //https://github.com/alpine-client/alpine-client-api/blob/master/src/main/java/com/alpineclient/plugin/network/packet/PacketModules.java + packer.packArrayHeader(disabledModules.length); + for (String module : disabledModules) { + packer.packString(module); + packer.packBoolean(false); //https://github.com/alpine-client/alpine-client-api/blob/master/src/main/java/com/alpineclient/plugin/config/impl/GeneralConfig.java + } + } catch (IOException e) { + throw new SecurityException(e); + } + + modulesPacket = stream.toByteArray(); + } + + public void handleHandshakeMessage(PluginMessageEvent event) { + Player player = (Player) event.getSource(); + PluginMessage.send(player, HANDSHAKE, magicHandshakeResponse); + PluginMessage.send(player, PLAY, modulesPacket); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/Badlion.java b/VelocityCore/src/de/steamwar/velocitycore/mods/Badlion.java new file mode 100644 index 00000000..74c18eb0 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/Badlion.java @@ -0,0 +1,55 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.google.gson.JsonObject; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; + +public class Badlion { + // https://github.com/BadlionClient/BadlionClientModAPI + + private final byte[] packet; + + public Badlion() { //TODO check if working or (json) modsDisallowed wrapper necessary + JsonObject disabled = new JsonObject(); + disabled.addProperty("disabled", true); + + JsonObject json = new JsonObject(); + json.add("Clear Glass", disabled); + json.add("ClearWater", disabled); + json.add("FOV Changer", disabled); + json.add("Hitboxes", disabled); + json.add("LevelHead", disabled); + json.add("MiniMap", disabled); + json.add("MLG Cobweb", disabled); + json.add("Replay", disabled); //TODO check if ReplayMod restrictions work + json.add("Schematica", disabled); + json.add("ToggleSneak", disabled); + json.add("ToggleSprint", disabled); + json.add("TNT Time", disabled); + + packet = json.toString().getBytes(); + } + + public void sendRestrictions(Player player) { + player.sendPluginMessage(MinecraftChannelIdentifier.from("badlion:mods"), packet); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/Controlify.java b/VelocityCore/src/de/steamwar/velocitycore/mods/Controlify.java new file mode 100644 index 00000000..56b7f224 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/Controlify.java @@ -0,0 +1,53 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import de.steamwar.velocitycore.listeners.PluginMessage; + +public class Controlify { + //https://modrinth.com/mod/controlify + //https://github.com/isXander/Controlify/blob/1.20.x/dev/src/main/java/dev/isxander/controlify/server/ServerPolicyPacket.java + //https://github.com/isXander/Controlify/blob/1.20.x/dev/src/main/java/dev/isxander/controlify/server/ServerPolicies.java + + public static final String CHANNEL = "controlify:server_policy"; + + private final byte[][] packets; + public Controlify() { + packets = new byte[][] { + restrict("reachAround"), + restrict("disableFlyDrifting") + }; + } + + private byte[] restrict(String name) { + return PluginMessage.genBufPacket(buf -> { + ProtocolUtils.writeString(buf, name); + buf.writeBoolean(false); + }); + } + + public void onRegister(Player player) { + for(byte[] packet : packets) + player.sendPluginMessage(MinecraftChannelIdentifier.from(CHANNEL), packet); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/FML.java b/VelocityCore/src/de/steamwar/velocitycore/mods/FML.java new file mode 100644 index 00000000..13d3497a --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/FML.java @@ -0,0 +1,95 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.BasicListener; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.Mod; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +public class FML extends BasicListener { + // https://wiki.vg/Minecraft_Forge_Handshake#FML_protocol_.281.7_-_1.12.29 + + public static final String CHANNEL = "FML|HS"; + private final byte[] helloPacket = new byte[]{ + /* Packet type: ServerHello */ 0, + /* FML protocol version */ 2, + /* Override dimension (int) */ 0, 0, 0, 0 + }; + + private static final Set unlocked = new HashSet<>(); + + @Subscribe + public void onPostLogin(PostLoginEvent event) { + Player player = event.getPlayer(); + + synchronized (unlocked) { + if(unlocked.contains(player.getUniqueId())){ + unlocked.remove(player.getUniqueId()); + return; + } + } + + //if(isFML(player, "\0FML\0")) + if(player.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_13)) + player.sendPluginMessage(new LegacyChannelIdentifier(CHANNEL), helloPacket); + } + + public void handlePluginMessage(PluginMessageEvent event) { + Player p = (Player) event.getSource(); + ByteBuf buf = Unpooled.wrappedBuffer(event.getData()); + + if (buf.readByte() == /* ModList */ 2) { + int numMods = ProtocolUtils.readVarInt(buf); + + List mods = new ArrayList<>(); + for(int i = 0; i < numMods; i++) { + String name = ProtocolUtils.readString(buf); + ProtocolUtils.readString(buf); // version + + mods.add(Mod.getOrCreate(name, Mod.Platform.FORGE)); + } + + if (ModUtils.handleMods(p, mods)) { + synchronized (unlocked) { + unlocked.add(p.getUniqueId()); + } + Chatter.disconnect(p).system("MODS_CHECKED"); + VelocityCore.schedule(() -> { + synchronized (unlocked) { + unlocked.remove(p.getUniqueId()); + } + }).delay(30, TimeUnit.SECONDS).schedule(); + } + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/FML2.java b/VelocityCore/src/de/steamwar/velocitycore/mods/FML2.java new file mode 100644 index 00000000..fa0917dd --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/FML2.java @@ -0,0 +1,176 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PreLoginEvent; +import com.velocitypowered.api.proxy.LoginPhaseConnection; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import com.velocitypowered.proxy.connection.client.LoginInboundConnection; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.BasicListener; +import de.steamwar.velocitycore.listeners.PluginMessage; +import de.steamwar.sql.Mod; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.AllArgsConstructor; +import net.kyori.adventure.text.Component; + +import java.util.*; +import java.util.logging.Level; + +public class FML2 extends BasicListener { + // FML2: https://wiki.vg/Minecraft_Forge_Handshake#FML2_protocol_.281.13_-_Current.29 + // FML3: https://github.com/adde0109/Ambassador/tree/non-api/src/main/java/org/adde0109/ambassador/forge + + // FORGE: https://github.com/MinecraftForge/MinecraftForge/blob/1.20.x/src/main/java/net/minecraftforge/network/NetworkInitialization.java + // FORGE: https://github.com/MinecraftForge/MinecraftForge/blob/1.20.x/src/main/java/net/minecraftforge/network/ForgePacketHandler.java + // FORGE: https://github.com/MinecraftForge/MinecraftForge/blob/1.20.x/src/main/java/net/minecraftforge/network/packets/ModVersions.java + + public static boolean isFML(LoginInboundConnection connection, String type) { + return Hostname.getExtraHandshakeData(connection).equals(type); + } + + private final byte[] fml2ModListPacket; + private final byte[] fml3ModListPacket; + private final byte[] forgeModListPacket; + + public FML2() { + fml2ModListPacket = generateModListPacket(false); + fml3ModListPacket = generateModListPacket(true); + forgeModListPacket = PluginMessage.genBufPacket(buf -> { + buf.writeByte(0); // Login wrapper packet + ProtocolUtils.writeString(buf, "forge:handshake"); + + ByteBuf packet = Unpooled.buffer(); + packet.writeByte(1); // Mod list packet + ProtocolUtils.writeVarInt(packet, 0); // Mod amount + + ProtocolUtils.writeVarInt(buf, packet.readableBytes()); + buf.writeBytes(packet); + }); + } + + @Subscribe + public void onLogin(PreLoginEvent event) { + LoginInboundConnection connection = (LoginInboundConnection) event.getConnection(); + + boolean fml2 = isFML(connection, "\0FML2\0"); + boolean fml3 = isFML(connection, "\0FML3\0"); + boolean forge = isFML(connection, "\0FORGE"); + if(!fml2 && !fml3 && !forge) + return; + + FML2LoginHandler handler = new FML2LoginHandler(connection, event.getUniqueId(), forge); + + if(forge) { + connection.sendLoginPluginMessage(MinecraftChannelIdentifier.from("forge:login"), forgeModListPacket, handler); + } else { + connection.sendLoginPluginMessage(MinecraftChannelIdentifier.from("fml:loginwrapper"), fml3 ? fml3ModListPacket : fml2ModListPacket, handler); + } + } + + private byte[] generateModListPacket(boolean fml3) { + return PluginMessage.genBufPacket(buf -> { + ProtocolUtils.writeString(buf, "fml:handshake"); + + ByteBuf packet = Unpooled.buffer(); + packet.writeByte(1); // Mod list packet + ProtocolUtils.writeVarInt(packet, 0); // Mod amount + + if(fml3) { + ProtocolUtils.writeVarInt(packet,1); // Channel amount + ProtocolUtils.writeString(packet, "forge:tier_sorting"); + ProtocolUtils.writeString(packet, "1.0"); + } else { + ProtocolUtils.writeVarInt(packet, 0); // Channel amount + } + + ProtocolUtils.writeVarInt(packet, 0); // Registries amount + if(fml3) + ProtocolUtils.writeVarInt(packet, 0); // DataPacks amount + + ProtocolUtils.writeVarInt(buf, packet.readableBytes()); + buf.writeBytes(packet); + }); + } + + @AllArgsConstructor + private static class FML2LoginHandler implements LoginPhaseConnection.MessageConsumer { + + private final LoginInboundConnection connection; + private final UUID uuid; + private final boolean forge; + + @Override + public String toString() { + return "SteamWar Forge Handler"; + } + + @Override + public void onMessageResponse(byte[] data) { + if(data == null) { + abort(null, "Not FML2/3 client"); + return; + } + + ByteBuf buf = Unpooled.wrappedBuffer(data); + if(forge && buf.readByte() != 0) { + abort(data, "Not FORGE login wrapper"); + return; + } + + if(!ProtocolUtils.readString(buf).equals(forge ? "forge:handshake" : "fml:handshake")) { + abort(data, "Not FML2/3/FORGE handshake response"); + return; + } + + if(ProtocolUtils.readVarInt(buf) != buf.readableBytes()) { + abort(data, "FML2/3/FORGE packet size mismatch"); + return; + } + + if(ProtocolUtils.readVarInt(buf) != (forge ? /* Mod Versions */ 1 : /* Mod List Reply */ 2)) { + abort(data, "Not FML2/3/FORGE mod list reply"); + return; + } + + List mods = new ArrayList<>(); + + int modCount = ProtocolUtils.readVarInt(buf); + for(int i = 0; i < modCount; i++) { + mods.add(Mod.getOrCreate(ProtocolUtils.readString(buf), Mod.Platform.FORGE)); + + if(forge) { + ProtocolUtils.readString(buf); // Human readable name + ProtocolUtils.readString(buf); // Version + } + } + + ModUtils.handleMods(uuid, Locale.getDefault(), connection::disconnect, mods); + } + + private void abort(byte[] response, String error) { + connection.disconnect(Component.text(error)); + VelocityCore.getLogger().log(Level.SEVERE, () -> error + "\n" + Base64.getEncoder().encodeToString(response)); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/FabricModSender.java b/VelocityCore/src/de/steamwar/velocitycore/mods/FabricModSender.java new file mode 100644 index 00000000..2f492a88 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/FabricModSender.java @@ -0,0 +1,144 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import de.steamwar.persistent.Storage; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.BasicListener; +import de.steamwar.sql.Mod; +import de.steamwar.sql.SWException; +import de.steamwar.sql.SteamwarUser; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +public class FabricModSender extends BasicListener { + + private final Set neededFabricMods = new HashSet<>(); + private final Set neededQuiltMods = new HashSet<>(); + + public FabricModSender() { + neededFabricMods.add("java"); + neededFabricMods.add("minecraft"); + neededFabricMods.add("steamwarmodsender"); + neededQuiltMods.addAll(neededFabricMods); + + neededFabricMods.add("fabricloader"); + neededQuiltMods.add("quilt_loader"); + + VelocityCore.schedule(() -> { + synchronized (Storage.fabricExpectPluginMessage) { + for (Map.Entry entry : Storage.fabricExpectPluginMessage.entrySet()) { + if (!Storage.fabricCheckedPlayers.containsKey(entry.getKey())) { + continue; + } + if (System.currentTimeMillis() - entry.getValue() > TimeUnit.SECONDS.toMillis(20)) { + Storage.fabricExpectPluginMessage.remove(entry.getKey()); + return; + } + } + } + }).repeat(1, TimeUnit.SECONDS).schedule(); + } + + public void handlePluginMessage(PluginMessageEvent e){ + Player player = (Player) e.getSource(); + SteamwarUser user = SteamwarUser.get(player.getUniqueId()); + + if (!Storage.fabricCheckedPlayers.containsKey(player)) { + synchronized (Storage.fabricExpectPluginMessage) { + if (Storage.fabricExpectPluginMessage.containsKey(player)) { + logMessage(user, "Was not fabric checked but send message nonetheless", Arrays.toString(e.getData())); + return; + } + } + } + Storage.fabricExpectPluginMessage.remove(player); + + List mods = new ArrayList<>(); + + ByteBuf buf = Unpooled.wrappedBuffer(e.getData()); + String data = ProtocolUtils.readString(buf, 1024*1024); + if(buf.readableBytes() > 0) { + logMessage(user, "Invalid message length", Arrays.toString(e.getData())); + return; + } + + JsonArray array = JsonParser.parseString(data).getAsJsonArray(); + + for(JsonElement mod : array) { + mods.add(Mod.getOrCreate(mod.getAsString(), Mod.Platform.FABRIC)); + } + + if(!neededModsContained(neededFabricMods, mods) && !neededModsContained(neededQuiltMods, mods)) { + logMessage(user, "Needed mods are not contained", data); + return; + } + + if(!ModUtils.handleMods(player,mods)) + return; + + if (!Storage.fabricCheckedPlayers.containsKey(player)) { + Storage.fabricCheckedPlayers.put(player, data.hashCode()); + } else if (Storage.fabricCheckedPlayers.get(player) != data.hashCode()) { + logMessage(user, "Mods changed during runtime", data); + } + } + + @Subscribe + public void onServerSwitchEvent(ServerConnectedEvent e) { + if (e.getPreviousServer().isEmpty()) return; + synchronized (Storage.fabricExpectPluginMessage) { + Storage.fabricExpectPluginMessage.put(e.getPlayer(), System.currentTimeMillis()); + } + } + + @Subscribe + public void onDisconnect(DisconnectEvent e) { + Player player = e.getPlayer(); + + Storage.fabricCheckedPlayers.remove(player); + synchronized (Storage.fabricExpectPluginMessage) { + Storage.fabricExpectPluginMessage.remove(player); + } + } + + private boolean neededModsContained(Set neededMods, List mods) { + return mods.stream() + .map(Mod::getModName) + .filter(neededMods::contains) + .count() == neededFabricMods.size(); + } + + private void logMessage(SteamwarUser user, String reason, String data) { + SWException.log("FabricModSender " + user.getUserName() + ": " + reason, data); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/Feather.java b/VelocityCore/src/de/steamwar/velocitycore/mods/Feather.java new file mode 100644 index 00000000..34fb7f92 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/Feather.java @@ -0,0 +1,57 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; + +public class Feather { + //https://github.com/Koupah/Feather-Client-API/blob/main/src/club/koupah/feather/packets/FeatherMod.java + //https://archive.org/details/feather-server-api + public static final String CHANNEL = "feather:client"; + + private final byte[] packet; + public Feather() { + JsonArray array = new JsonArray(); + array.add("clearWater"); + array.add("coordinates"); + array.add("coordinates"); + array.add("hitbox"); + array.add("hypixel"); + array.add("reachDisplay"); + array.add("snaplook"); + array.add("toggleSprint"); + + JsonObject mods = new JsonObject(); + mods.add("mods", array); + + JsonObject obj = new JsonObject(); + obj.addProperty("packetType", "DISABLE_MODS"); + obj.add("payload", mods); + + packet = obj.toString().getBytes(); + } + + public void sendRestrictions(Player player) { + player.sendPluginMessage(MinecraftChannelIdentifier.from(CHANNEL), packet); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/Hostname.java b/VelocityCore/src/de/steamwar/velocitycore/mods/Hostname.java new file mode 100644 index 00000000..dfba3612 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/Hostname.java @@ -0,0 +1,84 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.ConnectionHandshakeEvent; +import com.velocitypowered.proxy.connection.client.InitialInboundConnection; +import com.velocitypowered.proxy.connection.client.LoginInboundConnection; +import com.velocitypowered.proxy.protocol.packet.HandshakePacket; +import de.steamwar.persistent.Reflection; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.BasicListener; + +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Level; + +public class Hostname extends BasicListener { + + private static final Reflection.Field delegate = new Reflection.Field<>(LoginInboundConnection.class, "delegate"); + public static InitialInboundConnection getInitialInboundConnection(LoginInboundConnection loginInboundConnection) { + return delegate.get(loginInboundConnection); + } + + private static final Reflection.Field handshake = new Reflection.Field<>(InitialInboundConnection.class, "handshake"); + public static String getExtraHandshakeData(LoginInboundConnection loginInboundConnection) { + HandshakePacket handshakePacket = handshake.get(getInitialInboundConnection(loginInboundConnection)); + int i = handshakePacket.getServerAddress().indexOf('\0'); + if(i == -1) + return ""; + + return handshakePacket.getServerAddress().substring(i); + } + + private final Set knownHostnames = new HashSet<>(); + private final Set knownExtraData = new HashSet<>(); + + public Hostname() { + knownHostnames.add("steamwar.de"); + knownHostnames.add("78.31.71.136"); + knownHostnames.add("memewar.de"); // Chaoscaot + knownHostnames.add("dampfkrieg.de"); // Chaoscaot + knownHostnames.add("127.0.0.1"); // Geyser + + knownHostnames.add("@mat:matdoes.dev "); //https://github.com/mat-1/matscan + knownHostnames.add("wtf.mynx.lol"); //https://discord.com/invite/serverseeker + knownHostnames.add("masscan"); + knownHostnames.add("aaa"); + + knownExtraData.add(""); + knownExtraData.add("\0FML\0"); + knownExtraData.add("\0FML2\0"); + knownExtraData.add("\0FML3\0"); + knownExtraData.add("\0FORGE"); + } + + @Subscribe + public void onHandshake(ConnectionHandshakeEvent event) { + String hostname = event.getConnection().getVirtualHost().orElseThrow().getHostName().toLowerCase(); + if (!knownHostnames.contains(hostname) && !hostname.endsWith(".steamwar.de")) + VelocityCore.getLogger().log(Level.WARNING, () -> event.getConnection().getRemoteAddress() + " connected with unknown hostname " + hostname); + + String extraData = getExtraHandshakeData((LoginInboundConnection) event.getConnection()); + if (!knownExtraData.contains(extraData)) + VelocityCore.getLogger().log(Level.WARNING, () -> event.getConnection().getRemoteAddress() + " connected with unknown extra data " + extraData); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/LabyMod.java b/VelocityCore/src/de/steamwar/velocitycore/mods/LabyMod.java new file mode 100644 index 00000000..1db3c085 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/LabyMod.java @@ -0,0 +1,87 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.PluginMessage; +import de.steamwar.sql.Mod; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; + +public class LabyMod { + // https://docs.labymod.net/pages/server/introduction/ + // https://github.com/LabyMod/labymod-server-api + // https://dl.labymod.net/addons.json + + private final byte[] gameInfoPacket; + public LabyMod() { + gameInfoPacket = PluginMessage.genBufPacket(buf -> { + ProtocolUtils.writeString(buf, "discord_rpc"); + + JsonObject json = new JsonObject(); + json.addProperty("hasGame", true); + json.addProperty("game_mode", "steamwar.de"); + json.addProperty("game_startTime", 0); + json.addProperty("game_endTime", 0); + ProtocolUtils.writeString(buf, json.toString()); + }); + } + + public void handlePluginMessage(PluginMessageEvent event) { + Player player = (Player) event.getSource(); + player.sendPluginMessage(event.getIdentifier(), gameInfoPacket); + + ByteBuf buf = Unpooled.wrappedBuffer(event.getData()); + String purpose = ProtocolUtils.readString(buf); + if(!"INFO".equals(purpose)) + return; + + JsonObject message = JsonParser.parseString(ProtocolUtils.readString(buf)).getAsJsonObject(); + List mods = new LinkedList<>(); + + if(message.has("addons")) { + for(JsonElement element : message.getAsJsonArray("addons")) { + JsonObject addon = element.getAsJsonObject(); + mods.add(Mod.getOrCreate(addon.get("name").getAsString(), Mod.Platform.LABYMOD)); + } + } + + if(message.has("mods")) { + VelocityCore.getLogger().log(Level.WARNING, () -> "LabyMod External Mods for debugging: " + message.getAsJsonArray("mods")); + //for(JsonElement element : message.getAsJsonArray("mods")) { + // JsonObject addon = element.getAsJsonObject(); + // //TODO observe: FORGE and FABRIC mods available, do they always and with .jar? (would equal new mod platform) + // //mods.add(Mod.getOrCreate(addon.get("name").getAsString().replace(".jar", ""), Mod.Platform.FORGE)); + //} + } + + ModUtils.handleMods(player, mods); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/Lunar.java b/VelocityCore/src/de/steamwar/velocitycore/mods/Lunar.java new file mode 100644 index 00000000..de39b37c --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/Lunar.java @@ -0,0 +1,130 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.lunarclient.apollo.ApolloManager; +import com.lunarclient.apollo.libs.protobuf.Any; +import com.lunarclient.apollo.libs.protobuf.InvalidProtocolBufferException; +import com.lunarclient.apollo.libs.protobuf.Message; +import com.lunarclient.apollo.mods.impl.*; +import com.lunarclient.apollo.module.ApolloModuleManager; +import com.lunarclient.apollo.module.ApolloModuleManagerImpl; +import com.lunarclient.apollo.module.modsetting.ModSettingModule; +import com.lunarclient.apollo.network.NetworkOptions; +import com.lunarclient.apollo.option.Options; +import com.lunarclient.apollo.player.AbstractApolloPlayer; +import com.lunarclient.apollo.player.v1.ModMessage; +import com.lunarclient.apollo.player.v1.PlayerHandshakeMessage; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.sql.Mod; +import lombok.AllArgsConstructor; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.logging.Level; + +public class Lunar { + // https://lunarclient.dev/apollo/introduction + // https://github.com/LunarClient/Apollo + + private final ApolloModuleManager manager = new ApolloModuleManagerImpl().addModule(ModSettingModule.class); + + public Lunar() { //TODO seems defunct + Options modSettings = manager.getModule(ModSettingModule.class).getOptions(); + modSettings.set(ModReplaymod.ENABLED, false); // TODO check if restrictions working + modSettings.set(ModFreelook.ENABLED, false); + modSettings.set(ModHypixelMod.ENABLED, false); + modSettings.set(ModMinimap.ENABLED, false); + modSettings.set(ModNametag.ENABLED, false); + modSettings.set(ModTeamView.ENABLED, false); + modSettings.set(ModTntCountdown.ENABLED, false); + modSettings.set(ModToggleSneak.TOGGLE_SNEAK_CONTAINER, false); + } + + public void sendRestrictions(Player player) { + NetworkOptions.sendOptions(manager.getModules(), true, new SWApolloPlayer(player)); + } + + public void handlePluginMessage(PluginMessageEvent event) { + Player player = (Player) event.getSource(); + Any packet; + + try { + packet = Any.parseFrom(event.getData()); + } catch (InvalidProtocolBufferException e) { + throw new SecurityException(e); + } + + handle(PlayerHandshakeMessage.class, packet, handshake -> { + List mods = new ArrayList<>(); + + for(ModMessage mod : handshake.getInstalledModsList()) { + switch(mod.getType()) { + case TYPE_FABRIC_INTERNAL, TYPE_FORGE_INTERNAL: + // Controlled with ModSettings + break; + case TYPE_FABRIC_EXTERNAL: + mods.add(Mod.getOrCreate(mod.getName(), Mod.Platform.FABRIC)); + break; + case TYPE_FORGE_EXTERNAL: + mods.add(Mod.getOrCreate(mod.getName(), Mod.Platform.FORGE)); + break; + default: + VelocityCore.getLogger().log(Level.INFO, () -> player.getUsername() + " uses Lunar mod with unknown type " + mod); + break; + } + } + + ModUtils.handleMods(player, mods); + }); + } + + private void handle(Class type, Any packet, Consumer handler) { + try { + handler.accept(packet.unpack(type)); + } catch (InvalidProtocolBufferException ignored) { /*ignored*/ } + } + + @AllArgsConstructor + private static class SWApolloPlayer extends AbstractApolloPlayer { + + private final Player player; + + @Override + public void sendPacket(Message message) { + sendPacket(Any.pack(message).toByteArray()); + } + + @Override + public void sendPacket(byte[] bytes) { + player.sendPluginMessage(MinecraftChannelIdentifier.from(ApolloManager.PLUGIN_MESSAGE_CHANNEL), bytes); + } + + @Override public UUID getUniqueId() { return player.getUniqueId(); } + @Override public String getName() { return player.getUsername(); } + @Override public boolean hasPermission(String s) { return player.hasPermission(s); } + @Override public Object getPlayer() { return player; } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/ModUtils.java b/VelocityCore/src/de/steamwar/velocitycore/mods/ModUtils.java new file mode 100644 index 00000000..4342103b --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/ModUtils.java @@ -0,0 +1,92 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.commands.PunishmentCommand; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.Mod; +import de.steamwar.sql.Mod.ModType; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import lombok.Getter; +import lombok.experimental.UtilityClass; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.stream.Collectors; + +@UtilityClass +public class ModUtils { + + @Getter + private static final Map> playerModMap = new HashMap<>(); + + public static boolean handleMods(Player player, List mods) { + return handleMods(player.getUniqueId(), Chatter.of(player).getLocale(), player::disconnect, mods); + } + + public static boolean handleMods(UUID uuid, Locale locale, Consumer disconnect, List mods){ + Chatter sender = Chatter.of(uuid); + SteamwarUser user = sender.user(); + playerModMap.put(uuid,new ArrayList<>(mods)); + VelocityCore.getLogger().log(Level.INFO, "%s declares mods: %s".formatted(user.getUserName(), mods.stream().map(mod -> mod.getPlatform() + ":" + mod.getModName()).collect(Collectors.joining(" ")))); + + ModType max = ModType.YELLOW; + Iterator it = mods.iterator(); + while(it.hasNext()){ + Mod mod = it.next(); + if(mod.getModType() == ModType.UNKLASSIFIED || mod.getModType() == ModType.GREEN || (mod.getModType() == ModType.YOUTUBER_ONLY && user.hasPerm(UserPerm.RESTRICTED_MODS))) + it.remove(); + else if(mod.getModType() == ModType.RED) + max = ModType.RED; + } + + if(mods.isEmpty()) { + return true; + } + + + ModType finalMax = max; + String modList = mods.stream().filter(mod -> finalMax == ModType.YELLOW || mod.getModType() == ModType.RED).map(Mod::getModName).collect(Collectors.joining("\n")); + String message; + + if(mods.size() == 1) { + message = sender.parseToLegacy(max == ModType.RED ? "MOD_RED_SING" : "MOD_YELLOW_SING", locale, modList); + } else { + message = sender.parseToLegacy(max == ModType.RED ? "MOD_RED_PLUR" : "MOD_YELLOW_PLUR", locale, modList); + } + + if(max == ModType.RED) { + PunishmentCommand.ban(user, Timestamp.from(Instant.now().plus(7, ChronoUnit.DAYS)), message, SteamwarUser.get(-1), false); + VelocityCore.getLogger().log(Level.SEVERE, "%s %s wurde automatisch wegen der Mods %s gebannt.".formatted(user.getUserName(), user.getId(), modList)); + } + + disconnect.accept(LegacyComponentSerializer.legacySection().deserialize(message)); + return false; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/ReplayMod.java b/VelocityCore/src/de/steamwar/velocitycore/mods/ReplayMod.java new file mode 100644 index 00000000..45ba2192 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/ReplayMod.java @@ -0,0 +1,64 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.proxy.Player; +import de.steamwar.persistent.Bauserver; +import de.steamwar.persistent.Builderserver; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.persistent.Subserver; +import de.steamwar.velocitycore.listeners.BasicListener; +import de.steamwar.velocitycore.listeners.PluginMessage; + +import java.util.Arrays; + +public class ReplayMod extends BasicListener { + // https://gist.github.com/Johni0702/2547c463e51f65f312cb + // https://github.com/ReplayMod/replay-restrictions/blob/master/bungeecord/src/main/java/de/johni0702/replay/restrictions/BungeeCordPlugin.java + // https://github.com/ReplayMod/ReplayMod/blob/stable/src/main/java/com/replaymod/core/utils/Restrictions.java + + private final byte[] restrict; + + public ReplayMod() { + restrict = PluginMessage.genStreamPacket(out -> { + for(String restriction : Arrays.asList("no_xray", "no_noclip", "only_first_person", "only_recording_player")) { + byte[] bytes = restriction.getBytes(); + out.writeByte(bytes.length); + out.write(bytes); + out.writeBoolean(true); // restrict + } + }); + } + + @Subscribe + public void onPlayerJoin(ServerPostConnectEvent event) { + Player player = event.getPlayer(); + if(VelocityCore.get().getConfig().lobbyserver().getPlayersConnected().contains(player)) + return; + + Subserver subserver = Subserver.getSubserver(player.getCurrentServer().orElseThrow().getServerInfo()); + if(subserver instanceof Builderserver || (subserver instanceof Bauserver bauserver && bauserver.getOwner().equals(player.getUniqueId()))) + return; + + PluginMessage.send(player, "Replay|Restrict", "replaymod:restrict", restrict); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/Schematica.java b/VelocityCore/src/de/steamwar/velocitycore/mods/Schematica.java new file mode 100644 index 00000000..9cf2c0c7 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/Schematica.java @@ -0,0 +1,41 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; +import de.steamwar.velocitycore.listeners.BasicListener; + +public class Schematica extends BasicListener { + // https://github.com/Lunatrius/SchematicaPlugin/blob/master/src/main/java/com/github/lunatrius/schematica/plugin/SchematicaPlugin.java + + private final byte[] packet = new byte[] { + /* ProtocolVersion? */ 0, + /* PERM_PRINTER */ 1, + /* PERM_SAVE */ 0, + /* PERM_LOAD */ 1 + }; + + @Subscribe + public void onPostLogin(PostLoginEvent event) { + event.getPlayer().sendPluginMessage(new LegacyChannelIdentifier("schematica"), packet); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/WorldDownloader.java b/VelocityCore/src/de/steamwar/velocitycore/mods/WorldDownloader.java new file mode 100644 index 00000000..32a90eac --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/WorldDownloader.java @@ -0,0 +1,48 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.velocitycore.mods; + +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.listeners.PluginMessage; + +public class WorldDownloader { + // https://wiki.vg/Plugin_channels/World_downloader + // https://github.com/Pokechu22/WorldDownloader-Serverside-Companion + // https://github.com/Pokechu22/WorldDownloader + + private final byte[] controlPacket; + + public WorldDownloader() { + controlPacket = PluginMessage.genStreamPacket(out -> { + out.writeInt(1); // basic data packet + out.writeBoolean(false); // General download enabled + out.writeInt(-1); // Save radius + out.writeBoolean(false); // Chunk caching enabled + out.writeBoolean(false); // Entity saving enabled + out.writeBoolean(false); // Tile entity saving disabled + out.writeBoolean(false); // Container saving disabled + }); + } + + public void handlePluginMessage(PluginMessageEvent event) { + PluginMessage.send((Player) event.getSource(), "WDL|CONTROL", "wdl:control", controlPacket); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/NetworkSender.java b/VelocityCore/src/de/steamwar/velocitycore/network/NetworkSender.java new file mode 100644 index 00000000..8f0fd588 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/network/NetworkSender.java @@ -0,0 +1,43 @@ +/* + * 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.velocitycore.network; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.network.packets.NetworkPacket; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class NetworkSender { + + private static final ChannelIdentifier swBridge = MinecraftChannelIdentifier.create("sw", "bridge"); + + @SneakyThrows + public static void send(Player player, NetworkPacket packet) { + player.getCurrentServer().ifPresent(server -> server.sendPluginMessage(swBridge, packet.serialize())); + } + + public static void send(RegisteredServer server, NetworkPacket packet) { + server.sendPluginMessage(swBridge, packet.serialize()); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/ServerMetaInfo.java b/VelocityCore/src/de/steamwar/velocitycore/network/ServerMetaInfo.java new file mode 100644 index 00000000..9b4aeeae --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/network/ServerMetaInfo.java @@ -0,0 +1,25 @@ +/* + * 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.velocitycore.network; + +import com.velocitypowered.api.proxy.ServerConnection; +import de.steamwar.network.packets.MetaInfos; + +public record ServerMetaInfo(ServerConnection sender) implements MetaInfos {} diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/EloPlayerHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/EloPlayerHandler.java new file mode 100644 index 00000000..327adbac --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/EloPlayerHandler.java @@ -0,0 +1,251 @@ +/* + * 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.velocitycore.network.handlers; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.common.FightEndsPacket; +import de.steamwar.sql.SchematicType; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserElo; +import lombok.RequiredArgsConstructor; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.title.Title; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.IntFunction; +import java.util.stream.Collectors; + +public class EloPlayerHandler extends PacketHandler { + + private static final int MEDIAN_ELO_GAIN = 40; + private static final int MEDIAN_ELO_LOSE = 20; + private static final long REMATCH_LIFETIME = (long) 60 * 60 * 1000; + + private final Map> gameModeGames = new HashMap<>(); + + /** + * {@link FightEndsPacket#getWin()} == 1 -> Blue won + * {@link FightEndsPacket#getWin()} == 2 -> Red won + */ + @Handler + public void handle(FightEndsPacket fightEndsPacket) { + if (!ArenaMode.getBySchemType(SchematicType.fromDB(fightEndsPacket.getGameMode())).isRanked()) + return; + + if (EloSchemHandler.publicVsPrivate(fightEndsPacket)) + return; + + // Die nächsten Zeilen filtern ein Fight innerhalb eines Teams nicht gewertet wird, bzw auch wenn nur Teile beider Teams im + // gleichen Team sind dieser ungewertet ist. + Set teamsIds = fightEndsPacket.getBluePlayers().stream().map(SteamwarUser::get).map(SteamwarUser::getTeam).collect(Collectors.toSet()); + for (int redPlayer : fightEndsPacket.getRedPlayers()) { + int team = SteamwarUser.get(redPlayer).getTeam(); + if (team != 0 && teamsIds.contains(team)) { + return; + } + } + + // Get sizes of both teams + int blueTeamSize = fightEndsPacket.getBluePlayers().size(); + int redTeamSize = fightEndsPacket.getRedPlayers().size(); + + // Calculate the favored team + double bluePlayerFactor = 1 / (1 + Math.exp(0 - (fightEndsPacket.getWin() == 2 ? (double) redTeamSize / blueTeamSize : (double) blueTeamSize / redTeamSize) * 3 + 3)) * 2; + double redPlayerFactor = 1 / (1 + Math.exp(0 - (fightEndsPacket.getWin() == 1 ? (double) blueTeamSize / redTeamSize : (double) redTeamSize / blueTeamSize) * 3 + 3)) * 2; + + // Calculate the time factor + double timeFactor = getTimeFactor(fightEndsPacket.getDuration()); + + // Get total elo of both teams + int blueTeamElo = fightEndsPacket.getBluePlayers().stream().mapToInt(player -> UserElo.getEloOrDefault(player, fightEndsPacket.getGameMode())).sum(); + int redTeamElo = fightEndsPacket.getRedPlayers().stream().mapToInt(player -> UserElo.getEloOrDefault(player, fightEndsPacket.getGameMode())).sum(); + + // Adaptive elo bonus + int blueTeamEloBonus = 0; + int redTeamEloBonus = 0; + if (Math.abs(blueTeamElo / (double) fightEndsPacket.getBluePlayers().size() - redTeamElo / (double) fightEndsPacket.getRedPlayers().size()) > 400) { + int outlivedDuration = Math.max(fightEndsPacket.getDuration() - 60, 0); + if (fightEndsPacket.getWin() == 1 && blueTeamElo < redTeamElo) { + blueTeamEloBonus = outlivedDuration / 20; + redTeamEloBonus = -(blueTeamEloBonus / 2); + } else if (fightEndsPacket.getWin() == 2 && redTeamElo < blueTeamElo) { + redTeamEloBonus = outlivedDuration / 20; + blueTeamEloBonus = -(redTeamEloBonus / 2); + } else if (fightEndsPacket.getWin() == 0) { + if (redTeamElo < blueTeamElo) { + blueTeamEloBonus = outlivedDuration / 20; + redTeamEloBonus = -(blueTeamEloBonus / 2); + } else if (blueTeamElo < redTeamElo) { + redTeamEloBonus = outlivedDuration / 20; + blueTeamEloBonus = -(redTeamEloBonus / 2); + } + } + } + + // Calculate the elo factor + double blueEloFactor = ((fightEndsPacket.getWin() == 1 ? 1 : 0) - 1 / (1 + Math.pow(10, (redTeamElo - blueTeamElo) / 600.0))) * (1 / (1 + Math.exp(-Math.abs(blueTeamElo - redTeamElo) / 1200.0))) * 4; + double redEloFactor = blueEloFactor * -1; + + // Calculate favoured team on draw + if (fightEndsPacket.getWin() == 0) { + if (bluePlayerFactor > 1) { + blueEloFactor = Math.abs(blueEloFactor) * -1; + redEloFactor = Math.abs(redEloFactor); + } else if (bluePlayerFactor < 1) { + blueEloFactor = Math.abs(blueEloFactor); + redEloFactor = Math.abs(redEloFactor) * -1; + } else { + if (Math.abs(blueEloFactor) > 1) { + // Do nothing + } else if (Math.abs(blueEloFactor) < 1) { + blueEloFactor = -blueEloFactor; + redEloFactor = -blueEloFactor; + } else { + blueEloFactor = 0; + redEloFactor = 0; + } + } + } + + // Calculate the rematch factor + double rematchFactor = getRematchFactor(fightEndsPacket); + + // Calculate the win factor + double blueWinFactor = (fightEndsPacket.getWin() == 1 ? 1 : 0.7); + double redWinFactor = (fightEndsPacket.getWin() == 2 ? 1 : 0.7); + + // Calculate division factor + double divisionFactor = 1D / Math.max(blueTeamSize, redTeamSize); + + double blueFactor = bluePlayerFactor * timeFactor * blueEloFactor * rematchFactor * blueWinFactor * divisionFactor; + double redFactor = redPlayerFactor * timeFactor * redEloFactor * rematchFactor * redWinFactor * divisionFactor; + + // Calculate the elo gain for each player + int blueEloGain = (int) Math.round((blueFactor < 0 ? MEDIAN_ELO_LOSE : MEDIAN_ELO_GAIN) * blueFactor) + blueTeamEloBonus; + int redEloGain = (int) Math.round((redFactor < 0 ? MEDIAN_ELO_LOSE : MEDIAN_ELO_GAIN) * redFactor) + redTeamEloBonus; + + // BungeeCore.get().getLogger().info("Blue: " + fightEndsPacket.getBluePlayers() + " " + blueTeamSize + " " + bluePlayerFactor + " " + timeFactor + " " + blueEloFactor + " " + rematchFactor + " " + blueWinFactor + " " + divisionFactor + " " + blueEloGain); + // BungeeCore.get().getLogger().info("Red: " + fightEndsPacket.getRedPlayers() + " " + redTeamSize + " " + redPlayerFactor + " " + timeFactor + " " + redEloFactor + " " + rematchFactor + " " + redWinFactor + " " + divisionFactor + " " + redEloGain); + + update(fightEndsPacket.getBluePlayers(), fightEndsPacket.getGameMode(), blueEloGain); + update(fightEndsPacket.getRedPlayers(), fightEndsPacket.getGameMode(), redEloGain); + } + + private void update(List players, String gameMode, int eloGain) { + for (int player : players) { + // BungeeCore.get().getLogger().log(Level.INFO, "Player: " + player + " Elo: " + UserElo.getEloOrDefault(player, gameMode) + " Factor: " + factor); + int playerElo = UserElo.getEloOrDefault(player, gameMode); + playerElo += eloGain; + if (playerElo < 0) playerElo = 0; + + int oldProgression = UserElo.getProgression(player, gameMode); + UserElo.setElo(player, gameMode, playerElo); + int newProgression = UserElo.getProgression(player, gameMode); + + animate(player(player), UserElo.toEmblem(oldProgression).trim(), UserElo.toEmblem(newProgression).trim(), (oldProgression < newProgression) ? "§a" : "§c", eloGain); + } + } + + private void animate(Player player, String oldEmblem, String newEmblem, String arrowColor, int eloGain) { + if (player == null) + return; + + IntFunction getRankup = getEmblemTransition(oldEmblem, newEmblem, arrowColor); + + String color = ((eloGain > 0) ? "§a+" : (eloGain == 0 ? "§7" : "§c")); + + double eloStep = eloGain / 40.0; + for (int i = 0; i < 40; i++) { + Component eloGainComponent = LegacyComponentSerializer.legacySection().deserialize(color + (int) (eloStep * (i + 1))); + int finalI = i; + VelocityCore.schedule(() -> player.showTitle(Title.title( + LegacyComponentSerializer.legacySection().deserialize(getRankup.apply(finalI)), + eloGainComponent, + Title.Times.times(Duration.ofMillis(finalI == 0 ? 250 : 0), Duration.ofSeconds(2), Duration.ofMillis(finalI == 39 ? 250 : 0)) + ))).delay(i * 50L, TimeUnit.MILLISECONDS).schedule(); + } + } + + private static IntFunction getEmblemTransition(String oldEmblem, String newEmblem, String arrowColor) { + String finalOldEmblem = (oldEmblem.isEmpty() ? "/" : oldEmblem).trim(); + String finalNewEmblem = (newEmblem.isEmpty() ? "/" : newEmblem).trim(); + + return i -> { + if (oldEmblem.equals(newEmblem)) return "§8" + finalOldEmblem; + if (i < 8) return "§8" + finalOldEmblem; + if (i < 16) return "§8" + finalOldEmblem + arrowColor + " >"; + if (i < 24) return "§8" + finalOldEmblem + arrowColor + " >>"; + if (i < 32) return "§8" + finalOldEmblem + arrowColor + " >>>"; + return "§8" + finalOldEmblem + arrowColor + " >>> §8" + finalNewEmblem; + }; + } + + private Player player(int userId) { + return VelocityCore.getProxy().getPlayer(SteamwarUser.get(userId).getUUID()).orElse(null); + } + + private double getTimeFactor(int duration) { + if (duration <= 10) { + return 0.5; + } + if (duration <= 60) { + return 0.8; + } + return 1.0; + } + + private double getRematchFactor(FightEndsPacket fightEndsPacket) { + gameModeGames.computeIfAbsent(fightEndsPacket.getGameMode(), s -> new LinkedList<>()).add(new Game(fightEndsPacket.getBluePlayers(), fightEndsPacket.getRedPlayers())); + + LinkedList games = gameModeGames.get(fightEndsPacket.getGameMode()); + while (!games.isEmpty()) { + Game game = games.getFirst(); + if (game.livedMillis() > REMATCH_LIFETIME) { + games.removeFirst(); + } else { + break; + } + } + + long rematchCount = games.stream().filter(game -> game.isSame(fightEndsPacket.getBluePlayers(), fightEndsPacket.getRedPlayers())).count(); + return 1.0 / rematchCount; + } + + @RequiredArgsConstructor + private static class Game { + private long time = System.currentTimeMillis(); + private final List bluePlayers; + private final List redPlayers; + + public long livedMillis() { + return System.currentTimeMillis() - time; + } + + public boolean isSame(List bluePlayers, List redPlayers) { + return bluePlayers.containsAll(this.bluePlayers) && redPlayers.containsAll(this.redPlayers); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/EloSchemHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/EloSchemHandler.java new file mode 100644 index 00000000..68144738 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/EloSchemHandler.java @@ -0,0 +1,75 @@ +/* + * 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.velocitycore.network.handlers; + +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.common.FightEndsPacket; +import de.steamwar.sql.SchemElo; +import de.steamwar.sql.SchematicNode; +import de.steamwar.sql.SchematicType; + +public class EloSchemHandler extends PacketHandler { + + private static final int K = 20; + + public static boolean publicVsPrivate(FightEndsPacket packet) { + SchematicNode blueSchem = SchematicNode.getSchematicNode(packet.getBlueSchem()); + SchematicNode redSchem = SchematicNode.getSchematicNode(packet.getRedSchem()); + return (blueSchem.getOwner() == 0) != (redSchem.getOwner() == 0); + } + + @Handler + public void handle(FightEndsPacket fightEndsPacket) { + if (!ArenaMode.getBySchemType(SchematicType.fromDB(fightEndsPacket.getGameMode())).isRanked()) { + return; + } + + if (publicVsPrivate(fightEndsPacket)) + return; + + calcSchemElo(fightEndsPacket); + } + + private void calcSchemElo(FightEndsPacket fightEndsPacket) { + double blueResult; + if (fightEndsPacket.getWin() == 0) { + blueResult = 0.5; + } else if (fightEndsPacket.getWin() == 1) { + blueResult = 1; + } else { + blueResult = 0; + } + + int blueSchemElo = SchemElo.getCurrentElo(fightEndsPacket.getBlueSchem()); + int redSchemElo = SchemElo.getCurrentElo(fightEndsPacket.getRedSchem()); + calcSchemElo(redSchemElo, blueSchemElo, fightEndsPacket.getRedSchem(), blueResult); + calcSchemElo(blueSchemElo, redSchemElo, fightEndsPacket.getBlueSchem(), 1 - blueResult); + } + + private void calcSchemElo(int eloSchemOwn, int eloSchemEnemy, int schemId, double result) { + double winSchemExpectation = calcWinExpectation(eloSchemOwn, eloSchemEnemy); + SchemElo.setElo(schemId, (int) Math.round(eloSchemOwn + K * (result - winSchemExpectation))); + } + + private double calcWinExpectation(int eloOwn, int eloEnemy) { + return 1 / (1 + Math.pow(10, (eloEnemy - eloOwn) / 600f)); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/ExecuteCommandHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/ExecuteCommandHandler.java new file mode 100644 index 00000000..e96f9286 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/ExecuteCommandHandler.java @@ -0,0 +1,36 @@ +/* + This file is a part of the SteamWar software. + + Copyright (C) 2021 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.velocitycore.network.handlers; + +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.client.ExecuteCommandPacket; +import de.steamwar.sql.SteamwarUser; + +public class ExecuteCommandHandler extends PacketHandler { + + @Handler + public void handle(ExecuteCommandPacket packet) { + SteamwarUser target = SteamwarUser.get(packet.getPlayerId()); + String command = packet.getCommand(); + + VelocityCore.getProxy().getCommandManager().executeAsync(VelocityCore.getProxy().getPlayer(target.getUUID()).orElse(null), command); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java new file mode 100644 index 00000000..6565f1db --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/FightInfoHandler.java @@ -0,0 +1,56 @@ +/* + 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.velocitycore.network.handlers; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.velocitycore.network.ServerMetaInfo; +import de.steamwar.velocitycore.tablist.TablistManager; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.common.FightInfoPacket; + +import java.util.HashSet; +import java.util.Set; + +public class FightInfoHandler extends PacketHandler { + + private static final Set lobbys = new HashSet<>(); + + public static void addLobby(RegisteredServer lobby) { + lobbys.add(lobby); + } + + public static boolean onLobby(Player player) { + return player.getCurrentServer().map(connection -> lobbys.contains(connection.getServer())).orElse(false); + } + + @Handler + public void handle(FightInfoPacket packet) { + RegisteredServer server = ((ServerMetaInfo) packet.getMetaInfos()).sender().getServer(); + + FightInfoPacket lobbyPacket = packet.withServerName(server.getServerInfo().getName()); + + TablistManager.newFightInfo(server, packet); + + lobbys.removeIf(s -> s.getPlayersConnected().isEmpty()); + lobbys.forEach(lobby -> NetworkSender.send(lobby, lobbyPacket)); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/ImALobbyHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/ImALobbyHandler.java new file mode 100644 index 00000000..391ee1b7 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/ImALobbyHandler.java @@ -0,0 +1,32 @@ +/* + 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.velocitycore.network.handlers; + +import de.steamwar.velocitycore.network.ServerMetaInfo; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.client.ImALobbyPacket; + +public class ImALobbyHandler extends PacketHandler { + + @Handler + public void handle(ImALobbyPacket packet) { + FightInfoHandler.addLobby(((ServerMetaInfo) packet.getMetaInfos()).sender().getServer()); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/InventoryCallbackHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/InventoryCallbackHandler.java new file mode 100644 index 00000000..54668d94 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/InventoryCallbackHandler.java @@ -0,0 +1,63 @@ +/* + 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.velocitycore.network.handlers; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.inventory.InvCallback; +import de.steamwar.velocitycore.inventory.SWInventory; +import de.steamwar.velocitycore.network.NetworkSender; +import de.steamwar.velocitycore.network.ServerMetaInfo; +import de.steamwar.messages.Chatter; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.client.InventoryCallbackPacket; +import de.steamwar.network.packets.server.CloseInventoryPacket; +import de.steamwar.sql.SteamwarUser; + +import java.util.HashMap; +import java.util.Map; + +public class InventoryCallbackHandler extends PacketHandler { + + public static final Map inventoryHashMap = new HashMap<>(); + + @Handler + public void handle(InventoryCallbackPacket packet) { + SteamwarUser owner = SteamwarUser.get(packet.getOwner()); + InventoryCallbackPacket.CallbackType type = packet.getType(); + if(!inventoryHashMap.containsKey(owner.getId())) { + Chatter.of(owner).system("UPDATE_INTERRUPTION"); + if(type == InventoryCallbackPacket.CallbackType.CLICK) { + NetworkSender.send((Player) ((ServerMetaInfo) packet.getMetaInfos()).sender(), new CloseInventoryPacket(owner.getId())); + } + return; + } + if(type == InventoryCallbackPacket.CallbackType.CLICK) { + int pos = packet.getPosition(); + InvCallback.ClickType clickType = InvCallback.ClickType.valueOf(packet.getClickType().name()); + inventoryHashMap.get(owner.getId()).handleCallback(clickType, pos); + }else if(type == InventoryCallbackPacket.CallbackType.CLOSE) { + if(inventoryHashMap.get(owner.getId()).isNext()) { + inventoryHashMap.get(owner.getId()).handleClose(); + return; + } + inventoryHashMap.get(owner.getId()).setNext(true); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/network/handlers/PrepareSchemHandler.java b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/PrepareSchemHandler.java new file mode 100644 index 00000000..eec027c5 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/network/handlers/PrepareSchemHandler.java @@ -0,0 +1,41 @@ +/* + 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.velocitycore.network.handlers; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.velocitycore.ArenaMode; +import de.steamwar.velocitycore.ServerStarter; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.network.packets.PacketHandler; +import de.steamwar.network.packets.client.PrepareSchemPacket; +import de.steamwar.sql.SchematicType; +import de.steamwar.sql.SteamwarUser; + +public class PrepareSchemHandler extends PacketHandler { + + @Handler + public void handle(PrepareSchemPacket packet) { + Player player = VelocityCore.getProxy().getPlayer(SteamwarUser.get(packet.getPlayer()).getUUID()).orElse(null); + int schematicID = packet.getSchem(); + ArenaMode mode = ArenaMode.getBySchemType(SchematicType.fromDB(packet.getSchemType())); + + new ServerStarter().test(mode, mode.getRandomMap(), player).prepare(schematicID).start(); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/Tablist.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/Tablist.java new file mode 100644 index 00000000..f8dc1783 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/Tablist.java @@ -0,0 +1,319 @@ +/* + * 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.velocitycore.tablist; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.network.Connections; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; +import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.UpdateTeamsPacket; +import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; +import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; +import de.steamwar.messages.Chatter; +import de.steamwar.persistent.Storage; +import de.steamwar.velocitycore.VelocityCore; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPipeline; +import io.netty.util.ReferenceCountUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.*; +import java.util.stream.IntStream; + +@ChannelHandler.Sharable +public class Tablist extends ChannelInboundHandlerAdapter { + + private static final UUID[] swUuids = IntStream.range(0, 80).mapToObj(i -> UUID.randomUUID()).toArray(UUID[]::new); + private static final String[] swNames = IntStream.range(0, 80).mapToObj(i -> " »SW« " + String.format("%02d", i)).toArray(String[]::new); + public static final UpdateTeamsPacket createTeamPacket = new UpdateTeamsPacket("zzzzzsw-tab", UpdateTeamsPacket.Mode.CREATE, Component.empty(), Component.empty(), Component.empty(), UpdateTeamsPacket.NameTagVisibility.NEVER, UpdateTeamsPacket.CollisionRule.ALWAYS, 21, (byte)0x00, Arrays.stream(Tablist.swNames).toList()); + + private final Map directTabItems; + private final List current = new ArrayList<>(); + + private final Player player; + private final Chatter viewer; + private VelocityServerConnection connection; + + public Tablist(Player player) { + this.player = player; + this.viewer = Chatter.of(player); + this.directTabItems = Storage.directTabItems.computeIfAbsent(player, p -> new HashMap<>()); + injection(); + } + + public void update(TablistPart global, int seconds) { + if (connection == null) + return; + + player.sendPlayerListHeaderAndFooter(header(viewer, seconds), viewer.parse(false, "TABLIST_FOOTER", connection.getServerInfo().getName(), ping(), VelocityCore.getProxy().getPlayerCount())); + + List tablist = new ArrayList<>(); + List direct = new ArrayList<>(); + global.print(viewer, player, tablist, direct); + + // NPC handling + List update = new ArrayList<>(); + synchronized (directTabItems) { + for (TablistPart.Item item : direct) { + UpsertPlayerInfoPacket.Entry tabItem = directTabItems.get(item.getUuid()); + + if(tabItem == null) { + tablist.add(0, item); + } else if(!item.getDisplayName().equals(getDisplayName(tabItem))) { + tabItem.setDisplayName(new ComponentHolder(player.getProtocolVersion(), item.getDisplayName())); + tabItem.setListed(true); + update.add(tabItem); + } + } + } + + // Main list handling + int i = 0; + List add = new ArrayList<>(); + List remove = new ArrayList<>(); + for (; i < tablist.size() && i < 80; i++) { + TablistPart.Item item = tablist.get(i); + UpsertPlayerInfoPacket.Entry tabItem; + if(current.size() > i) { + tabItem = current.get(i); + } else { + tabItem = new UpsertPlayerInfoPacket.Entry(swUuids[i]); + tabItem.setProfile(new GameProfile(swUuids[i], swNames[i], Collections.emptyList())); + tabItem.setGameMode(1); + tabItem.setListed(true); + tabItem.setLatency(1000); + current.add(tabItem); + } + + if(!tabItem.getProfile().getProperties().equals(item.getProperties())) { + tabItem.setProfile(new GameProfile(tabItem.getProfile().getId(), tabItem.getProfile().getName(), item.getProperties())); + tabItem.setDisplayName(new ComponentHolder(player.getProtocolVersion(), item.getDisplayName())); + add.add(tabItem); + if(current.size() > i) { + remove.add(tabItem); + } + } else if(!item.getDisplayName().equals(getDisplayName(tabItem))) { + tabItem.setDisplayName(new ComponentHolder(player.getProtocolVersion(), item.getDisplayName())); + update.add(tabItem); + } + } + + // Excess removal + while(i < current.size()) { + remove.add(current.remove(i)); + } + + sendTabPacket(remove, null); + sendTabPacket(update, UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME); + sendTabPacket(add, UpsertPlayerInfoPacket.Action.ADD_PLAYER); + } + + public void onServerSwitch() { + injection(); + + synchronized (directTabItems) { + directTabItems.clear(); + } + + if(player.getProtocolVersion().greaterThan(ProtocolVersion.MINECRAFT_1_20)) { + current.clear(); + sendPacket(player, createTeamPacket); + } + } + + private void injection() { + connection = (VelocityServerConnection) player.getCurrentServer().orElse(null); + if(connection == null) + return; + + ChannelPipeline pipeline = connection.getConnection().getChannel().pipeline(); + if(pipeline.get("steamwar-tablist") != null) //Catch unclean exit + pipeline.remove("steamwar-tablist"); + + pipeline.addBefore(Connections.HANDLER, "steamwar-tablist", this); + } + + public void disable() { + sendTabPacket(current, null); + current.clear(); + + if(connection != null) + connection.getConnection().getChannel().pipeline().remove(this); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if(msg instanceof UpsertPlayerInfoPacket packet) { + packet.getActions().remove(UpsertPlayerInfoPacket.Action.INITIALIZE_CHAT); + packet.getActions().remove(UpsertPlayerInfoPacket.Action.UPDATE_LATENCY); + packet.getActions().remove(UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME); + packet.getActions().remove(UpsertPlayerInfoPacket.Action.UPDATE_LISTED); + if(packet.getActions().isEmpty()) { + ReferenceCountUtil.release(msg); + return; + } + + if(packet.containsAction(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) { + for (UpsertPlayerInfoPacket.Entry entry : packet.getEntries()) { + entry.setLatency(1); + entry.setChatSession(null); + + if (!Storage.teamServers.containsValue(connection.getServerInfo())) { + entry.setDisplayName(new ComponentHolder(player.getProtocolVersion(), Component.empty())); + entry.setListed(false); + } else if (entry.getDisplayName() == null) { + entry.setDisplayName(new ComponentHolder(player.getProtocolVersion(), Component.text(entry.getProfile().getName()).color(NamedTextColor.GRAY))); + } + + synchronized (directTabItems) { + directTabItems.put(entry.getProfileId(), entry); + } + } + + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_LISTED); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_LATENCY); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME); + packet.addAction(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE); + } + + if(packet.containsAction(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE)) { + for(UpsertPlayerInfoPacket.Entry entry : packet.getEntries()) { + if(!entry.getProfileId().equals(player.getUniqueId()) && entry.getGameMode() == 3) + entry.setGameMode(1); + } + } + } else if(msg instanceof RemovePlayerInfoPacket packet) { + for(UUID uuid : packet.getProfilesToRemove()) { + synchronized (directTabItems) { + directTabItems.remove(uuid); + } + } + } else if(msg instanceof LegacyPlayerListItemPacket packet) { + if(packet.getAction() == LegacyPlayerListItemPacket.ADD_PLAYER) { + for(LegacyPlayerListItemPacket.Item entry : packet.getItems()) { + entry.setLatency(1).setPlayerKey(null); + + if (!Storage.teamServers.containsValue(connection.getServerInfo())) { + entry.setDisplayName(Component.empty()); + } else if (entry.getDisplayName() == null) { + entry.setDisplayName(Component.text(entry.getName()).color(NamedTextColor.GRAY)); + } + + UpsertPlayerInfoPacket.Entry newEntry = new UpsertPlayerInfoPacket.Entry(entry.getUuid()); + newEntry.setProfile(new GameProfile(entry.getUuid(), entry.getName(), entry.getProperties())); + newEntry.setDisplayName(new ComponentHolder(player.getProtocolVersion(), entry.getDisplayName())); + newEntry.setListed(true); + synchronized (directTabItems) { + directTabItems.put(entry.getUuid(), newEntry); + } + } + } else if(packet.getAction() == LegacyPlayerListItemPacket.UPDATE_GAMEMODE) { + for(LegacyPlayerListItemPacket.Item entry : packet.getItems()) { + if(!player.getUniqueId().equals(entry.getUuid()) && entry.getGameMode() == 3) + entry.setGameMode(1); + } + } else if(packet.getAction() == LegacyPlayerListItemPacket.REMOVE_PLAYER) { + for(LegacyPlayerListItemPacket.Item entry : packet.getItems()) { + synchronized (directTabItems) { + directTabItems.remove(entry.getUuid()); + } + } + } else { + ReferenceCountUtil.release(msg); + return; + } + } + + ctx.fireChannelRead(msg); + } + + private void sendTabPacket(List items, UpsertPlayerInfoPacket.Action action) { //Breaks in 1.19.3 if action != UPDATE_DISPLAY_NAME, ADD_PLAYER or REMOVE_PLAYER + if(items.isEmpty()) + return; + + if(player.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_19_3)) { + int legacyAction = LegacyPlayerListItemPacket.REMOVE_PLAYER; + if(action == UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME) + legacyAction = LegacyPlayerListItemPacket.UPDATE_DISPLAY_NAME; + else if (action == UpsertPlayerInfoPacket.Action.ADD_PLAYER) + legacyAction = LegacyPlayerListItemPacket.ADD_PLAYER; + + sendPacket(player, new LegacyPlayerListItemPacket(legacyAction, items.stream().map(item -> new LegacyPlayerListItemPacket + .Item(item.getProfileId()) + .setName(item.getProfile().getName()) + .setProperties(item.getProfile().getProperties()) + .setDisplayName(item.getDisplayName().getComponent()) + .setLatency(item.getLatency()) + .setGameMode(item.getGameMode())).toList())); + return; + } + + if(action == null) { //REMOVE + sendPacket(player, new RemovePlayerInfoPacket(items.stream().map(item -> item.getProfile().getId()).toList())); + return; + } + + sendPacket(player, new UpsertPlayerInfoPacket( + action == UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME ? EnumSet.of(UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME, UpsertPlayerInfoPacket.Action.UPDATE_LISTED) : EnumSet.of(UpsertPlayerInfoPacket.Action.ADD_PLAYER, UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME, UpsertPlayerInfoPacket.Action.UPDATE_LATENCY, UpsertPlayerInfoPacket.Action.UPDATE_LISTED, UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE), + items + )); + } + + private Component header(Chatter p, int seconds) { + int phase = (seconds % 10) / 5; + if (phase == 0) + return p.parse(false, "TABLIST_PHASE_DISCORD"); + else + return p.parse(false, "TABLIST_PHASE_WEBSITE"); + } + + private String ping() { + long ping = player.getPing(); + if (ping == -1) { + return "§7?"; + } else if (ping < 50) { + return "§a" + ping; + } else if (ping < 150) { + return "§e" + ping; + } else { + return "§c" + ping; + } + } + + private Component getDisplayName(UpsertPlayerInfoPacket.Entry entry) { + ComponentHolder displayName = entry.getDisplayName(); + if(displayName == null) + return null; + + return displayName.getComponent(); + } + + public static void sendPacket(Player player, MinecraftPacket packet) { + ((ConnectedPlayer) player).getConnection().write(packet); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistBuild.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistBuild.java new file mode 100644 index 00000000..26e22837 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistBuild.java @@ -0,0 +1,70 @@ +/* + * 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.velocitycore.tablist; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.messages.Chatter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TablistBuild implements TablistPart { + + private final List servers = new ArrayList<>(); + private final Map> players = new HashMap<>(); + + public TablistBuild() { + for (RegisteredServer server : VelocityCore.getProxy().getAllServers()){ + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if(server.getPlayersConnected().isEmpty() || subserver == null || subserver.getType() != Servertype.BAUSERVER) + continue; + + servers.add(server); + players.put(server, server.getPlayersConnected().stream().sorted(((p1, p2) -> p1.getUsername().compareToIgnoreCase(p2.getUsername()))).map(Item::new).toList()); + } + servers.sort((s1, s2) -> s1.getServerInfo().getName().compareToIgnoreCase(s2.getServerInfo().getName())); + } + + @Override + public String sortKey() { + return "Build"; + } + + @Override + public void print(Chatter viewer, Player player, List tablist, List direct) { + RegisteredServer server = player.getCurrentServer().map(ServerConnection::getServer).orElse(null); + if(players.keySet().stream().anyMatch(info -> server != info)) { + tablist.add(new Item(null, "", TablistServer.GRAY)); + tablist.add(new Item(null, viewer.parseToLegacy("TABLIST_BAU"), TablistServer.LIGHT_GRAY)); + } + + for (RegisteredServer info : servers) { + TablistServer.teamify(players.get(info), player) + .forEach(((server == info) ? direct : tablist)::add); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistGroup.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistGroup.java new file mode 100644 index 00000000..2f39f023 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistGroup.java @@ -0,0 +1,45 @@ +/* + * 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.velocitycore.tablist; + +import com.velocitypowered.api.proxy.Player; +import de.steamwar.messages.Chatter; + +import java.util.List; + +public class TablistGroup implements TablistPart { + private final List sublists; + + public TablistGroup(List sublists) { + this.sublists = sublists; + } + + @Override + public String sortKey() { + return ""; + } + + @Override + public void print(Chatter viewer, Player player, List tablist, List direct) { + for (TablistPart sublist : sublists) { + sublist.print(viewer, player, tablist, direct); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java new file mode 100644 index 00000000..2b8b47bd --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java @@ -0,0 +1,112 @@ +/* + * 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.velocitycore.tablist; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; +import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Storage; +import de.steamwar.persistent.Subserver; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.velocitycore.listeners.BasicListener; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class TablistManager extends BasicListener { + + private static final Map fightInfos = new HashMap<>(); + + public static synchronized void newFightInfo(RegisteredServer info, FightInfoPacket packet) { + fightInfos.put(info, packet); + fightInfos.keySet().removeIf(serverInfo -> serverInfo.getPlayersConnected().isEmpty()); + } + + private final Map tablists = new HashMap<>(); + + private int seconds = 0; + + public TablistManager() { + VelocityCore.schedule(this::updateTablist).repeat(1, TimeUnit.SECONDS).schedule(); + synchronized (tablists) { + VelocityCore.getProxy().getAllPlayers().forEach(player -> tablists.put(player, new Tablist(player))); + } + } + + @Subscribe + public void onJoin(PostLoginEvent event) { + synchronized (tablists) { + tablists.put(event.getPlayer(), new Tablist(event.getPlayer())); + } + Tablist.sendPacket(event.getPlayer(), Tablist.createTeamPacket); + } + + @Subscribe + public void onServerConnection(ServerPostConnectEvent event) { + synchronized (tablists) { + tablists.get(event.getPlayer()).onServerSwitch(); + } + } + + @Subscribe + public void onLeave(DisconnectEvent event) { + synchronized (tablists) { + tablists.remove(event.getPlayer()); + Storage.directTabItems.remove(event.getPlayer()); + } + } + + public void disable() { + synchronized (tablists) { + tablists.forEach((player, tablist) -> tablist.disable()); + tablists.clear(); + } + } + + private void updateTablist() { + List subservers = new ArrayList<>(); + for (RegisteredServer server : new ArrayList<>(VelocityCore.getProxy().getAllServers())){ + if(server.getPlayersConnected().isEmpty()) + continue; + + Subserver subserver = Subserver.getSubserver(server.getServerInfo()); + if(fightInfos.containsKey(server)) + subservers.add(new TablistServer(server, fightInfos.get(server))); + else if(subserver == null || subserver.getType() != Servertype.BAUSERVER) + subservers.add(new TablistServer(server)); + } + subservers.add(new TablistBuild()); + subservers.sort((s1, s2) -> s1.sortKey().compareToIgnoreCase(s2.sortKey())); + TablistPart global = new TablistGroup(subservers); + + synchronized (tablists) { + tablists.forEach((player, tablist) -> tablist.update(global, seconds)); + } + seconds++; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistPart.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistPart.java new file mode 100644 index 00000000..308c9820 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistPart.java @@ -0,0 +1,65 @@ +/* + * 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.velocitycore.tablist; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.GameProfile; +import de.steamwar.messages.Chatter; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserPerm; +import lombok.Getter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +import java.util.List; +import java.util.UUID; + +public interface TablistPart { + String sortKey(); + void print(Chatter viewer, Player player, List tablist, List direct); + + @Getter + class Item { + private final UUID uuid; + private final Component displayName; + private final List properties; + + public Item(UUID uuid, String displayName, List properties) { + this.uuid = uuid; + this.displayName = LegacyComponentSerializer.legacySection().deserialize(displayName); + this.properties = properties; + } + + public Item(Player player) { + this(player, false); + } + + public Item(Player player, boolean sameTeam) { + this.uuid = player.getUniqueId(); + UserPerm.Prefix prefix = SteamwarUser.get(player.getUniqueId()).prefix(); + if (prefix == UserPerm.emptyPrefix && sameTeam) { + this.displayName = LegacyComponentSerializer.legacySection().deserialize("§f" + player.getUsername()); + } else { + this.displayName = LegacyComponentSerializer.legacySection().deserialize(prefix.getColorCode() + player.getUsername()); + } + this.properties = player.getGameProfileProperties(); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistServer.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistServer.java new file mode 100644 index 00000000..6601f2ca --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistServer.java @@ -0,0 +1,103 @@ +/* + * 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.velocitycore.tablist; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.util.GameProfile; +import de.steamwar.persistent.Servertype; +import de.steamwar.persistent.Subserver; +import de.steamwar.velocitycore.VelocityCore; +import de.steamwar.messages.Chatter; +import de.steamwar.network.packets.common.FightInfoPacket; +import de.steamwar.sql.SteamwarUser; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public class TablistServer implements TablistPart { + + public static final List GRAY = List.of(new GameProfile.Property("textures", "eyJ0aW1lc3RhbXAiOjE0NTU1NzQxMTk0MzMsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzZlNzJkMzE0NzczMmQ5NzFkZWZhZTIzMWIzOGQ5NDI0MTRiMDU3YTcxNTFjNTNjNWZkNjI5NmEzYjllZGEwYWIifX19", "ro/ZKHt7278yhCr+CFTcPp/q6wAUlef//85k2DzkfRaZqy0CtGgwisDs2U4pVKvQ2pfXvitzWgbJvD0bLeQ12xWi4c1Fc29LCArosVJoFmrJDHz7N2MlstHT+ynQROb9d2aiFA6uOXfLjPKb1noUZ/YQoZjqcPIvD5oFZtD5DHV5O4hYz0IvgHbIjDqjz6ITsTcKiBlbxNg2loTFxSlW1ZfnNCO+kcAmeyB5NFY3j0e+/AqVANiNoiC3OKsECM/yEx/acf+vKWcT8mQn4wRoIGtxfEU7ZjNtgdh73NvXXBygW+K9AiJ242g8Y06Xxuk8kaNEGmT6H/mM7nbwjZmQQXpi/Pao2gYqyeIofeCPfr8RsGXoDX3nXDAw8/LyhTCHgx+sp6IQYSfGcSMJtoNeTJ0liIFxqn1V9/zKmzOZAPzR6qrQPOjoRFljLAlv7rfzotaEqh/1ldd40GdS8tstczn7f29OQerNDaqvbDb00Gy0STdUr1bVyCDptA54XKjT9WFv7QpBikEculxqSppAXPxD2Fb/ZmphbZx8WEGfG6bVFhf6fQdDAUXlcv8BxjElNPwlolF86M2KJd5VquLluhrCjwID7OK/pffNultAVH+Lxw4QOAXmJqjUrA1KHgyG1S0Cwj/f4E2hdxZJBvkfVtq9qPkd9nignhEoTCTOHf0=")); + public static final List LIGHT_GRAY = List.of(new GameProfile.Property("textures", "eyJ0aW1lc3RhbXAiOjE0NTU2MjU1OTM5NjIsInByb2ZpbGVJZCI6ImIzYjE4MzQ1MzViZjRiNzU4ZTBjZGJmMGY4MjA2NTZlIiwicHJvZmlsZU5hbWUiOiIxMDExMTEiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzc4Y2I3ZmMyMDhiMzM4NTUwNGE4MTQ0MjA0NDI4ZmRjZDYzMjRiZWIzMWNhMmNlODZjYzQyNGI5NjNkODVjIn19fQ==", "R/wZUZRC1dishRdM9a2SSxxW3oYa0XSb/MxHbQpEUA791HxyqjaKLDu0wFX2r2a8ZTeVjzXpNzkg3+PkrA11o8h7lt86MTD1pi/rQqj/WRuoqf2LP+ypbssKV+LU15cYez2cj3QQVcJDXgWEnfSLNuBv6NG8BDUpUAjTWldvu99NCJHUoD0jNMHxY/fu4k5vCgOjaBaKgkjVk2bmUhegusmtMwco+3pYx+y8+gUW8ptx5SnePG+dOwTqLyBFiOt2AQ+gSvbU/jP9aAXgxOwz/b1pMaBWtzVhFU865NHlIdSpIHg/sh3uNah3a7gTgtTvxPQv1OzM/KtqYKiamsrRzAQMzRcs4A7Tp0GakLuxEaz401IwvQ7UGVYLFzGUVLB2MyqtPgifiqQSQxZpiqj9sM5QadhsUw00nfX7mTdW46U0MtNIbby1rLrvgQKoj08zt6LJlhI3yjyawy4iZkgF4oc+PCNwZc93GIbVL9LJaGkXk3RVA+JpGwfMJrGVbL7hl8ibbAcUv7uCEWdkAgZCd6w75jEE4tlhDSPDD4rXbn+FeTZRg2n/PGKtnoTZRzbniiFaNoSAHDZSVRG39xvBDFvtmL3SPaKhzKaifiYrgNn453WtR3kymqdAtPf1GN9d1VltGZ/+vMPwqPJb6thcrlcU64UGHbg1olRkiyZHvY8=")); + + private final RegisteredServer server; + private final List players; + + public TablistServer(RegisteredServer server) { + this(server, server.getPlayersConnected().stream().sorted((p1, p2) -> p1.getUsername().compareToIgnoreCase(p2.getUsername())).map(TablistPart.Item::new).toList()); + } + + public TablistServer(RegisteredServer server, FightInfoPacket info) { + this(server, new ArrayList<>()); + + Collection onlinePlayers = server.getPlayersConnected(); + addPlayers(info.getBlueName().substring(0, 2), info.getBluePlayers(), onlinePlayers); + addPlayers(info.getRedName().substring(0, 2), info.getRedPlayers(), onlinePlayers); + addPlayers("§7", info.getSpectators(), onlinePlayers); + } + + public TablistServer(RegisteredServer server, List players) { + this.server = server; + this.players = players; + } + + @Override + public String sortKey() { + return server.getServerInfo().getName(); + } + + @Override + public void print(Chatter viewer, Player player, List tablist, List direct) { + boolean onServer = player.getCurrentServer().map(ServerConnection::getServer).orElse(null) == server; + List items = onServer ? direct : tablist; + + if(!onServer) { + items.add(new Item(null, "", GRAY)); + items.add(new Item(null, "§7§l" + server.getServerInfo().getName(), LIGHT_GRAY)); + } + + teamify(players, player).forEach(items::add); + } + + private void addPlayers(String prefix, List teamPlayers, Collection onlinePlayers) { + teamPlayers.stream().map(SteamwarUser::get).map( + user -> onlinePlayers.stream().filter(player -> player.getUniqueId().equals(user.getUUID())).findAny() + ).filter(Optional::isPresent).map(Optional::get).sorted( + (p1, p2) -> p1.getUsername().compareToIgnoreCase(p2.getUsername()) + ).forEachOrdered(player -> players.add(new Item(player.getUniqueId(), prefix + player.getUsername(), player.getGameProfileProperties()))); + } + + public static Stream teamify(List players, Player player) { + int team = SteamwarUser.get(player.getUniqueId()).getTeam(); + if (team == 0) return players.stream(); + return players.stream().map(item -> { + Player p = VelocityCore.getProxy().getPlayer(item.getUuid()).orElse(null); + if (p == null) return item; + Subserver subserver = Subserver.getSubserver(p.getCurrentServer().map(ServerConnection::getServerInfo).orElse(null)); + if (subserver != null && subserver.getType() == Servertype.ARENA) return item; + if (SteamwarUser.get(p.getUniqueId()).getTeam() != team) return item; + return new Item(p, true); + }); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/util/BauLock.java b/VelocityCore/src/de/steamwar/velocitycore/util/BauLock.java new file mode 100644 index 00000000..03bc4315 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/util/BauLock.java @@ -0,0 +1,66 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 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.velocitycore.util; + +import de.steamwar.messages.Chatter; +import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.UserConfig; +import de.steamwar.sql.UserPerm; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class BauLock { + + private static final String BAU_LOCK_CONFIG_NAME = "baulockstate"; + public static void setLocked(Chatter owner, BauLockState state) { + UserConfig.updatePlayerConfig(owner.user().getId(), BAU_LOCK_CONFIG_NAME, state == BauLockState.OPEN ? null : state.name()); + owner.system("BAU_LOCKED_" + state.name()); + } + + public static boolean isLocked(SteamwarUser owner, SteamwarUser target) { + if (owner.getId() == target.getId()) + return false; + + boolean locked; + String state = UserConfig.getConfig(owner.getId(), BAU_LOCK_CONFIG_NAME); + switch (state == null ? BauLockState.OPEN : BauLockState.valueOf(state)) { + case NOBODY: + locked = true; + break; + case SERVERTEAM: + locked = !target.hasPerm(UserPerm.TEAM); + break; + case TEAM_AND_SERVERTEAM: + if (target.hasPerm(UserPerm.TEAM)) { + return false; + } + locked = owner.getTeam() != target.getTeam(); + break; + case TEAM: + locked = owner.getTeam() != target.getTeam(); + break; + case OPEN: + default: + locked = false; + } + + return locked; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/util/BauLockState.java b/VelocityCore/src/de/steamwar/velocitycore/util/BauLockState.java new file mode 100644 index 00000000..8fe89bf9 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/util/BauLockState.java @@ -0,0 +1,29 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 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.velocitycore.util; + +public enum BauLockState { + + NOBODY, // Locks the build server for all users + SERVERTEAM, // opens the build server only for every added user which is a server team member + TEAM_AND_SERVERTEAM, //opens the build server only for every added user which is in the same team as the buildOwner and every server team member + TEAM, //opens the build server only for every added user which is in the same team as the buildOwner + OPEN //unlocks the build server for all users +} diff --git a/build.gradle.kts b/build.gradle.kts index d06e8a6a..a33cd959 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import java.net.URI + /* * This file is a part of the SteamWar software. * @@ -35,5 +37,26 @@ java { allprojects { repositories { mavenCentral() + maven { + url = URI("https://repo.papermc.io/repository/maven-public/") + content { + includeGroup("com.velocitypowered") + } + } + maven { + url = URI("https://m2.dv8tion.net/releases") + content { + includeGroup("net.dv8tion") + } + } + maven { + url = URI("https://repo.lunarclient.dev") + content { + includeGroup("com.lunarclient") + } + } + maven { + url = URI("https://steamwar.de/maven/") + } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 263416d1..9bad9b06 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,3 +21,5 @@ rootProject.name = "SteamWar" include("CommandFramework") include("CommonCore") +include("VelocityCore") +include("VelocityCore:Persistent")