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(':'))