diff --git a/.gitignore b/.gitignore index f6d3010c..3cfd7618 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,7 @@ hs_err_pid* # run-* Gradle tasks **/loader/run/ **/velocity/run/ +**/fabric/run + +# Fabric mapping migration +**/fabric/remappedSrc \ No newline at end of file diff --git a/build.gradle b/build.gradle index 025d5828..746d8edd 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ plugins { alias(libs.plugins.run.paper) apply false alias(libs.plugins.run.waterfall) apply false alias(libs.plugins.run.velocity) apply false + alias(libs.plugins.fabric.loom) apply false } version = '3.0.0-SNAPSHOT' diff --git a/buildscript/final.gradle b/buildscript/final.gradle index c7fe364f..faae721e 100644 --- a/buildscript/final.gradle +++ b/buildscript/final.gradle @@ -1,6 +1,6 @@ // Buildscript for loaders & standalone platforms -task copyOutput(type: Copy) { +tasks.register('copyOutput', Copy) { from(this.shadowJar) into rootProject.file('jars') } @@ -14,5 +14,7 @@ shadowJar { rename { fileName -> 'LICENSE.txt' } } - finalizedBy copyOutput + if (it.project.name != "fabric") { + finalizedBy copyOutput + } } diff --git a/buildscript/relocations.gradle b/buildscript/relocations.gradle index e1f65043..ed052d9c 100644 --- a/buildscript/relocations.gradle +++ b/buildscript/relocations.gradle @@ -42,8 +42,6 @@ 'net.kyori.adventure.ansi', 'net.kyori.adventure.examination', 'net.kyori.adventure.option', - 'net.kyori.adventure.platform', - 'net.kyori.adventure.text.serializer', // EnhancedLegacyText, MCDiscordReserializer 'dev.vankka.enhancedlegacytext', diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 9e9b0a9e..1701a15c 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -6,6 +6,8 @@ allprojects { [ 'net.kyori', + 'net.kyori.adventure.platform', + 'net.kyori.adventure.text.serializer', 'me.lucko.commodore' ].each { tasks.shadowJar.relocate it, 'com.discordsrv.dependencies.' + it diff --git a/bungee/build.gradle b/bungee/build.gradle index dffdf48a..0443efdd 100644 --- a/bungee/build.gradle +++ b/bungee/build.gradle @@ -10,7 +10,9 @@ shadowJar { configure { [ - 'net.kyori' + 'net.kyori', + 'net.kyori.adventure.platform', + 'net.kyori.adventure.text.serializer' ].each { relocate it, 'com.discordsrv.dependencies.' + it } diff --git a/common/src/main/java/com/discordsrv/common/config/main/generic/GameCommandExecutionConditionConfig.java b/common/src/main/java/com/discordsrv/common/config/main/generic/GameCommandExecutionConditionConfig.java index c945ebb8..6c95578a 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/generic/GameCommandExecutionConditionConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/generic/GameCommandExecutionConditionConfig.java @@ -127,9 +127,9 @@ public boolean isAcceptableCommand(DiscordGuildMember member, DiscordUser user, for (String configCommand : commands) { if (isCommandMatch(configCommand, command, suggestions, helper) != blacklist) { - return true; + return blacklist; } } - return false; + return !blacklist; } } diff --git a/fabric/build.gradle b/fabric/build.gradle new file mode 100644 index 00000000..46d3e272 --- /dev/null +++ b/fabric/build.gradle @@ -0,0 +1,89 @@ +apply from: rootProject.file('buildscript/standalone.gradle') +apply plugin: 'fabric-loom' + +configurations.configureEach { + resolutionStrategy { + force "org.slf4j:slf4j-api:1.7.36" // Introduced by Minecraft itself + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +processResources { + filesMatching('**/fabric.mod.json') { + expand 'VERSION': project.version, 'MINECRAFT_VERSION': libs.fabric.minecraft.get().version, 'LOADER_VERSION': libs.fabric.loader.get().version + } + dependsOn generateRuntimeDownloadResourceForRuntimeDownloadOnly +} + +shadowJar { + configurations = [project.configurations.shadow] + mergeServiceFiles() +} + +tasks.register('copyRemappedJar', Copy) { + from remapJar.archiveFile + into rootProject.file('jars') +} + +remapJar { + dependsOn shadowJar + mustRunAfter shadowJar + inputFile = shadowJar.archiveFile + archiveBaseName = 'DiscordSRV-Fabric' + archiveClassifier = jar.archiveClassifier + + finalizedBy copyRemappedJar +} + +artifacts { + archives remapJar + shadow shadowJar +} + +loom { + serverOnlyMinecraftJar() + accessWidenerPath = file('src/main/resources/discordsrv.accesswidener') +} + +repositories { + exclusiveContent { + forRepository { + maven { url = 'https://maven.fabricmc.net/' } + } + filter { + includeGroup 'net.fabricmc' + } + } +} + +dependencies { + // To change the versions see the settings.gradle file + minecraft(libs.fabric.minecraft) + mappings(variantOf(libs.fabric.yarn) { classifier("v2") }) + compileOnly(libs.fabric.loader) + + // Fabric API + modImplementation(libs.fabric.api) + modImplementation(libs.fabric.permissions.api) + include(libs.fabric.permissions.api) + + // API + annotationProcessor project(':api') + shadow project(':common:common-api') + + // Common + shadow project(':common') + + // Adventure + modImplementation(libs.adventure.platform.fabric) + include(libs.adventure.platform.fabric) + + // DependencyDownload + shadow(libs.mcdependencydownload.fabric) { + exclude module: 'fabric-loader' + } +} \ No newline at end of file diff --git a/fabric/src/main/java/com/discordsrv/fabric/DiscordSRVFabricBootstrap.java b/fabric/src/main/java/com/discordsrv/fabric/DiscordSRVFabricBootstrap.java new file mode 100644 index 00000000..437e9fa5 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/DiscordSRVFabricBootstrap.java @@ -0,0 +1,125 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric; + +import com.discordsrv.common.abstraction.bootstrap.IBootstrap; +import com.discordsrv.common.abstraction.bootstrap.LifecycleManager; +import com.discordsrv.common.core.logging.Logger; +import com.discordsrv.common.core.logging.backend.impl.Log4JLoggerImpl; +import dev.vankka.dependencydownload.classpath.ClasspathAppender; +import dev.vankka.mcdependencydownload.fabric.classpath.FabricClasspathAppender; +import net.fabricmc.api.DedicatedServerModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.loader.api.FabricLoader; +import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences; +import net.minecraft.GameVersion; +import net.minecraft.MinecraftVersion; +import net.minecraft.server.MinecraftServer; +import org.apache.logging.log4j.LogManager; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; + +public class DiscordSRVFabricBootstrap implements DedicatedServerModInitializer, IBootstrap { + + private final Logger logger; + private final ClasspathAppender classpathAppender; + private final LifecycleManager lifecycleManager; + private final Path dataDirectory; + private MinecraftServer minecraftServer; + private FabricDiscordSRV discordSRV; + private MinecraftServerAudiences adventure; + + public DiscordSRVFabricBootstrap() { + this.logger = new Log4JLoggerImpl(LogManager.getLogger("DiscordSRV")); + this.classpathAppender = new FabricClasspathAppender(); + this.dataDirectory = FabricLoader.getInstance().getConfigDir().resolve("DiscordSRV"); + try { + this.lifecycleManager = new LifecycleManager( + this.logger, + dataDirectory, + Collections.singletonList("dependencies/runtimeDownload-fabric.txt"), + classpathAppender + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + this.minecraftServer = null; + this.adventure = null; + } + + @Override + public void onInitializeServer() { + ServerLifecycleEvents.SERVER_STARTING.register(minecraftServer -> { + this.minecraftServer = minecraftServer; + this.adventure = MinecraftServerAudiences.of(minecraftServer); + lifecycleManager.loadAndEnable(() -> this.discordSRV = new FabricDiscordSRV(this)); + }); + + ServerLifecycleEvents.SERVER_STARTED.register(minecraftServer -> this.discordSRV.runServerStarted()); + + ServerLifecycleEvents.SERVER_STOPPING.register(minecraftServer -> { + if (this.discordSRV != null) this.discordSRV.runDisable(); + }); + } + + @Override + public Logger logger() { + return logger; + } + + @Override + public ClasspathAppender classpathAppender() { + return classpathAppender; + } + + @Override + public ClassLoader classLoader() { + return getClass().getClassLoader(); + } + + @Override + public LifecycleManager lifecycleManager() { + return lifecycleManager; + } + + @Override + public Path dataDirectory() { + return dataDirectory; + } + + @Override + public String platformVersion() { + GameVersion version = MinecraftVersion.CURRENT; + return version.getName() + " (from Fabric)"; //TODO: get current build version for Fabric + } + + public MinecraftServer getServer() { + return minecraftServer; + } + + public FabricDiscordSRV getDiscordSRV() { + return discordSRV; + } + + public MinecraftServerAudiences getAdventure() { + return adventure; + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/FabricDiscordSRV.java b/fabric/src/main/java/com/discordsrv/fabric/FabricDiscordSRV.java new file mode 100644 index 00000000..a304d76c --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/FabricDiscordSRV.java @@ -0,0 +1,180 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric; + +import com.discordsrv.common.AbstractDiscordSRV; +import com.discordsrv.common.abstraction.plugin.PluginManager; +import com.discordsrv.common.command.game.abstraction.GameCommandExecutionHelper; +import com.discordsrv.common.command.game.abstraction.handler.ICommandHandler; +import com.discordsrv.common.config.configurate.manager.ConnectionConfigManager; +import com.discordsrv.common.config.configurate.manager.MainConfigManager; +import com.discordsrv.common.config.configurate.manager.MessagesConfigManager; +import com.discordsrv.common.config.configurate.manager.abstraction.ServerConfigManager; +import com.discordsrv.common.config.connection.ConnectionConfig; +import com.discordsrv.common.config.messages.MessagesConfig; +import com.discordsrv.common.core.scheduler.StandardScheduler; +import com.discordsrv.common.feature.debug.data.OnlineMode; +import com.discordsrv.common.feature.messageforwarding.game.MinecraftToDiscordChatModule; +import com.discordsrv.fabric.command.game.FabricGameCommandExecutionHelper; +import com.discordsrv.fabric.command.game.handler.FabricCommandHandler; +import com.discordsrv.fabric.config.main.FabricConfig; +import com.discordsrv.fabric.console.FabricConsole; +import com.discordsrv.fabric.module.ban.FabricBanModule; +import com.discordsrv.fabric.module.chat.*; +import com.discordsrv.fabric.player.FabricPlayerProvider; +import com.discordsrv.fabric.plugin.FabricModManager; +import com.discordsrv.fabric.requiredlinking.FabricRequiredLinkingModule; +import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences; +import net.minecraft.server.MinecraftServer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.security.CodeSource; +import java.util.jar.JarFile; + +public class FabricDiscordSRV extends AbstractDiscordSRV { + + private final StandardScheduler scheduler; + private final FabricConsole console; + private final FabricPlayerProvider playerProvider; + private final FabricModManager modManager; + private final FabricCommandHandler commandHandler; + + private final ConnectionConfigManager connectionConfigManager; + private final MainConfigManager configManager; + private final MessagesConfigManager messagesConfigManager; + + private final FabricGameCommandExecutionHelper executionHelper; + + public FabricDiscordSRV(DiscordSRVFabricBootstrap bootstrap) { + super(bootstrap); + + this.scheduler = new StandardScheduler(this); + this.console = new FabricConsole(this); + this.playerProvider = new FabricPlayerProvider(this); + this.modManager = new FabricModManager(); + this.commandHandler = new FabricCommandHandler(this); + this.executionHelper = new FabricGameCommandExecutionHelper(this); + + // Config + this.connectionConfigManager = new ConnectionConfigManager<>(this, ConnectionConfig::new); + this.configManager = new ServerConfigManager<>(this, FabricConfig::new); + this.messagesConfigManager = new MessagesConfigManager<>(this, MessagesConfig::new); + + load(); + } + + @Override + protected void enable() throws Throwable { + super.enable(); + + // Chat + registerModule(MinecraftToDiscordChatModule::new); + registerModule(FabricChatModule::new); + registerModule(FabricDeathModule::new); + registerModule(FabricJoinModule::new); + registerModule(FabricQuitModule::new); + registerModule(FabricAdvancementModule::new); + + // Required linking + registerModule(FabricRequiredLinkingModule::new); + + // Punishments + registerModule(FabricBanModule::new); + } + + @Override + protected URL getManifest() { + // Referenced from ManifestUtil in Fabric API + try { + CodeSource codeSource = getClass().getProtectionDomain().getCodeSource(); + return URI.create("jar:" + codeSource.getLocation().toString() + "!/" + JarFile.MANIFEST_NAME).toURL(); + } catch (MalformedURLException e) { + this.logger().error("Failed to get manifest URL", e); + return null; + } + } + + public MinecraftServer getServer() { + return bootstrap.getServer(); + } + + @NotNull + public MinecraftServerAudiences getAdventure() { + return bootstrap.getAdventure(); + } + + @Override + public ServerType serverType() { + return ServerType.SERVER; + } + + @Override + public StandardScheduler scheduler() { + return scheduler; + } + + @Override + public FabricConsole console() { + return console; + } + + @Override + public @NotNull FabricPlayerProvider playerProvider() { + return playerProvider; + } + + @Override + public PluginManager pluginManager() { + return modManager; + } + + @Override + public OnlineMode onlineMode() { + return OnlineMode.of(getServer().isOnlineMode()); + } + + @Override + public ICommandHandler commandHandler() { + return commandHandler; + } + + @Override + public ConnectionConfigManager connectionConfigManager() { + return connectionConfigManager; + } + + @Override + public MainConfigManager configManager() { + return configManager; + } + + @Override + public MessagesConfigManager messagesConfigManager() { + return messagesConfigManager; + } + + @Override + public @Nullable GameCommandExecutionHelper executeHelper() { + return executionHelper; + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/command/game/FabricGameCommandExecutionHelper.java b/fabric/src/main/java/com/discordsrv/fabric/command/game/FabricGameCommandExecutionHelper.java new file mode 100644 index 00000000..66165ec9 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/command/game/FabricGameCommandExecutionHelper.java @@ -0,0 +1,154 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.command.game; + +import com.discordsrv.common.command.game.abstraction.GameCommandExecutionHelper; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.RootCommandNode; +import net.minecraft.server.command.ServerCommandSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +public class FabricGameCommandExecutionHelper implements GameCommandExecutionHelper { + + protected final FabricDiscordSRV discordSRV; + private final CommandDispatcher dispatcher; + + public FabricGameCommandExecutionHelper(FabricDiscordSRV discordSRV) { + this.discordSRV = discordSRV; + this.dispatcher = discordSRV.getServer().getCommandManager().getDispatcher(); + } + + @Override + public CompletableFuture> suggestCommands(List parts) { + String fullCommand = String.join(" ", parts); + if (parts.isEmpty() || fullCommand.isBlank()) { + return getRootCommands(); + } + try { + ParseResults parse = dispatcher.parse(fullCommand, discordSRV.getServer().getCommandSource()); + if (!parse.getExceptions().isEmpty()) { + // There's an error with the command syntax, return the full command and the error message for the user. + List data = new ArrayList<>(); + data.add(fullCommand); + parse.getExceptions().values().stream().map(Exception::getMessage).map(this::splitErrorMessage).forEach(data::addAll); + + return CompletableFuture.completedFuture(data); + } + + List> nodes = parse.getContext().getNodes(); + if (!nodes.isEmpty()) { + CommandNode lastNode = nodes.getLast().getNode(); + if (lastNode.getChildren().isEmpty() && lastNode.getRedirect() == null) { + // We reached the end of the command tree. Suggest the full command as a valid command. + return CompletableFuture.completedFuture(Collections.singletonList(fullCommand)); + } + } + + Suggestions suggestions = dispatcher.getCompletionSuggestions(parse).get(); + List data = suggestions.getList().stream() + .map(suggestion -> fullCommand.substring(0, suggestion.getRange().getStart()) + suggestion.getText()) + .collect(Collectors.toList()); + if (data.isEmpty()) { + // Suggestions are empty, Likely the user is still typing an argument. + // If the context is empty, We search all commands from the root. + CommandNode lastNode = !nodes.isEmpty() ? nodes.getLast().getNode() : parse.getContext().getRootNode(); + + for (CommandNode child : lastNode.getChildren()) { + if (child.getName().toLowerCase().startsWith(parts.getLast().toLowerCase())) { + if (lastNode instanceof RootCommandNode) { + data.add(child.getName()); + continue; + } + + String commandWithoutLastPart = fullCommand.substring(0, fullCommand.length() - parts.getLast().length()); + data.add(commandWithoutLastPart + child.getName()); + } + } + } + data = data.stream().map(String::trim).distinct().collect(Collectors.toList()); + return CompletableFuture.completedFuture(data); + } catch (InterruptedException | ExecutionException e) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + } + + @Override + public List getAliases(String command) { + return Collections.emptyList(); + } + + @Override + public boolean isSameCommand(String command1, String command2) { + CommandNode commandNode1 = dispatcher.findNode(Collections.singleton(command1)); + CommandNode commandNode2 = dispatcher.findNode(Collections.singleton(command2)); + if (commandNode1 != null && commandNode2 != null) { + return commandNode1.equals(commandNode2); + } + return false; + } + + private CompletableFuture> getRootCommands() { + return CompletableFuture.completedFuture( + dispatcher.getRoot().getChildren() + .stream() + .map(CommandNode::getName) + .collect(Collectors.toList()) + ); + } + + // Split the error message if it's too long on a period or a comma. If the message reached 97 characters, split at that point and continue. + private List splitErrorMessage(String message) { + List parts = new ArrayList<>(); + int start = 0; + + while (start < message.length()) { + // Maximum line length (100 - 7 for "Error: " = 93) + int end = Math.min(start + 93, message.length()); + String chunk = message.substring(start, end); + + int splitIndex = Math.max(chunk.lastIndexOf('.'), chunk.lastIndexOf(',')); + if (splitIndex != -1 && start + splitIndex < end) { + parts.add("Error: " + message.substring(start, start + splitIndex + 1)); + start += splitIndex + 1; + } else { + // Split at 90 characters (leaving room for "Error: " and "...") + if (end < message.length()) { + parts.add("Error: " + message.substring(start, start + 90) + "..."); + start += 90; + } else { + parts.add("Error: " + message.substring(start)); + break; + } + } + } + + return parts; + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/command/game/handler/FabricCommandHandler.java b/fabric/src/main/java/com/discordsrv/fabric/command/game/handler/FabricCommandHandler.java new file mode 100644 index 00000000..305b8de9 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/command/game/handler/FabricCommandHandler.java @@ -0,0 +1,54 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.command.game.handler; + +import com.discordsrv.common.command.game.abstraction.command.GameCommand; +import com.discordsrv.common.command.game.abstraction.handler.ICommandHandler; +import com.discordsrv.common.command.game.abstraction.handler.util.BrigadierUtil; +import com.discordsrv.common.command.game.abstraction.sender.ICommandSender; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.mojang.brigadier.tree.LiteralCommandNode; +import net.minecraft.command.CommandSource; +import net.minecraft.server.command.ServerCommandSource; + +public class FabricCommandHandler implements ICommandHandler { + + private final FabricDiscordSRV discordSRV; + + public FabricCommandHandler(FabricDiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + + private ICommandSender getSender(CommandSource source) { + if (source instanceof ServerCommandSource) { + if (((ServerCommandSource) source).getPlayer() != null) { + return discordSRV.playerProvider().player(((ServerCommandSource) source).getPlayer()); + } else { + return discordSRV.console(); + } + } + return null; + } + + @Override + public void registerCommand(GameCommand command) { + LiteralCommandNode node = BrigadierUtil.convertToBrigadier(command, this::getSender); + discordSRV.getServer().getCommandManager().getDispatcher().getRoot().addChild(node); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/command/game/sender/FabricCommandSender.java b/fabric/src/main/java/com/discordsrv/fabric/command/game/sender/FabricCommandSender.java new file mode 100644 index 00000000..1ef91d07 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/command/game/sender/FabricCommandSender.java @@ -0,0 +1,52 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.command.game.sender; + +import com.discordsrv.common.command.game.abstraction.sender.ICommandSender; +import com.discordsrv.fabric.FabricDiscordSRV; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.kyori.adventure.audience.Audience; +import net.minecraft.server.command.ServerCommandSource; +import org.jetbrains.annotations.NotNull; + +public class FabricCommandSender implements ICommandSender { + + protected final FabricDiscordSRV discordSRV; + protected final ServerCommandSource commandSource; + + public FabricCommandSender(FabricDiscordSRV discordSRV, ServerCommandSource commandSource) { + this.discordSRV = discordSRV; + this.commandSource = commandSource; + } + + @Override + public boolean hasPermission(String permission) { + return Permissions.check(commandSource, permission, 4); + } + + @Override + public void runCommand(String command) { + discordSRV.getServer().getCommandManager().executeWithPrefix(commandSource, command); + } + + @Override + public @NotNull Audience audience() { + return discordSRV.getAdventure().audience(commandSource); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/config/main/FabricConfig.java b/fabric/src/main/java/com/discordsrv/fabric/config/main/FabricConfig.java new file mode 100644 index 00000000..c61df1a7 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/config/main/FabricConfig.java @@ -0,0 +1,50 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.config.main; + +import com.discordsrv.common.config.configurate.annotation.Order; +import com.discordsrv.common.config.main.MainConfig; +import com.discordsrv.common.config.main.PresenceUpdaterConfig; +import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; +import com.discordsrv.common.config.main.channels.base.server.ServerBaseChannelConfig; +import com.discordsrv.common.config.main.channels.base.server.ServerChannelConfig; +import com.discordsrv.common.config.main.linking.ServerRequiredLinkingConfig; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +@ConfigSerializable +public class FabricConfig extends MainConfig { + + @Order(5) + public ServerRequiredLinkingConfig requiredLinking = new ServerRequiredLinkingConfig(); + + @Override + public BaseChannelConfig createDefaultBaseChannel() { + return new ServerBaseChannelConfig(); + } + + @Override + public BaseChannelConfig createDefaultChannel() { + return new ServerChannelConfig(); + } + + @Override + public PresenceUpdaterConfig defaultPresenceUpdater() { + return new PresenceUpdaterConfig.Server(); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/console/FabricConsole.java b/fabric/src/main/java/com/discordsrv/fabric/console/FabricConsole.java new file mode 100644 index 00000000..ea023299 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/console/FabricConsole.java @@ -0,0 +1,58 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.console; + +import com.discordsrv.common.command.game.abstraction.executor.CommandExecutorProvider; +import com.discordsrv.common.core.logging.backend.LoggingBackend; +import com.discordsrv.common.core.logging.backend.impl.Log4JLoggerImpl; +import com.discordsrv.common.feature.console.Console; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.discordsrv.fabric.command.game.sender.FabricCommandSender; +import com.discordsrv.fabric.console.executor.FabricCommandExecutor; +import com.discordsrv.fabric.console.executor.FabricCommandFeedbackExecutor; +import net.kyori.adventure.text.Component; +import net.minecraft.server.command.ServerCommandSource; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class FabricConsole extends FabricCommandSender implements Console { + + private final LoggingBackend loggingBackend; + private final CommandExecutorProvider executorProvider; + + public FabricConsole(FabricDiscordSRV discordSRV) { + super(discordSRV, discordSRV.getServer().getCommandSource()); + this.loggingBackend = Log4JLoggerImpl.getRoot(); + + Function, ServerCommandSource> commandSenderProvider = + consumer -> new FabricCommandFeedbackExecutor(discordSRV.getServer(), consumer).getCommandSource(); + this.executorProvider = consumer -> new FabricCommandExecutor(discordSRV, commandSenderProvider.apply(consumer)); + } + + @Override + public LoggingBackend loggingBackend() { + return loggingBackend; + } + + @Override + public CommandExecutorProvider commandExecutorProvider() { + return executorProvider; + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/console/executor/FabricCommandExecutor.java b/fabric/src/main/java/com/discordsrv/fabric/console/executor/FabricCommandExecutor.java new file mode 100644 index 00000000..def7a667 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/console/executor/FabricCommandExecutor.java @@ -0,0 +1,39 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.console.executor; + +import com.discordsrv.common.command.game.abstraction.executor.CommandExecutor; +import com.discordsrv.fabric.FabricDiscordSRV; +import net.minecraft.server.command.ServerCommandSource; + +public class FabricCommandExecutor implements CommandExecutor { + + private final FabricDiscordSRV discordSRV; + private final ServerCommandSource source; + + public FabricCommandExecutor(FabricDiscordSRV discordSRV, ServerCommandSource source) { + this.discordSRV = discordSRV; + this.source = source; + } + + @Override + public void runCommand(String command) { + discordSRV.getServer().getCommandManager().executeWithPrefix(source, command); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/console/executor/FabricCommandFeedbackExecutor.java b/fabric/src/main/java/com/discordsrv/fabric/console/executor/FabricCommandFeedbackExecutor.java new file mode 100644 index 00000000..d506acb2 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/console/executor/FabricCommandFeedbackExecutor.java @@ -0,0 +1,82 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.console.executor; + +import net.kyori.adventure.text.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandOutput; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.Vec2f; +import net.minecraft.util.math.Vec3d; + +import java.util.function.Consumer; + +public class FabricCommandFeedbackExecutor implements CommandOutput, Consumer { + + private final MinecraftServer server; + private final Consumer componentConsumer; + + public FabricCommandFeedbackExecutor(MinecraftServer server, Consumer componentConsumer) { + this.server = server; + this.componentConsumer = componentConsumer; + } + + public ServerCommandSource getCommandSource() { + ServerWorld serverWorld = server.getOverworld(); + return new ServerCommandSource( + this, + serverWorld == null ? Vec3d.ZERO : Vec3d.of(serverWorld.getSpawnPos()), + Vec2f.ZERO, + serverWorld, + 4, + "DiscordSRV", + Text.literal("DiscordSRV"), + server, + null + ); + } + + @Override + public void sendMessage(Text message) { + accept(Component.text(Formatting.strip(message.getString()))); + } + + @Override + public void accept(Component component) { + componentConsumer.accept(component); + } + + @Override + public boolean shouldReceiveFeedback() { + return true; + } + + @Override + public boolean shouldTrackOutput() { + return true; + } + + @Override + public boolean shouldBroadcastConsoleToOps() { + return true; + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/mixin/PlayerAdvancementTrackerMixin.java b/fabric/src/main/java/com/discordsrv/fabric/mixin/PlayerAdvancementTrackerMixin.java new file mode 100644 index 00000000..645dea26 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/mixin/PlayerAdvancementTrackerMixin.java @@ -0,0 +1,41 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.mixin; + +import com.discordsrv.fabric.module.chat.FabricAdvancementModule; +import net.minecraft.advancement.AdvancementEntry; +import net.minecraft.advancement.PlayerAdvancementTracker; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(PlayerAdvancementTracker.class) +public class PlayerAdvancementTrackerMixin { + + @Shadow + private ServerPlayerEntity owner; + + @Inject(method = "grantCriterion", at = @At(value = "INVOKE", target = "Lnet/minecraft/advancement/PlayerAdvancementTracker;onStatusUpdate(Lnet/minecraft/advancement/AdvancementEntry;)V")) + public void onGrant(AdvancementEntry advancementEntry, String criterionName, CallbackInfoReturnable cir) { + FabricAdvancementModule.onGrant(advancementEntry, owner); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/mixin/ban/BanCommandMixin.java b/fabric/src/main/java/com/discordsrv/fabric/mixin/ban/BanCommandMixin.java new file mode 100644 index 00000000..2e486dc5 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/mixin/ban/BanCommandMixin.java @@ -0,0 +1,37 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.mixin.ban; + +import com.discordsrv.fabric.module.ban.FabricBanModule; +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.authlib.GameProfile; +import net.minecraft.server.dedicated.command.BanCommand; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BanCommand.class) +public class BanCommandMixin { + + @Inject(method = "ban", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/BannedPlayerList;add(Lnet/minecraft/server/ServerConfigEntry;)V", shift = At.Shift.AFTER)) + private static void ban(CallbackInfoReturnable cir, @Local GameProfile gameProfile) { + FabricBanModule.onBan(gameProfile); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/mixin/ban/PardonCommandMixin.java b/fabric/src/main/java/com/discordsrv/fabric/mixin/ban/PardonCommandMixin.java new file mode 100644 index 00000000..d164a79e --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/mixin/ban/PardonCommandMixin.java @@ -0,0 +1,40 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.mixin.ban; + +import com.discordsrv.fabric.module.ban.FabricBanModule; +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.authlib.GameProfile; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.dedicated.command.PardonCommand; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Collection; + +@Mixin(PardonCommand.class) +public class PardonCommandMixin { + + @Inject(method = "pardon", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/BannedPlayerList;remove(Ljava/lang/Object;)V")) + private static void pardon(ServerCommandSource source, Collection targets, CallbackInfoReturnable cir, @Local GameProfile gameProfile) { + FabricBanModule.onPardon(gameProfile); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/mixin/requiredlinking/CommandManagerMixin.java b/fabric/src/main/java/com/discordsrv/fabric/mixin/requiredlinking/CommandManagerMixin.java new file mode 100644 index 00000000..97840fa8 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/mixin/requiredlinking/CommandManagerMixin.java @@ -0,0 +1,37 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.mixin.requiredlinking; + +import com.discordsrv.fabric.requiredlinking.FabricRequiredLinkingModule; +import com.mojang.brigadier.ParseResults; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(CommandManager.class) +public class CommandManagerMixin { + + @Inject(method = "execute", at = @At("HEAD"), cancellable = true) + private void execute(ParseResults parseResults, String command, CallbackInfo ci) { + FabricRequiredLinkingModule.onCommandExecute(parseResults, command, ci); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/mixin/requiredlinking/PlayerManagerMixin.java b/fabric/src/main/java/com/discordsrv/fabric/mixin/requiredlinking/PlayerManagerMixin.java new file mode 100644 index 00000000..e2a7dd19 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/mixin/requiredlinking/PlayerManagerMixin.java @@ -0,0 +1,40 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.mixin.requiredlinking; + +import com.discordsrv.fabric.requiredlinking.FabricRequiredLinkingModule; +import com.mojang.authlib.GameProfile; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.net.SocketAddress; + +@Mixin(net.minecraft.server.PlayerManager.class) +public class PlayerManagerMixin { + + @Inject(method = "checkCanJoin", at = @At("TAIL"), cancellable = true) + public void checkCanJoin(SocketAddress address, GameProfile profile, CallbackInfoReturnable cir) { + Text kickReason = FabricRequiredLinkingModule.checkCanJoin(profile); + + cir.setReturnValue(kickReason); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/mixin/requiredlinking/ServerPlayNetworkHandlerMixin.java b/fabric/src/main/java/com/discordsrv/fabric/mixin/requiredlinking/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 00000000..dd643d22 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/mixin/requiredlinking/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,41 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.mixin.requiredlinking; + +import com.discordsrv.fabric.requiredlinking.FabricRequiredLinkingModule; +import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayNetworkHandler.class) +public class ServerPlayNetworkHandlerMixin { + + @Shadow + public ServerPlayerEntity player; + + @Inject(method = "onPlayerMove", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerPlayerEntity;getServerWorld()Lnet/minecraft/server/world/ServerWorld;", ordinal = 1), cancellable = true) + private void onPlayerMove(PlayerMoveC2SPacket packet, CallbackInfo ci) { + FabricRequiredLinkingModule.onPlayerMove(player, packet, ci); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/module/AbstractFabricModule.java b/fabric/src/main/java/com/discordsrv/fabric/module/AbstractFabricModule.java new file mode 100644 index 00000000..478f8a48 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/module/AbstractFabricModule.java @@ -0,0 +1,45 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.module; + +import com.discordsrv.common.core.module.type.AbstractModule; +import com.discordsrv.fabric.FabricDiscordSRV; + +public abstract class AbstractFabricModule extends AbstractModule { + + protected boolean enabled = false; + + public AbstractFabricModule(FabricDiscordSRV discordSRV) { + super(discordSRV); + } + + @Override + public void enable() { + enabled = true; + this.register(); + } + + @Override + public void disable() { + enabled = false; + } + + public void register() { + } +} \ No newline at end of file diff --git a/fabric/src/main/java/com/discordsrv/fabric/module/ban/FabricBanModule.java b/fabric/src/main/java/com/discordsrv/fabric/module/ban/FabricBanModule.java new file mode 100644 index 00000000..bd8a22bf --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/module/ban/FabricBanModule.java @@ -0,0 +1,158 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.module.ban; + +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.module.type.PunishmentModule; +import com.discordsrv.api.punishment.Punishment; +import com.discordsrv.common.abstraction.player.IPlayer; +import com.discordsrv.common.feature.bansync.BanSyncModule; +import com.discordsrv.common.util.ComponentUtil; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.discordsrv.fabric.module.AbstractFabricModule; +import com.mojang.authlib.GameProfile; +import net.minecraft.server.BannedPlayerEntry; +import net.minecraft.server.BannedPlayerList; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.UserCache; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.util.Date; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class FabricBanModule extends AbstractFabricModule implements PunishmentModule.Bans { + + private static FabricBanModule instance; + + public FabricBanModule(FabricDiscordSRV discordSRV) { + super(discordSRV); + + instance = this; + } + + public static void onBan(GameProfile gameProfile) { + if (instance == null) return; + FabricDiscordSRV discordSRV = instance.discordSRV; + BanSyncModule module = discordSRV.getModule(BanSyncModule.class); + if (module == null) return; + + UUID playerUUID = gameProfile.getId(); + IPlayer player = discordSRV.playerProvider().player(gameProfile.getId()); + if (player == null) { + throw new RuntimeException("Player " + playerUUID + " not present in player provider"); + } + + instance.getBan(playerUUID).whenComplete((punishment, t) -> { + if (punishment != null) { + module.notifyBanned(player, punishment); + } + }); + } + + public static void onPardon(GameProfile gameProfile) { + if (instance == null) return; + FabricDiscordSRV discordSRV = instance.discordSRV; + BanSyncModule module = discordSRV.getModule(BanSyncModule.class); + if (module != null) instance.removeBan(gameProfile.getId()).complete(null); + } + + @Override + public void enable() { + this.enabled = true; + } + + @Override + public CompletableFuture<@Nullable Punishment> getBan(@NotNull UUID playerUUID) { + BannedPlayerList banList = discordSRV.getServer().getPlayerManager().getUserBanList(); + + Optional gameProfile = Objects.requireNonNull(discordSRV.getServer().getUserCache()).getByUuid(playerUUID); + if (gameProfile.isEmpty()) { + return CompletableFuture.completedFuture(null); + } + + BannedPlayerEntry banEntry = banList.get(gameProfile.get()); + if (banEntry == null) { + return CompletableFuture.completedFuture(null); + } + Date expiration = banEntry.getExpiryDate(); + + return CompletableFuture.completedFuture(new Punishment( + expiration != null ? expiration.toInstant() : null, + ComponentUtil.fromPlain(banEntry.getReason()), + ComponentUtil.fromPlain(banEntry.getSource()) + )); + } + + @Override + public CompletableFuture addBan( + @NotNull UUID playerUUID, + @Nullable Instant until, + @Nullable MinecraftComponent reason, + @NotNull MinecraftComponent punisher + ) { + try { + MinecraftServer server = discordSRV.getServer(); + UserCache userCache = server.getUserCache(); + + GameProfile gameProfile = null; + if (userCache != null) { + gameProfile = userCache.getByUuid(playerUUID).orElse(null); + } + + String reasonProvided = reason != null ? reason.asPlainString() : null; + Date expiration = until != null ? Date.from(until) : null; + + BannedPlayerEntry banEntry = new BannedPlayerEntry(gameProfile, new Date(), reasonProvided, expiration, punisher.asPlainString()); + server.getPlayerManager().getUserBanList().add(banEntry); + + ServerPlayerEntity serverPlayerEntity = server.getPlayerManager().getPlayer(playerUUID); + if (serverPlayerEntity != null) { + serverPlayerEntity.networkHandler.disconnect( + reason != null + ? discordSRV.getAdventure().asNative(reason.asAdventure()) + : Text.translatable("multiplayer.disconnect.banned") + ); + } + } catch (Exception e) { + discordSRV.logger().error("Failed to ban player", e); + } + + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture removeBan(@NotNull UUID playerUUID) { + BannedPlayerList banList = discordSRV.getServer().getPlayerManager().getUserBanList(); + + Optional gameProfile = Objects.requireNonNull(discordSRV.getServer().getUserCache()).getByUuid(playerUUID); + if (gameProfile.isEmpty()) { + return CompletableFuture.completedFuture(null); + } + + banList.remove(gameProfile.get()); + return CompletableFuture.completedFuture(null); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricAdvancementModule.java b/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricAdvancementModule.java new file mode 100644 index 00000000..dd35094c --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricAdvancementModule.java @@ -0,0 +1,66 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.module.chat; + +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.events.message.receive.game.AwardMessageReceiveEvent; +import com.discordsrv.common.abstraction.player.IPlayer; +import com.discordsrv.common.util.ComponentUtil; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.discordsrv.fabric.module.AbstractFabricModule; +import net.minecraft.advancement.Advancement; +import net.minecraft.advancement.AdvancementEntry; +import net.minecraft.server.network.ServerPlayerEntity; + +public class FabricAdvancementModule extends AbstractFabricModule { + + private static FabricAdvancementModule instance; + private final FabricDiscordSRV discordSRV; + + public FabricAdvancementModule(FabricDiscordSRV discordSRV) { + super(discordSRV); + this.discordSRV = discordSRV; + instance = this; + } + + public static void onGrant(AdvancementEntry advancementEntry, ServerPlayerEntity owner) { + if (instance == null || !instance.enabled) return; + + FabricDiscordSRV discordSRV = instance.discordSRV; + Advancement advancement = advancementEntry.value(); + if (advancement.display().isEmpty() || advancement.name().isEmpty()) return; // Usually a crafting recipe. + MinecraftComponent advancementTitle = ComponentUtil.toAPI(discordSRV.getAdventure().asAdventure(advancement.display().get().getTitle())); + + // TODO: Add description to the event. So we can explain how the player got the advancement. +// String description = Formatting.strip(advancement.display().get().getDescription().getString()); +// MinecraftComponent advancementDescription = ComponentUtil.fromPlain(description); + + IPlayer player = discordSRV.playerProvider().player(owner); + discordSRV.eventBus().publish( + new AwardMessageReceiveEvent( + null, + player, + advancementTitle, + null, + null, + false + ) + ); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricChatModule.java b/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricChatModule.java new file mode 100644 index 00000000..9818d86a --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricChatModule.java @@ -0,0 +1,55 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.module.chat; + +import com.discordsrv.api.events.message.receive.game.GameChatMessageReceiveEvent; +import com.discordsrv.common.feature.channel.global.GlobalChannel; +import com.discordsrv.common.util.ComponentUtil; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.discordsrv.fabric.module.AbstractFabricModule; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; +import net.minecraft.network.message.MessageType; +import net.minecraft.network.message.SignedMessage; +import net.minecraft.server.network.ServerPlayerEntity; + +public class FabricChatModule extends AbstractFabricModule { + + private final FabricDiscordSRV discordSRV; + + public FabricChatModule(FabricDiscordSRV discordSRV) { + super(discordSRV); + this.discordSRV = discordSRV; + } + + public void register() { + ServerMessageEvents.CHAT_MESSAGE.register(this::onChatMessage); + } + + private void onChatMessage(SignedMessage signedMessage, ServerPlayerEntity serverPlayerEntity, MessageType.Parameters parameters) { + if (!enabled) return; + + discordSRV.eventBus().publish(new GameChatMessageReceiveEvent( + null, + discordSRV.playerProvider().player(serverPlayerEntity), + ComponentUtil.toAPI(discordSRV.getAdventure().asAdventure(signedMessage.getContent())), + new GlobalChannel(discordSRV), + false + )); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricDeathModule.java b/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricDeathModule.java new file mode 100644 index 00000000..45e70dba --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricDeathModule.java @@ -0,0 +1,64 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.module.chat; + +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.events.message.receive.game.DeathMessageReceiveEvent; +import com.discordsrv.api.player.DiscordSRVPlayer; +import com.discordsrv.common.util.ComponentUtil; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.discordsrv.fabric.module.AbstractFabricModule; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +public class FabricDeathModule extends AbstractFabricModule { + + private final FabricDiscordSRV discordSRV; + + public FabricDeathModule(FabricDiscordSRV discordSRV) { + super(discordSRV); + this.discordSRV = discordSRV; + } + + public void register() { + ServerLivingEntityEvents.AFTER_DEATH.register(this::onDeath); + } + + private void onDeath(LivingEntity livingEntity, DamageSource damageSource) { + if (!enabled) return; + if (livingEntity instanceof ServerPlayerEntity) { + Text message = damageSource.getDeathMessage(livingEntity); + MinecraftComponent minecraftComponent = ComponentUtil.toAPI(discordSRV.getAdventure().asAdventure(message)); + + DiscordSRVPlayer player = discordSRV.playerProvider().player((ServerPlayerEntity) livingEntity); + discordSRV.eventBus().publish( + new DeathMessageReceiveEvent( + damageSource, + player, + minecraftComponent, + null, + false + ) + ); + } + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricJoinModule.java b/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricJoinModule.java new file mode 100644 index 00000000..220400b4 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricJoinModule.java @@ -0,0 +1,80 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.module.chat; + +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.events.message.receive.game.JoinMessageReceiveEvent; +import com.discordsrv.api.player.DiscordSRVPlayer; +import com.discordsrv.common.util.ComponentUtil; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.discordsrv.fabric.module.AbstractFabricModule; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; + +import java.util.Objects; + +public class FabricJoinModule extends AbstractFabricModule { + + private final FabricDiscordSRV discordSRV; + + public FabricJoinModule(FabricDiscordSRV discordSRV) { + super(discordSRV); + this.discordSRV = discordSRV; + } + + public void register() { + ServerPlayConnectionEvents.JOIN.register(this::onJoin); + } + + private void onJoin(ServerPlayNetworkHandler serverPlayNetworkHandler, PacketSender packetSender, MinecraftServer minecraftServer) { + if (!enabled) return; + + ServerPlayerEntity playerEntity = serverPlayNetworkHandler.player; + MinecraftComponent component = getJoinMessage(playerEntity); + boolean firstJoin = Objects.requireNonNull(minecraftServer.getUserCache()).findByName(playerEntity.getGameProfile().getName()).isEmpty(); + + DiscordSRVPlayer player = discordSRV.playerProvider().player(playerEntity); + discordSRV.eventBus().publish( + new JoinMessageReceiveEvent( + serverPlayNetworkHandler, + player, + component, + null, + firstJoin, + false + ) + ); + } + + private MinecraftComponent getJoinMessage(ServerPlayerEntity playerEntity) { + MutableText mutableText; + if (playerEntity.getGameProfile().getName().equalsIgnoreCase(playerEntity.getName().getString())) { + mutableText = Text.translatable("multiplayer.player.joined", playerEntity.getDisplayName()); + } else { + mutableText = Text.translatable("multiplayer.player.joined.renamed", playerEntity.getDisplayName(), playerEntity.getName()); + } + + return ComponentUtil.toAPI(discordSRV.getAdventure().asAdventure(mutableText)); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricQuitModule.java b/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricQuitModule.java new file mode 100644 index 00000000..f1d0446e --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/module/chat/FabricQuitModule.java @@ -0,0 +1,69 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.module.chat; + +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.api.events.message.receive.game.LeaveMessageReceiveEvent; +import com.discordsrv.common.util.ComponentUtil; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.discordsrv.fabric.module.AbstractFabricModule; +import com.discordsrv.fabric.player.FabricPlayer; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +public class FabricQuitModule extends AbstractFabricModule { + + private final FabricDiscordSRV discordSRV; + + public FabricQuitModule(FabricDiscordSRV discordSRV) { + super(discordSRV); + this.discordSRV = discordSRV; + } + + public void register() { + ServerPlayConnectionEvents.DISCONNECT.register(this::onDisconnect); + } + + private void onDisconnect(ServerPlayNetworkHandler serverPlayNetworkHandler, MinecraftServer minecraftServer) { + if (!enabled) return; + + ServerPlayerEntity player = serverPlayNetworkHandler.player; + MinecraftComponent component = getQuitMessage(player); + + discordSRV.eventBus().publish( + new LeaveMessageReceiveEvent( + serverPlayNetworkHandler, + new FabricPlayer(discordSRV, player), + component, + null, + component == null + ) + ); + } + + public MinecraftComponent getQuitMessage(ServerPlayerEntity player) { + Text message = Text.translatable("multiplayer.player.left", player.getDisplayName()).formatted(Formatting.YELLOW); + + return ComponentUtil.toAPI(discordSRV.getAdventure().asAdventure(message)); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/player/FabricPlayer.java b/fabric/src/main/java/com/discordsrv/fabric/player/FabricPlayer.java new file mode 100644 index 00000000..3fdb7610 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/player/FabricPlayer.java @@ -0,0 +1,102 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.player; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.abstraction.player.IPlayer; +import com.discordsrv.common.abstraction.player.provider.model.SkinInfo; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.discordsrv.fabric.command.game.sender.FabricCommandSender; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + + +public class FabricPlayer extends FabricCommandSender implements IPlayer { + + private final ServerPlayerEntity player; + + public FabricPlayer(FabricDiscordSRV discordSRV, ServerPlayerEntity player) { + super(discordSRV, player.getCommandSource()); + this.player = player; + } + + @Override + public DiscordSRV discordSRV() { + return discordSRV; + } + + @Override + public @NotNull String username() { + return player.getName().getString(); + } + + @Override + public @Nullable Locale locale() { + return Locale.of(player.getClientOptions().language()); + } + + @Override + public CompletableFuture kick(Component component) { + player.networkHandler.disconnect(Text.of(component.toString())); + return CompletableFuture.completedFuture(null); + } + + @Override + public void addChatSuggestions(Collection suggestions) { + // API not available in Fabric + } + + @Override + public void removeChatSuggestions(Collection suggestions) { + // API not available in Fabric + } + + @Override + public @Nullable SkinInfo skinInfo() { + // Unimplemented + return null; + } + + @Override + public @NotNull Identity identity() { + return player.identity(); + } + + @Override + public @NotNull Component displayName() { + // Use Adventure's Pointer, otherwise username + return player.getOrDefaultFrom( + Identity.DISPLAY_NAME, + () -> Component.text(player.getName().getString()) + ); + } + + @Override + public String toString() { + return "FabricPlayer{" + username() + "}"; + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/player/FabricPlayerProvider.java b/fabric/src/main/java/com/discordsrv/fabric/player/FabricPlayerProvider.java new file mode 100644 index 00000000..0643da14 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/player/FabricPlayerProvider.java @@ -0,0 +1,83 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.player; + +import com.discordsrv.common.abstraction.player.provider.AbstractPlayerProvider; +import com.discordsrv.fabric.FabricDiscordSRV; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; + +public class FabricPlayerProvider extends AbstractPlayerProvider { + + private boolean enabled = false; + + public FabricPlayerProvider(FabricDiscordSRV discordSRV) { + super(discordSRV); + // Register events here instead of in subscribe() to avoid duplicate registrations. Since there's no unregister method for events in Fabric, we need to make sure we only register once. + ServerPlayConnectionEvents.JOIN.register(this::onConnection); + ServerPlayConnectionEvents.DISCONNECT.register(this::onDisconnect); + } + + @Override + public void subscribe() { + enabled = true; + if (discordSRV.getServer() == null || discordSRV.getServer().getPlayerManager() == null) { + return; // Server not started yet, So there's no players to add + } + + // Add players that are already connected + for (ServerPlayerEntity player : discordSRV.getServer().getPlayerManager().getPlayerList()) { + addPlayer(player, true); + } + } + + @Override + public void unsubscribe() { + enabled = false; + } + + private void onConnection(ServerPlayNetworkHandler serverPlayNetworkHandler, PacketSender packetSender, MinecraftServer minecraftServer) { + addPlayer(serverPlayNetworkHandler.player, false); + } + + private void onDisconnect(ServerPlayNetworkHandler serverPlayNetworkHandler, MinecraftServer minecraftServer) { + removePlayer(serverPlayNetworkHandler.player); + } + + private void addPlayer(ServerPlayerEntity player, boolean initial) { + if (!enabled) return; + addPlayer(player.getUuid(), new FabricPlayer(discordSRV, player), initial); + } + + private void removePlayer(ServerPlayerEntity player) { + if (!enabled) return; + removePlayer(player.getUuid()); + } + + public FabricPlayer player(ServerPlayerEntity player) { + FabricPlayer srvPlayer = player(player.getUuid()); + if (srvPlayer == null) { + throw new IllegalStateException("Player not available"); + } + return srvPlayer; + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/plugin/FabricModManager.java b/fabric/src/main/java/com/discordsrv/fabric/plugin/FabricModManager.java new file mode 100644 index 00000000..1c348b6c --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/plugin/FabricModManager.java @@ -0,0 +1,53 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.plugin; + +import com.discordsrv.common.abstraction.plugin.Plugin; +import com.discordsrv.common.abstraction.plugin.PluginManager; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.metadata.Person; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +public class FabricModManager implements PluginManager { + + @Override + public boolean isPluginEnabled(String modIdentifier) { + return FabricLoader.getInstance().isModLoaded(modIdentifier.toLowerCase(Locale.ROOT)); + } + + @Override + public List getPlugins() { + return FabricLoader.getInstance().getAllMods().stream() + .map(modContainer -> { + String id = modContainer.getMetadata().getId(); + return new Plugin( + id, + modContainer.getMetadata().getName(), + modContainer.getMetadata().getVersion().toString(), + modContainer.getMetadata().getAuthors().stream() + .map(Person::getName) + .collect(Collectors.toList()) + ); + }) + .collect(Collectors.toList()); + } +} diff --git a/fabric/src/main/java/com/discordsrv/fabric/requiredlinking/FabricRequiredLinkingModule.java b/fabric/src/main/java/com/discordsrv/fabric/requiredlinking/FabricRequiredLinkingModule.java new file mode 100644 index 00000000..fb72bc81 --- /dev/null +++ b/fabric/src/main/java/com/discordsrv/fabric/requiredlinking/FabricRequiredLinkingModule.java @@ -0,0 +1,309 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2025 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.fabric.requiredlinking; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.abstraction.player.IPlayer; +import com.discordsrv.common.config.main.linking.ServerRequiredLinkingConfig; +import com.discordsrv.common.feature.linking.LinkStore; +import com.discordsrv.common.feature.linking.requirelinking.ServerRequireLinkingModule; +import com.discordsrv.fabric.FabricDiscordSRV; +import com.discordsrv.fabric.player.FabricPlayer; +import com.github.benmanes.caffeine.cache.Cache; +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.ParseResults; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.ServerConfigurationConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.kyori.adventure.text.Component; +import net.minecraft.network.message.MessageType; +import net.minecraft.network.message.SignedMessage; +import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerConfigurationNetworkHandler; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + + +public class FabricRequiredLinkingModule extends ServerRequireLinkingModule { + + private static FabricRequiredLinkingModule instance; + private final Cache linkCheckRateLimit; + private final Map frozen = new ConcurrentHashMap<>(); + private final List loginsHandled = new CopyOnWriteArrayList<>(); + private boolean enabled = false; + + public FabricRequiredLinkingModule(FabricDiscordSRV discordSRV) { + super(discordSRV); + this.linkCheckRateLimit = discordSRV.caffeineBuilder() + .expireAfterWrite(LinkStore.LINKING_CODE_RATE_LIMIT) + .build(); + + register(); + + instance = this; + } + + // + // Kick + // + + @Nullable + public static Text checkCanJoin(GameProfile profile) { + if (instance == null || (instance.discordSRV != null && instance.discordSRV.status() != DiscordSRV.Status.CONNECTED)) { + return Text.of("Currently unavailable to check link status because the server is still connecting to Discord.\n\nTry again in a minute."); + } + if (!instance.enabled) return null; + + FabricDiscordSRV discordSRV = instance.discordSRV; + assert discordSRV != null; + ServerRequiredLinkingConfig config = instance.config(); + if (!config.enabled || config.action != ServerRequiredLinkingConfig.Action.KICK) { + return null; + } + + UUID playerUUID = profile.getId(); + String playerName = profile.getName(); + + Component kickReason = instance.getBlockReason(playerUUID, playerName, true).join(); + if (kickReason != null) { + return discordSRV.getAdventure().asNative(kickReason); + } + + return null; + } + + // + // Freeze + // + + public static void onPlayerMove(ServerPlayerEntity player, PlayerMoveC2SPacket packet, CallbackInfo ci) { + if (instance == null || !instance.enabled) return; + Component freezeReason = instance.frozen.get(player.getUuid()); + if (freezeReason == null) { + return; + } + + BlockPos from = player.getBlockPos(); + BlockPos to = new BlockPos( + MathHelper.floor(packet.getX(player.getX())), + MathHelper.floor(packet.getY(player.getY())), + MathHelper.floor(packet.getZ(player.getZ())) + ); + if (from.getX() == to.getX() && from.getY() >= to.getY() && from.getZ() == to.getZ()) { + return; + } + + player.requestTeleport(from.getX() + 0.5, from.getY(), from.getZ() + 0.5); + IPlayer iPlayer = instance.discordSRV.playerProvider().player(player); + iPlayer.sendMessage(freezeReason); + + ci.cancel(); + } + + public static void onCommandExecute(ParseResults parseResults, String command, CallbackInfo ci) { + if (instance == null || !instance.enabled) return; + FabricDiscordSRV discordSRV = instance.discordSRV; + ServerPlayerEntity playerEntity = parseResults.getContext().getSource().getPlayer(); + if (playerEntity == null) return; + + if (!instance.isFrozen(playerEntity)) { + return; + } + + if (command.startsWith("/")) command = command.substring(1); + if (command.equals("discord link") || command.equals("link")) { + + FabricPlayer player = discordSRV.playerProvider().player(playerEntity); + + UUID uuid = player.uniqueId(); + + if (instance.linkCheckRateLimit.getIfPresent(uuid) != null) { + player.sendMessage(discordSRV.messagesConfig(player).pleaseWaitBeforeRunningThatCommandAgain.asComponent()); + return; + } + instance.linkCheckRateLimit.put(uuid, true); + + player.sendMessage(discordSRV.messagesConfig(player).checkingLinkStatus.asComponent()); + + instance.getBlockReason(uuid, player.username(), false).whenComplete((reason, t) -> { + if (t != null) { + return; + } + + if (reason == null) { + instance.frozen.remove(uuid); + player.sendMessage(discordSRV.messagesConfig(player).nowLinked1st.asComponent()); + } else { + instance.freeze(player, reason); + } + }); + } + + ci.cancel(); + } + + @Override + public ServerRequiredLinkingConfig config() { + return discordSRV.config().requiredLinking; + } + + @Override + public void enable() { + super.enable(); + + this.enabled = true; + } + + public void register() { + ServerMessageEvents.ALLOW_CHAT_MESSAGE.register(this::allowChatMessage); + ServerConfigurationConnectionEvents.CONFIGURE.register(this::onPlayerPreLogin); + ServerPlayConnectionEvents.JOIN.register(this::onPlayerJoin); + ServerPlayConnectionEvents.DISCONNECT.register(this::onPlayerQuit); + } + + @Override + public void disable() { + super.disable(); + + this.enabled = false; + } + + @Override + public void recheck(IPlayer player) { + getBlockReason(player.uniqueId(), player.username(), false).whenComplete((component, throwable) -> { + if (component != null) { + switch (action()) { + case KICK: + player.kick(component); + break; + case FREEZE: + freeze(player, component); + break; + } + } else if (action() == ServerRequiredLinkingConfig.Action.FREEZE) { + frozen.remove(player.uniqueId()); + } + }); + } + + public ServerRequiredLinkingConfig.Action action() { + return config().action; + } + + // + // Freeze + // + + private boolean isFrozen(ServerPlayerEntity player) { + Component freezeReason = frozen.get(player.getUuid()); + if (freezeReason == null) { + frozen.remove(player.getUuid()); + return false; + } + return true; + } + + private void freeze(IPlayer player, Component blockReason) { + frozen.put(player.uniqueId(), blockReason); + player.sendMessage(blockReason); + } + + private boolean allowChatMessage(SignedMessage signedMessage, ServerPlayerEntity player, MessageType.Parameters parameters) { + // True if the message should be sent + Component freezeReason = instance.frozen.get(player.getUuid()); + if (freezeReason == null) { + return true; + } + + IPlayer iPlayer = instance.discordSRV.playerProvider().player(player); + iPlayer.sendMessage(freezeReason); + return false; + } + + private void onPlayerPreLogin(ServerConfigurationNetworkHandler handler, MinecraftServer minecraftServer) { + if (!enabled) return; + UUID playerUUID = handler.getDebugProfile().getId(); + loginsHandled.add(playerUUID); + handleLogin(playerUUID, handler.getDebugProfile().getName()); + } + + private void onPlayerJoin(ServerPlayNetworkHandler serverPlayNetworkHandler, PacketSender packetSender, MinecraftServer minecraftServer) { + if (!enabled) return; + UUID playerUUID = serverPlayNetworkHandler.player.getUuid(); + + if (!loginsHandled.contains(playerUUID)) { + handleLogin(playerUUID, serverPlayNetworkHandler.player.getName().getString()); + } + + Component blockReason = frozen.get(playerUUID); + if (blockReason == null) { + return; + } + + IPlayer srvPlayer = discordSRV.playerProvider().player(playerUUID); + if (srvPlayer == null) { + throw new IllegalStateException("Player not available: " + playerUUID); + } + + srvPlayer.sendMessage(blockReason); + } + + private void onPlayerQuit(ServerPlayNetworkHandler serverPlayNetworkHandler, MinecraftServer minecraftServer) { + if (!enabled) return; + UUID playerUUID = serverPlayNetworkHandler.player.getUuid(); + loginsHandled.remove(playerUUID); + frozen.remove(playerUUID); + } + + private void handleLogin(UUID playerUUID, String username) { + if (discordSRV.isShutdown()) { + return; + } else if (!discordSRV.isReady()) { + try { + discordSRV.waitForStatus(DiscordSRV.Status.CONNECTED); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + ServerRequiredLinkingConfig config = config(); + if (!config.enabled || config.action != ServerRequiredLinkingConfig.Action.FREEZE) { + return; + } + + Component blockReason = getBlockReason(playerUUID, username, false).join(); + if (blockReason != null) { + frozen.put(playerUUID, blockReason); + } + } +} diff --git a/fabric/src/main/resources/discordsrv.accesswidener b/fabric/src/main/resources/discordsrv.accesswidener new file mode 100644 index 00000000..3fde5170 --- /dev/null +++ b/fabric/src/main/resources/discordsrv.accesswidener @@ -0,0 +1 @@ +accessWidener v1 named diff --git a/fabric/src/main/resources/discordsrv.mixins.json b/fabric/src/main/resources/discordsrv.mixins.json new file mode 100644 index 00000000..81e555c0 --- /dev/null +++ b/fabric/src/main/resources/discordsrv.mixins.json @@ -0,0 +1,16 @@ +{ + "required": true, + "package": "com.discordsrv.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "PlayerAdvancementTrackerMixin", + "ban.BanCommandMixin", + "ban.PardonCommandMixin", + "requiredlinking.CommandManagerMixin", + "requiredlinking.PlayerManagerMixin", + "requiredlinking.ServerPlayNetworkHandlerMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..d8e1fc29 --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "id": "discordsrv", + "version": "${VERSION}", + "name": "DiscordSRV", + "description": "", + "authors": [], + "contact": {}, + "license": "GPLv3", + "environment": "server", + "entrypoints": { + "server": [ + "com.discordsrv.fabric.DiscordSRVFabricBootstrap" + ] + }, + "mixins": [ + "discordsrv.mixins.json" + ], + "jars": [ + { + "file": "META-INF/jars/adventure-platform-mod-shared-fabric-repack-6.1.0.jar" + } + ], + "accessWidener": "discordsrv.accesswidener", + "depends": { + "fabricloader": ">=${LOADER_VERSION}", + "fabric": "*", + "minecraft": "${MINECRAFT_VERSION}", + "fabric-permissions-api-v0": "*" + } +} diff --git a/gradle.properties b/gradle.properties index 1f0dad2c..77a90b8a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ # Lower compile time by parallelizing the build org.gradle.parallel=true +org.gradle.jvmargs=-Xmx4096M diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136..a4b76b95 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew index b740cf13..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30db..9d21a218 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/settings.gradle b/settings.gradle index 01b20669..505ef2bc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ pluginManagement { repositories { mavenLocal() maven { url = 'https://s01.oss.sonatype.org/content/repositories/snapshots/' } + maven { url = 'https://maven.fabricmc.net/'} gradlePluginPortal() } } @@ -39,6 +40,15 @@ dependencyResolutionManagement { // Velocity library('velocity', 'com.velocitypowered', 'velocity-api').version('3.4.0-SNAPSHOT') + // Fabric + version('fabric-loom', '1.9-SNAPSHOT') + plugin('fabric-loom', 'fabric-loom').versionRef('fabric-loom') + library('fabric-minecraft', 'com.mojang', 'minecraft').version('1.21.4') + library('fabric-yarn', 'net.fabricmc', 'yarn').version('1.21.4+build.8') + library('fabric-loader', 'net.fabricmc', 'fabric-loader').version('0.16.9') + library('fabric-api', 'net.fabricmc.fabric-api', 'fabric-api').version('0.114.3+1.21.4') + library('fabric-permissions-api', 'me.lucko', 'fabric-permissions-api').version('0.3.3') + // DependencyDownload version('dependencydownload', '2.0.0-SNAPSHOT') plugin('dependencydownload-plugin', 'dev.vankka.dependencydownload.plugin').versionRef('dependencydownload') @@ -53,6 +63,7 @@ dependencyResolutionManagement { library('mcdependencydownload-bungee-bootstrap', 'dev.vankka', 'minecraftdependencydownload-bungee').versionRef('mcdependencydownload') library('mcdependencydownload-bungee-loader', 'dev.vankka', 'minecraftdependencydownload-bungee-loader').versionRef('mcdependencydownload') library('mcdependencydownload-velocity', 'dev.vankka', 'minecraftdependencydownload-velocity').versionRef('mcdependencydownload') + library('mcdependencydownload-fabric', 'dev.vankka', 'minecraftdependencydownload-fabric').versionRef('mcdependencydownload') // Annotations library('jetbrains-annotations', 'org.jetbrains', 'annotations').version('24.1.0') @@ -132,8 +143,10 @@ dependencyResolutionManagement { // Adventure Platform version('adventure-platform', '4.3.4') + version('adventure-platform-mod', '6.2.0') library('adventure-platform-bukkit', 'net.kyori', 'adventure-platform-bukkit').versionRef('adventure-platform') library('adventure-platform-bungee', 'net.kyori', 'adventure-platform-bungeecord').versionRef('adventure-platform') + library('adventure-platform-fabric', 'net.kyori', 'adventure-platform-fabric').versionRef('adventure-platform-mod') library('adventure-serializer-bungee', 'net.kyori', 'adventure-text-serializer-bungeecord').versionRef('adventure-platform') // Upgrade ansi (used by ansi serializer) @@ -161,7 +174,9 @@ rootProject.name = 'DiscordSRV-Ascension' // Bungee 'bungee', 'bungee:loader', // Velocity - 'velocity' + 'velocity', + // Fabric + 'fabric' ].each { include it findProject(':' + it).name = String.join('-', it.split(':'))