diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e3cb313..759a226 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @Thatsmusic99 @MattyTheHacker +* @Thatsmusic99 @MattyTheHacker @LMBishop diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c1aee4..04c2b2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,39 +3,42 @@ name: "Build" on: push: branches: [ "master" ] + pull_request: + branches: [ "master" ] jobs: build: - name: Build - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - strategy: fail-fast: false matrix: - language: [ 'java' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v3 + target: [ bukkit, fabric, forge ] - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - - - name: Setup Java and Maven - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - - name: Build with Maven - run: mvn -B package --file pom.xml + name: Build ${{ matrix.target }} + runs-on: ubuntu-latest + if: "!startsWith(github.event.commits[0].message, '[ci-skip]')" - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + cache: gradle + cache-dependency-path: | + **/*.gradle* + **/gradle-wrapper.properties + + - name: Build with Gradle + run: "./gradlew :${{matrix.target}}:build" + + - name: Upload builds + uses: actions/upload-artifact@v4 + with: + name: "cssminecraft-${{matrix.target}}.jar" + path: | + ${{matrix.target}}/build/libs/cssminecraft-${{matrix.target}}-*.jar + !**/*-no-map.jar + if-no-files-found: error diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml deleted file mode 100644 index 19da554..0000000 --- a/.github/workflows/pullrequest.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: "Build Pull Request" - -on: - pull_request: - # The branches below must be a subset of the branches above - branches: [ "master" ] - -jobs: - build: - name: Build - runs-on: ubuntu-latest - permissions: - actions: read - contents: write - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'java' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - - - name: Setup Java and Maven - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - - name: Build with Maven - run: mvn -B package --file pom.xml - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.gitignore b/.gitignore index ffac74e..4bb6260 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ target/ *.iml +.idea +.gradle +**/build/ +!src/**/build/ +gradle-app.setting +!gradle-wrapper.jar +!gradle-wrapper.properties +.gradletasknamecache diff --git a/README.md b/README.md index f846f4f..6b5de71 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,110 @@ -## CSS-Minecraft -This is the source code for CSS' Minecraft plugin. +# CSS Minecraft +This is the source code for CSS' Minecraft plugin/mod. ## Building -Ensure that you have maven installed. -Then: +To compile, run the `build` Gradle task. + ``` -maven clean package +./gradlew build ``` -This will automatically execute the shadow-jar goal in the package cycle. - -There will now be a .jar file located in /target - -You can now put this .jar file in the /plugins folder in a Spigot/CraftBukkit 1.17.1 server and run the server. +Jars will be output to `**/build/libs/cssminecraft-*.jar`. ## Configuration +The configuration file will be located in the server configuration directory, which depends on the platform. +Typically, this will be at: -MEMBER_ROLE_ID: The ID of the role that the plugin checks against when someone runs the /makegreen command. +* **bukkit**: `plugins/CSSMinecraft/config.yml` +* **fabric**: `config/cssminecraft/config.yml` +* **forge**: `config/cssminecraft/config.yml` -BRIDGE_CHANNEL_ID: The ID of the channel to send bridged messages to Minecraft. +```yaml +# The ID of the role that the plugin checks against when someone runs the /makegreen command. +member-role-id: 0 -DISCORD_SERVER_ID: The ID of the guild to interact with. +# The ID of the channel to send bridged messages to Minecraft. +bridge-channel-id: 0 -WEBHOOK_URL: The URL of the Discord webhook to send bridged messages from Minecraft. +# The ID of the guild to interact with. +discord-server-id: 0 -AVATAR_SERVICE: A link to an avatar service, with %s as a placeholder of the user's minecraft username. +# The URL of the Discord webhook to send bridged messages from Minecraft. +webhook-url: "" -BOT_TOKEN: The token of the Discord bot that will be detecting messages to send to Minecraft, as well as Member roles. +# The token of the Discord bot that will be detecting messages to send to Minecraft, as well as Member roles. +bot-token: "" +# A link to an avatar service, with %s as a placeholder of the user's minecraft username. +# This is used as the profile picture URL in webhook messages. +# We'd recommend the following value: https://cravatar.eu/helmhead/%s/190.png +avatar-service: "" + +# The verbosity of logging (0 = error only, 1 = +warnings, 2 = +info, 3 = +debug) +logging-level: 2 +``` ## Dependencies -This plugin depends on [LuckPerms](https://www.spigotmc.org/resources/luckperms.28140/), which needs to be placed alongside this in the /plugins folder. +This plugin optionally depends on [LuckPerms](https://luckperms.net/) to grant the member role. + +Without it, only the Discord message bridge will be functional. ## Development PR's welcome, feel free to do whatever. + +The project is written mostly in an abstract fashion to help re-use code across +different platforms. + +Each Gradle subproject has the following purpose: +* `/common`: platform-independent interfaces and implementations which houses most logic - the +following subprojects depend on this +* `/bukkit`: specific implementation for Bukkit / Spigot / Paper etc. +* `/fabric`: specific implementation for Fabric servers +* `/forge`: specific implementation for Forge servers + +Note that this is a server only mod, and will not work on clients. + +## Version matrix + +The following table documents mod compatibility for older versions of Minecraft and their platforms. +Any versions prior to 1.12.1 are backports. + +If a Minecraft version is not listed here, then no version of the mod exists for it. + +All version branches will follow the name `minecraft/`. + +| Minecraft | Java | Bukkit | Forge | Fabric | Links | +|-----------|------|--------|-------|--------|-------------------------------------------------------------------------| +| 1.21.1 | 21 | ✅ | ✅ | ✅ | (master) | +| 1.18.2 | 17 | ❌ | ❌ | ✅ | [Branch](https://github.com/CSSUoB/CSS-Minecraft/tree/minecraft/1.18.2) | +| 1.12.2 | 8 | ❌ | ✅ | ❌ | [Branch](https://github.com/CSSUoB/CSS-Minecraft/tree/minecraft/1.12.2) | + +**Never merge `minecraft/*` branches into master.** Build features/fixes in master and cherry-pick backwards. + +### Upgrading to future versions + +The `master` branch should always target the latest version. +Before upgrading, create a new release branch for the current version using the naming +scheme `minecraft/`. + +Then, make the necessary changes to upgrade Minecraft version. Bukkit / Spigot / Paper +has a stable enough API where not many changes will be needed (if any at all), but +other platforms will likely break. + +Once changes are done, update the version matrix and open a new PR to `master`. + +### Backporting to older versions + +This mod was originally made for Minecraft 1.21, thus +it will require backporting to work on older modpacks. + +Create a branch from the nearest Minecraft version and name it `minecraft/`. +You may be required to change the Java version, or upgrade/downgrade Gradle. +It should be noted that Fabric does not exist prior to Minecraft 1.14. + +Once finished, push the branch to GitHub and update this version matrix with the platform +and version you have backported. + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4f38adf --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +plugins { + id "java" + id "com.gradleup.shadow" version "8.3.0" +} + +subprojects { + apply plugin: "java" + apply plugin: "com.gradleup.shadow" + + group = "com.cssbham" + version = "1.0.0" + + sourceCompatibility = 21 + targetCompatibility = 21 + + tasks.withType(JavaCompile) { + options.encoding = "UTF-8" + } + + tasks.withType(Javadoc) { + options.encoding = "UTF-8" + } + + repositories { + mavenCentral() + } + + assemble.dependsOn shadowJar +} diff --git a/bukkit/build.gradle b/bukkit/build.gradle new file mode 100644 index 0000000..64c6534 --- /dev/null +++ b/bukkit/build.gradle @@ -0,0 +1,31 @@ +plugins { + id "java" +} + +processResources { + duplicatesStrategy = duplicatesStrategy.INCLUDE + from(sourceSets.main.resources.srcDirs) { + include "plugin.yml" + expand("version": project.version) + } +} + +repositories { + maven { url = "https://repo.papermc.io/repository/maven-public/" } +} + +dependencies { + compileOnly "io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT" + + implementation project(path: ":common", configuration: "shadow") +} + +shadowJar { + dependencies { + include(project(":common")) + } + + archiveFileName = "cssminecraft-bukkit-${project.version}.jar" + + minimize() +} \ No newline at end of file diff --git a/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/BukkitCSSMinecraftPlugin.java b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/BukkitCSSMinecraftPlugin.java new file mode 100644 index 0000000..8394771 --- /dev/null +++ b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/BukkitCSSMinecraftPlugin.java @@ -0,0 +1,71 @@ +package com.cssbham.cssminecraft.bukkit; + +import com.cssbham.cssminecraft.bukkit.adapter.BukkitServerChatAdapter; +import com.cssbham.cssminecraft.bukkit.command.BukkitCommandService; +import com.cssbham.cssminecraft.bukkit.executor.BukkitServerExecutor; +import com.cssbham.cssminecraft.bukkit.listener.BukkitEventListener; +import com.cssbham.cssminecraft.bukkit.logger.BukkitLogger; +import com.cssbham.cssminecraft.common.AbstractCSSMinecraftPlugin; +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import com.cssbham.cssminecraft.common.command.CommandService; +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import com.cssbham.cssminecraft.common.logger.Logger; +import org.bukkit.plugin.java.JavaPlugin; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Implementation of CSS Minecraft Plugin for Bukkit + */ +public class BukkitCSSMinecraftPlugin extends AbstractCSSMinecraftPlugin { + + private final JavaPlugin plugin; + private final BukkitLogger logger; + private final BukkitServerChatAdapter serverChatAdapter; + private final BukkitServerExecutor executor; + private final BukkitCommandService commandService; + + public BukkitCSSMinecraftPlugin(JavaPlugin plugin) { + this.plugin = plugin; + this.logger = new BukkitLogger(plugin); + this.serverChatAdapter = new BukkitServerChatAdapter(); + this.executor = new BukkitServerExecutor(logger, plugin); + this.commandService = new BukkitCommandService(logger, executor, serverChatAdapter); + } + + @Override + public void enable() { + super.enable(); + + BukkitEventListener eventListener = new BukkitEventListener(plugin, executor); + eventListener.bindPlatformToEventBus(super.getEventBus()); + + plugin.getCommand("makegreen").setExecutor(commandService); + } + + @Override + public Logger getLogger() { + return logger; + } + + @Override + public ServerChatAdapter provideServerChatAdapter() { + return serverChatAdapter; + } + + @Override + public Path provideConfigurationPath() { + return Paths.get(plugin.getDataFolder().getPath(), "config.yml"); + } + + @Override + public ServerExecutor provideServerExecutor() { + return executor; + } + + @Override + public CommandService provideCommandService() { + return commandService; + } +} diff --git a/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/CSSMinecraftLoader.java b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/CSSMinecraftLoader.java new file mode 100644 index 0000000..c4d419d --- /dev/null +++ b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/CSSMinecraftLoader.java @@ -0,0 +1,32 @@ +package com.cssbham.cssminecraft.bukkit; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Entrypoint for Bukkit API + */ +public class CSSMinecraftLoader extends JavaPlugin { + + private final BukkitCSSMinecraftPlugin plugin; + + public CSSMinecraftLoader() { + this.plugin = new BukkitCSSMinecraftPlugin(this); + } + + @Override + public void onEnable() { + try { + plugin.enable(); + } catch (Exception e) { + plugin.getLogger().severe("Plugin initialisation failed - disabling"); + Bukkit.getPluginManager().disablePlugin(this); + } + } + + @Override + public void onDisable() { + plugin.disable(); + } + +} diff --git a/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/adapter/BukkitServerChatAdapter.java b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/adapter/BukkitServerChatAdapter.java new file mode 100644 index 0000000..2ae374b --- /dev/null +++ b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/adapter/BukkitServerChatAdapter.java @@ -0,0 +1,30 @@ +package com.cssbham.cssminecraft.bukkit.adapter; + +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.UUID; + +public class BukkitServerChatAdapter implements ServerChatAdapter { + + @Override + public void broadcastMessage(Component message) { + Bukkit.broadcast(message); + } + + @Override + public void sendMessageToPlayer(UUID user, Component component) { + Player player = Bukkit.getPlayer(user); + if (null != player) { + player.sendMessage(component); + } + } + + @Override + public void sendMessageToConsole(Component component) { + Bukkit.getConsoleSender().sendMessage(component); + } + +} diff --git a/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/command/BukkitCommandService.java b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/command/BukkitCommandService.java new file mode 100644 index 0000000..4081680 --- /dev/null +++ b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/command/BukkitCommandService.java @@ -0,0 +1,50 @@ +package com.cssbham.cssminecraft.bukkit.command; + +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import com.cssbham.cssminecraft.common.command.AbstractCommandService; +import com.cssbham.cssminecraft.common.command.CommandContext; +import com.cssbham.cssminecraft.common.command.CommandSender; +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import com.cssbham.cssminecraft.common.logger.Logger; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class BukkitCommandService extends AbstractCommandService implements CommandExecutor { + + private ServerChatAdapter chatAdapter; + + public BukkitCommandService(Logger logger, ServerExecutor executor, ServerChatAdapter chatAdapter) { + super(logger, executor); + + this.chatAdapter = chatAdapter; + } + + @Override + public boolean onCommand(@NotNull org.bukkit.command.CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + CommandSender commandSender; + if (sender instanceof Player player) { + commandSender = new CommandSender( + chatAdapter, + player.getUniqueId(), + player.getName(), + false + ); + } else { + commandSender = new CommandSender( + chatAdapter, + new UUID(0, 0), + sender.getName(), + true + ); + } + + CommandContext context = new CommandContext(label, args); + + super.execute(commandSender, context); + return true; + } +} diff --git a/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/executor/BukkitServerExecutor.java b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/executor/BukkitServerExecutor.java new file mode 100644 index 0000000..2756de5 --- /dev/null +++ b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/executor/BukkitServerExecutor.java @@ -0,0 +1,21 @@ +package com.cssbham.cssminecraft.bukkit.executor; + +import com.cssbham.cssminecraft.common.executor.AsyncServerExecutor; +import com.cssbham.cssminecraft.common.logger.Logger; +import org.bukkit.plugin.java.JavaPlugin; + +public class BukkitServerExecutor extends AsyncServerExecutor { + + private final JavaPlugin plugin; + + public BukkitServerExecutor(Logger logger, JavaPlugin plugin) { + super(logger); + this.plugin = plugin; + } + + @Override + public void doSync(Runnable runnable) { + plugin.getServer().getScheduler().runTask(plugin, runnable); + } + +} diff --git a/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/listener/BukkitEventListener.java b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/listener/BukkitEventListener.java new file mode 100644 index 0000000..2c5a137 --- /dev/null +++ b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/listener/BukkitEventListener.java @@ -0,0 +1,84 @@ +package com.cssbham.cssminecraft.bukkit.listener; + +import com.cssbham.cssminecraft.common.event.Event; +import com.cssbham.cssminecraft.common.event.EventBus; +import com.cssbham.cssminecraft.common.event.PlatformEventAdapter; +import com.cssbham.cssminecraft.common.event.events.ServerMessageEvent; +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.Objects; + +public class BukkitEventListener implements Listener, PlatformEventAdapter { + + private final JavaPlugin plugin; + private final ServerExecutor executor; + private EventBus eventBus; + + public BukkitEventListener(JavaPlugin plugin, ServerExecutor executor) { + this.plugin = plugin; + this.executor = executor; + } + + @Override + public void bindPlatformToEventBus(EventBus eventBus) { + this.eventBus = eventBus; + + plugin.getServer().getPluginManager().registerEvents(this, plugin); + } + + private void dispatchEvent(Event event) { + Objects.requireNonNull(event, "event bus not bound"); + + executor.doAsync(() -> eventBus.dispatch(event)); + } + + @EventHandler + public void onPlayerChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + + dispatchEvent(new ServerMessageEvent( + player.getUniqueId(), + player.getName(), + componentToString(player.displayName()), + event.getMessage() + )); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + + dispatchEvent(new com.cssbham.cssminecraft.common.event.events.PlayerJoinEvent( + player.getUniqueId(), + player.getName(), + componentToString(player.displayName()), + plugin.getServer().getOnlinePlayers().size() + )); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + + dispatchEvent(new com.cssbham.cssminecraft.common.event.events.PlayerQuitEvent( + player.getUniqueId(), + player.getName(), + componentToString(player.displayName()), + plugin.getServer().getOnlinePlayers().size() - 1 + )); + } + + private String componentToString(Component component) { + return PlainTextComponentSerializer.plainText().serialize(component); + } + +} diff --git a/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/logger/BukkitLogger.java b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/logger/BukkitLogger.java new file mode 100644 index 0000000..1985d97 --- /dev/null +++ b/bukkit/src/main/java/com/cssbham/cssminecraft/bukkit/logger/BukkitLogger.java @@ -0,0 +1,29 @@ +package com.cssbham.cssminecraft.bukkit.logger; + +import com.cssbham.cssminecraft.common.logger.AbstractLogger; +import org.bukkit.plugin.java.JavaPlugin; + +public class BukkitLogger extends AbstractLogger { + + private final JavaPlugin plugin; + + public BukkitLogger(JavaPlugin plugin) { + this.plugin = plugin; + } + + @Override + protected void logInfo(String string) { + plugin.getLogger().info(string); + } + + @Override + protected void logError(String string) { + plugin.getLogger().severe(string); + } + + @Override + protected void logWarning(String string) { + plugin.getLogger().warning(string); + } + +} diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml new file mode 100644 index 0000000..b5a953c --- /dev/null +++ b/bukkit/src/main/resources/plugin.yml @@ -0,0 +1,13 @@ +name: CSSMinecraft +version: '${version}' +main: com.cssbham.cssminecraft.bukkit.CSSMinecraftLoader +api-version: 1.21 # probably +authors: [ RaineTheBoosted, LMBishop ] +description: CSS' Minecraft plugin +website: https://github.com/CSSUoB/CSS-Minecraft +softdepend: [ LuckPerms ] +commands: + makegreen: + description: Make yourself green by verifying your CSS membership. + usage: / [Discord Username] + aliases: [ mg, green ] \ No newline at end of file diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..8cb6aff --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,36 @@ +plugins { + id "java" + id "java-library" +} + +dependencies { + implementation ("net.dv8tion:JDA:5.0.2") { + exclude(module: "opus-java") + exclude(module: "annotations") + } + implementation "club.minnced:discord-webhooks:0.8.4" + implementation "org.yaml:snakeyaml:2.2" + + compileOnly "net.kyori:adventure-api:4.17.0" + compileOnly "net.luckperms:api:5.4" +} + +shadowJar { + relocate "club.minnced", "com.cssbham.cssminecraft.lib.discordwebhooks" + relocate "com.fasterxml.jackson", "com.cssbham.cssminecraft.lib.jackson" + relocate "com.iwebpp.crypto", "com.cssbham.cssminecraft.lib.iwebppcrypto" + relocate "com.neovisionaries.ws", "com.cssbham.cssminecraft.lib.nvws" + relocate "gnu.trove", "com.cssbham.cssminecraft.lib.trove" + relocate "net.dv8tion.jda", "com.cssbham.cssminecraft.lib.jda" + relocate "okhttp3", "com.cssbham.cssminecraft.lib.okhttp3" + relocate "okio", "com.cssbham.cssminecraft.lib.okio" + relocate "org.apache.commons.collections4", "com.cssbham.cssminecraft.lib.collections4" + relocate "org.slf4j", "com.cssbham.cssminecraft.lib.slf4j" + relocate "org.yaml.snakeyaml", "com.cssbham.cssminecraft.lib.snakeyaml" + relocate "org.intellij.lang.annotations", "com.cssbham.cssminecraft.lib.annotations.intellij" + relocate "org.jetbrains.annotations", "com.cssbham.cssminecraft.lib.annotations.jetbrains" + relocate "org.json", "com.cssbham.cssminecraft.lib.json" + relocate "kotlin", "com.cssbham.cssminecraft.lib.kotlin" + + minimize() +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/AbstractCSSMinecraftPlugin.java b/common/src/main/java/com/cssbham/cssminecraft/common/AbstractCSSMinecraftPlugin.java new file mode 100644 index 0000000..867d777 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/AbstractCSSMinecraftPlugin.java @@ -0,0 +1,119 @@ +package com.cssbham.cssminecraft.common; + +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import com.cssbham.cssminecraft.common.command.CommandService; +import com.cssbham.cssminecraft.common.command.handler.MakeGreenCommandHandler; +import com.cssbham.cssminecraft.common.config.ConfigService; +import com.cssbham.cssminecraft.common.config.option.ConfigOption; +import com.cssbham.cssminecraft.common.config.source.ConfigSource; +import com.cssbham.cssminecraft.common.config.source.YamlConfigSource; +import com.cssbham.cssminecraft.common.discord.DiscordClientService; +import com.cssbham.cssminecraft.common.event.EventBus; +import com.cssbham.cssminecraft.common.event.SimpleEventBus; +import com.cssbham.cssminecraft.common.event.events.DiscordMessageEvent; +import com.cssbham.cssminecraft.common.event.events.PlayerJoinEvent; +import com.cssbham.cssminecraft.common.event.events.PlayerQuitEvent; +import com.cssbham.cssminecraft.common.event.events.ServerMessageEvent; +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import com.cssbham.cssminecraft.common.handler.DiscordMessageEventHandler; +import com.cssbham.cssminecraft.common.handler.PlayerJoinEventHandler; +import com.cssbham.cssminecraft.common.handler.PlayerQuitEventHandler; +import com.cssbham.cssminecraft.common.handler.ServerMessageEventHandler; +import com.cssbham.cssminecraft.common.logger.Logger; +import com.cssbham.cssminecraft.common.permission.PermissionPluginService; +import com.cssbham.cssminecraft.common.permission.PermissionPluginServiceFactory; + +import java.nio.file.Path; + +/** + * Abstract implementation of the CSS Minecraft plugin, to be extended by + * platforms. + */ +public abstract class AbstractCSSMinecraftPlugin implements CSSMinecraftPlugin { + + private ConfigService configService; + private DiscordClientService discordClientService; + private EventBus eventBus; + + @Override + public void enable() { + ConfigSource configSource = new YamlConfigSource(provideConfigurationPath()); + + Logger logger = getLogger(); + + this.configService = new ConfigService(logger); + configService.useSource(configSource); + + logger.setServerLoggingLevel(Logger.LoggingLevel.fromNumber(configService.getValue(ConfigOption.LOGGING_LEVEL))); + + this.eventBus = new SimpleEventBus(logger); + + this.discordClientService = new DiscordClientService(configService, eventBus, logger); + try { + discordClientService.initialiseClients(); + } catch (Exception e) { + logger.severe(String.format("Failed to initialise Discord clients: %s", e.getMessage())); + throw e; + } + + eventBus.subscribe(ServerMessageEvent.class, new ServerMessageEventHandler(discordClientService)); + eventBus.subscribe(PlayerJoinEvent.class, new PlayerJoinEventHandler(discordClientService)); + eventBus.subscribe(PlayerQuitEvent.class, new PlayerQuitEventHandler(discordClientService)); + eventBus.subscribe(DiscordMessageEvent.class, new DiscordMessageEventHandler(provideServerChatAdapter())); + + PermissionPluginService permissionPluginService = PermissionPluginServiceFactory.any(); + CommandService commandService = provideCommandService(); + + commandService.register("makegreen", new MakeGreenCommandHandler(discordClientService, permissionPluginService), "mg", "green"); + } + + @Override + public void disable() { + if (null != discordClientService) { + discordClientService.shutdownClients(); + } + + provideServerExecutor().shutdown(); + } + + public ConfigService getConfigService() { + return configService; + } + + public DiscordClientService getDiscordClientService() { + return discordClientService; + } + + public EventBus getEventBus() { + return eventBus; + } + + /** + * Provide platform-specific {@link ServerChatAdapter} + * + * @return a server chat adapter wrapping platform chat functions + */ + public abstract ServerChatAdapter provideServerChatAdapter(); + + /** + * Provide configuration path + * + * @return path to config file + */ + public abstract Path provideConfigurationPath(); + + /** + * Provide platform-specific {@link ServerExecutor} + * + * @return a server executor wrapping the platform main thread + */ + public abstract ServerExecutor provideServerExecutor(); + + /** + * Provide platform-specific {@link CommandService} + * + * @return a command service + */ + public abstract CommandService provideCommandService(); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/CSSMinecraftPlugin.java b/common/src/main/java/com/cssbham/cssminecraft/common/CSSMinecraftPlugin.java new file mode 100644 index 0000000..d1cc2aa --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/CSSMinecraftPlugin.java @@ -0,0 +1,26 @@ +package com.cssbham.cssminecraft.common; + +import com.cssbham.cssminecraft.common.logger.Logger; + +/** + * Base interface for the CSS Minecraft plugin. + */ +public interface CSSMinecraftPlugin { + + /** + * Enable the plugin. Should be called during the enable + * phase of the plugin lifecycle. + */ + void enable(); + + /** + * Disable the plugin. Should be called during the disable + * phase of the plugin lifecycle, or during server shutdown. + */ + void disable(); + + /** + * Get the plugin logger + */ + Logger getLogger(); +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/adapter/ServerChatAdapter.java b/common/src/main/java/com/cssbham/cssminecraft/common/adapter/ServerChatAdapter.java new file mode 100644 index 0000000..8b5c590 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/adapter/ServerChatAdapter.java @@ -0,0 +1,35 @@ +package com.cssbham.cssminecraft.common.adapter; + +import net.kyori.adventure.text.Component; + +import java.util.UUID; + +/** + * Abstraction for platform-specific server chat functions. + */ +public interface ServerChatAdapter { + + /** + * Broadcast a message to every player online and the server + * console. + * + * @param message the message to broadcast + */ + void broadcastMessage(Component message); + + /** + * Send a message to a specific player. + * + * @param user the user to send to + * @param component the message to send + */ + void sendMessageToPlayer(UUID user, Component component); + + /** + * Send a message to console. + * + * @param component the message to send + */ + void sendMessageToConsole(Component component); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/command/AbstractCommandService.java b/common/src/main/java/com/cssbham/cssminecraft/common/command/AbstractCommandService.java new file mode 100644 index 0000000..c5b8211 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/command/AbstractCommandService.java @@ -0,0 +1,62 @@ +package com.cssbham.cssminecraft.common.command; + +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import com.cssbham.cssminecraft.common.logger.Logger; + +import java.util.HashMap; +import java.util.Map; + +/** + * Abstract implementation of command service. Platform implementations should + * bind their platform-specific command framework to this. + */ +public abstract class AbstractCommandService implements CommandService { + + private final Map commands; + private final Logger logger; + private final ServerExecutor executor; + + public AbstractCommandService(Logger logger, ServerExecutor executor) { + this.commands = new HashMap<>(); + this.logger = logger; + this.executor = executor; + } + + @Override + public final void register(String label, CommandHandler handler, String... aliases) { + commands.put(label, handler); + for (String alias : aliases) { + commands.put(alias, handler) ; + } + } + + @Override + public final void execute(CommandSender sender, CommandContext context) { + CommandHandler handler = commands.get(context.label()); + + logger.debug(String.format("Handler for /%s executed by %s (%s): %s", + context.label(), + sender.getName(), + sender.getUuid(), + (null == handler) ? null : handler.getClass().getName() + )); + + if (null == handler) { + return; + } + + executor.doAsync(() -> { + try { + handler.handle(sender, context); + } catch (Exception e) { + logger.severe(String.format("Exception handling command /%s for %s: %s", + context.label(), + sender.getName(), + e.getMessage() + )); + e.printStackTrace(); + } + }); + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandContext.java b/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandContext.java new file mode 100644 index 0000000..c34e612 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandContext.java @@ -0,0 +1,4 @@ +package com.cssbham.cssminecraft.common.command; + +public record CommandContext (String label, String[] args) { +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandHandler.java b/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandHandler.java new file mode 100644 index 0000000..a94e5a1 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandHandler.java @@ -0,0 +1,7 @@ +package com.cssbham.cssminecraft.common.command; + +public interface CommandHandler { + + void handle(CommandSender sender, CommandContext context); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandSender.java b/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandSender.java new file mode 100644 index 0000000..9a54d72 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandSender.java @@ -0,0 +1,44 @@ +package com.cssbham.cssminecraft.common.command; + +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import net.kyori.adventure.text.Component; + +import java.util.UUID; + +/** + * A wrapper for command senders. + */ +public class CommandSender { + + private final UUID uuid; + private final String name; + private final boolean console; + private final ServerChatAdapter chatAdapter; + + public CommandSender(ServerChatAdapter chatAdapter, UUID uuid, String name, boolean console) { + this.uuid = uuid; + this.name = name; + this.console = console; + this.chatAdapter = chatAdapter; + } + + public UUID getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public boolean isConsole() { + return console; + } + + public void sendMessage(Component message) { + if (console) { + chatAdapter.sendMessageToConsole(message); + } else { + chatAdapter.sendMessageToPlayer(uuid, message); + } + } +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandService.java b/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandService.java new file mode 100644 index 0000000..3f71f12 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/command/CommandService.java @@ -0,0 +1,26 @@ +package com.cssbham.cssminecraft.common.command; + +/** + * Abstraction for command service, which manages command registration and + * execution. + */ +public interface CommandService { + + /** + * Register a command for execution with the service. + * + * @param label the command label + * @param handler the command executor + * @param aliases command aliases + */ + void register(String label, CommandHandler handler, String... aliases); + + /** + * Execute a command with a given command context. + * + * @param sender command sender + * @param context command context + */ + void execute(CommandSender sender, CommandContext context); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/command/handler/MakeGreenCommandHandler.java b/common/src/main/java/com/cssbham/cssminecraft/common/command/handler/MakeGreenCommandHandler.java new file mode 100644 index 0000000..b9ba9dd --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/command/handler/MakeGreenCommandHandler.java @@ -0,0 +1,67 @@ +package com.cssbham.cssminecraft.common.command.handler; + +import com.cssbham.cssminecraft.common.command.CommandContext; +import com.cssbham.cssminecraft.common.command.CommandHandler; +import com.cssbham.cssminecraft.common.command.CommandSender; +import com.cssbham.cssminecraft.common.discord.DiscordClientService; +import com.cssbham.cssminecraft.common.permission.PermissionPluginService; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + +import java.util.concurrent.ExecutionException; + +public class MakeGreenCommandHandler implements CommandHandler { + + private static final String DISCORD_USERNAME_PATTERN = "[a-z0-9._]{2,32}"; + + private final DiscordClientService discordClientService; + private final PermissionPluginService permissionPluginService; + + public MakeGreenCommandHandler(DiscordClientService discordClientService, PermissionPluginService permissionPluginService) { + this.discordClientService = discordClientService; + this.permissionPluginService = permissionPluginService; + } + + @Override + public void handle(CommandSender sender, CommandContext context) { + if (!permissionPluginService.isAvailable()) { + sender.sendMessage(Component.text("There is no permissions plugin available.").color(NamedTextColor.RED)); + return; + } + + if (sender.isConsole()) { + sender.sendMessage(Component.text("Only players may use this command.").color(NamedTextColor.RED)); + return; + } + + String arg = String.join(" ", context.args()); + if (!arg.matches(DISCORD_USERNAME_PATTERN)) { + sender.sendMessage(Component.text("Invalid Discord tag format.").color(NamedTextColor.RED)); + return; + } + + if (discordClientService.getDiscordClient().isMember(arg)) { + sender.sendMessage(Component.text("Making you green...").color(NamedTextColor.GRAY)); + try { + permissionPluginService.grantMemberRole(sender.getUuid()).get(); + } catch (InterruptedException | ExecutionException e) { + sender.sendMessage(Component.text("There was a problem making you green. Try again later.") + .color(NamedTextColor.RED)); + throw new RuntimeException(e); + } + sender.sendMessage(Component.text("Congratulations, you are now green!").color(NamedTextColor.GREEN)); + } else { + sender.sendMessage(Component.text("You don't appear to be a ").color(NamedTextColor.RED).append( + Component.text("Member").color(NamedTextColor.GREEN) + ).append(Component.text(" on Discord! If you are, please link your account first and try again. " + + "Otherwise, you can get membership at ").color(NamedTextColor.RED) + ).append(Component.text("https://cssbham.com/join") + .clickEvent(ClickEvent.openUrl("https://cssbham.com/join")) + .color(NamedTextColor.AQUA) + .decorate(TextDecoration.UNDERLINED))); + } + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/config/ConfigService.java b/common/src/main/java/com/cssbham/cssminecraft/common/config/ConfigService.java new file mode 100644 index 0000000..7034012 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/config/ConfigService.java @@ -0,0 +1,48 @@ +package com.cssbham.cssminecraft.common.config; + +import com.cssbham.cssminecraft.common.config.option.ConfigValue; +import com.cssbham.cssminecraft.common.config.source.ConfigSource; +import com.cssbham.cssminecraft.common.logger.Logger; + +import java.util.Objects; + +/** + * Configuration service to create plugin config, and retrieve config + * values. + */ +public class ConfigService { + + private final Logger logger; + + private ConfigSource configSource; + + public ConfigService(Logger logger) { + this.logger = logger; + } + + /** + * Initialise a config source and associate it with this service. + * + * @param configSource the config source + */ + public void useSource(ConfigSource configSource) { + this.configSource = configSource; + this.logger.info(String.format("Using config source: %s", configSource.getClass().getName())); + + configSource.initialise(); + } + + /** + * Get the value of a {@link ConfigValue}. + * + * @param option the option to retrieve + * @return the value + * @param the type of config value + */ + public T getValue(ConfigValue option) { + Objects.requireNonNull(configSource, "config source not initialised"); + + return option.get(configSource); + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/config/option/ConfigOption.java b/common/src/main/java/com/cssbham/cssminecraft/common/config/option/ConfigOption.java new file mode 100644 index 0000000..556be72 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/config/option/ConfigOption.java @@ -0,0 +1,42 @@ +package com.cssbham.cssminecraft.common.config.option; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static com.cssbham.cssminecraft.common.config.option.ConfigValueFactory.*; + +/** + * A list of all config options. + */ +public class ConfigOption { + + public static final ConfigValue WEBHOOK_URL = buildString("webhook-url", ""); + + public static final ConfigValue AVATAR_SERVICE = buildString("avatar-service", ""); + + public static final ConfigValue BOT_TOKEN = buildString("bot-token", ""); + + public static final ConfigValue MEMBER_ROLE_ID = buildLong("member-role-id", 0); + + public static final ConfigValue BRIDGE_CHANNEL_ID = buildLong("bridge-channel-id", 0); + + public static final ConfigValue DISCORD_SERVER_ID = buildLong("discord-server-id", 0); + + public static final ConfigValue LOGGING_LEVEL = buildInt("logging-level", 2); + + public static List> allValues() { + return Arrays.stream(ConfigOption.class.getFields()) + .filter(f -> Modifier.isStatic(f.getModifiers())) + .filter(f -> ConfigValue.class.equals(f.getType())) + .map(f -> { + try { + return (ConfigValue) f.get(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/config/option/ConfigValue.java b/common/src/main/java/com/cssbham/cssminecraft/common/config/option/ConfigValue.java new file mode 100644 index 0000000..4a69429 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/config/option/ConfigValue.java @@ -0,0 +1,51 @@ +package com.cssbham.cssminecraft.common.config.option; + +import com.cssbham.cssminecraft.common.config.source.ConfigSource; + +import java.util.function.Function; + +/** + * Abstraction for config values. + * + * @param the type + */ +public class ConfigValue { + + private final String path; + private final T def; + private final Function getter; + + public ConfigValue(String path, T def, Function getter) { + this.path = path; + this.def = def; + this.getter = getter; + } + + /** + * Get this value from the config source. + * + * @param configSource the config source + * @return the value + */ + public T get(ConfigSource configSource) { + return this.getter.apply(configSource); + } + + /** + * Get the path for this value. + * + * @return path + */ + public String getPath() { + return path; + } + + /** + * Get the default value for this value. + * + * @return the default value + */ + public T getDefault() { + return def; + } +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/config/option/ConfigValueFactory.java b/common/src/main/java/com/cssbham/cssminecraft/common/config/option/ConfigValueFactory.java new file mode 100644 index 0000000..bea3b86 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/config/option/ConfigValueFactory.java @@ -0,0 +1,22 @@ +package com.cssbham.cssminecraft.common.config.option; + +/** + * Factory methods for creating config values. + */ +public final class ConfigValueFactory { + + private ConfigValueFactory() {} + + public static ConfigValue buildString(String path, String def) { + return new ConfigValue<>(path, def, (configSource -> configSource.getString(path, def))); + } + + public static ConfigValue buildLong(String path, long def) { + return new ConfigValue<>(path, def, (configSource -> configSource.getLong(path, def))); + } + + public static ConfigValue buildInt(String path, int def) { + return new ConfigValue<>(path, def, (configSource -> configSource.getInteger(path, def))); + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/config/source/ConfigSource.java b/common/src/main/java/com/cssbham/cssminecraft/common/config/source/ConfigSource.java new file mode 100644 index 0000000..4cfd40e --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/config/source/ConfigSource.java @@ -0,0 +1,22 @@ +package com.cssbham.cssminecraft.common.config.source; + +/** + * Abstraction for configuration sources. + */ +public interface ConfigSource { + + int getInteger(String path, int def); + + long getLong(String path, long def); + + boolean getBoolean(String path, boolean def); + + String getString(String path, String def); + + /** + * Initialise this configuration source and create default + * configuration. + */ + void initialise(); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/config/source/StubConfigSource.java b/common/src/main/java/com/cssbham/cssminecraft/common/config/source/StubConfigSource.java new file mode 100644 index 0000000..ffe4a87 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/config/source/StubConfigSource.java @@ -0,0 +1,30 @@ +package com.cssbham.cssminecraft.common.config.source; + +public class StubConfigSource implements ConfigSource { + + @Override + public int getInteger(String path, int def) { + return 0; + } + + @Override + public long getLong(String path, long def) { + return 0; + } + + @Override + public boolean getBoolean(String path, boolean def) { + return false; + } + + @Override + public String getString(String path, String def) { + return "test"; + } + + @Override + public void initialise() { + + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/config/source/YamlConfigSource.java b/common/src/main/java/com/cssbham/cssminecraft/common/config/source/YamlConfigSource.java new file mode 100644 index 0000000..18042fe --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/config/source/YamlConfigSource.java @@ -0,0 +1,100 @@ +package com.cssbham.cssminecraft.common.config.source; + +import com.cssbham.cssminecraft.common.config.option.ConfigOption; +import com.cssbham.cssminecraft.common.config.option.ConfigValue; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.DumperOptions; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; +import java.util.Map; + +/** + * Config source implementation for SnakeYAML. + */ +public class YamlConfigSource implements ConfigSource { + + private final Path configurationPath; + + private Map data; + + public YamlConfigSource(Path configurationPath) { + this.configurationPath = configurationPath; + } + + @Override + public void initialise() { + try { + this.createDefaultIfNotExists(); + + try (InputStreamReader reader = new InputStreamReader(Files.newInputStream(configurationPath))) { + Yaml yaml = new Yaml(); + this.data = yaml.load(reader); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void createDefaultIfNotExists() throws IOException { + if (Files.exists(configurationPath)) { + return; + } + + Files.createDirectories(configurationPath.getParent()); + + Map configuration = new HashMap<>(); + for (ConfigValue configValue : ConfigOption.allValues()) { + configuration.put(configValue.getPath(), configValue.getDefault()); + } + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + Yaml yaml = new Yaml(options); + + try (OutputStreamWriter writer = new OutputStreamWriter( + Files.newOutputStream(configurationPath, StandardOpenOption.CREATE) + )) { + yaml.dump(configuration, writer); + } + } + + @Override + public int getInteger(String path, int def) { + if (null == data) return def; + Object object = data.getOrDefault(path, def); + if (!(object instanceof Number value)) return def; + + return value.intValue(); + } + + @Override + public long getLong(String path, long def) { + if (null == data) return def; + Object object = data.getOrDefault(path, def); + if (!(object instanceof Number value)) return def; + + return value.longValue(); + } + + @Override + public boolean getBoolean(String path, boolean def) { + if (null == data) return def; + Object object = data.getOrDefault(path, def); + if (!(object instanceof Boolean value)) return def; + + return value; + } + + @Override + public String getString(String path, String def) { + if (null == data) return def; + + return String.valueOf(data.getOrDefault(path, def)); + } + + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/discord/DiscordClientService.java b/common/src/main/java/com/cssbham/cssminecraft/common/discord/DiscordClientService.java new file mode 100644 index 0000000..134f588 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/discord/DiscordClientService.java @@ -0,0 +1,76 @@ +package com.cssbham.cssminecraft.common.discord; + +import com.cssbham.cssminecraft.common.config.ConfigService; +import com.cssbham.cssminecraft.common.config.option.ConfigOption; +import com.cssbham.cssminecraft.common.discord.client.DiscordClient; +import com.cssbham.cssminecraft.common.discord.client.JDADiscordClient; +import com.cssbham.cssminecraft.common.discord.webhook.DiscordWebHookClient; +import com.cssbham.cssminecraft.common.discord.webhook.WebHookClient; +import com.cssbham.cssminecraft.common.event.EventBus; +import com.cssbham.cssminecraft.common.logger.Logger; + +import java.util.Objects; + +/** + * A service to manage Discord clients. + */ +public class DiscordClientService { + + private final ConfigService configService; + private final EventBus eventBus; + private final Logger logger; + + private DiscordClient discordClient; + private WebHookClient webHookClient; + + public DiscordClientService(ConfigService configService, EventBus eventBus, Logger logger) { + this.configService = configService; + this.eventBus = eventBus; + this.logger = logger; + } + + /** + * Initialise Discord clients. Will throw if underlying clients throw. + */ + public void initialiseClients() { + this.discordClient = new JDADiscordClient( + eventBus, + logger, + configService.getValue(ConfigOption.BOT_TOKEN), + configService.getValue(ConfigOption.DISCORD_SERVER_ID), + configService.getValue(ConfigOption.MEMBER_ROLE_ID), + configService.getValue(ConfigOption.BRIDGE_CHANNEL_ID) + ); + this.webHookClient = new DiscordWebHookClient( + configService.getValue(ConfigOption.WEBHOOK_URL), + configService.getValue(ConfigOption.AVATAR_SERVICE) + ); + + this.logger.info("Initialising Discord clients"); + + this.discordClient.initialise(); + this.webHookClient.initialise(); + } + + /** + * Shutdown Discord clients. + */ + public void shutdownClients() { + this.logger.info("Shutting down Discord clients"); + + this.discordClient.shutdown(); + this.webHookClient.shutdown(); + } + + public DiscordClient getDiscordClient() { + Objects.requireNonNull(discordClient, "discord client not initialised"); + + return discordClient; + } + + public WebHookClient getWebHookClient() { + Objects.requireNonNull(webHookClient, "webhook client not initialised"); + + return webHookClient; + } +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/discord/client/DiscordClient.java b/common/src/main/java/com/cssbham/cssminecraft/common/discord/client/DiscordClient.java new file mode 100644 index 0000000..a504790 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/discord/client/DiscordClient.java @@ -0,0 +1,20 @@ +package com.cssbham.cssminecraft.common.discord.client; + +/** + * Abstraction for Discord clients. + */ +public interface DiscordClient { + + void initialise(); + + void shutdown(); + + /** + * Get whether a discord tag has the Member role + * + * @param identifier discord tag + * @return true if they have the role, false otherwise + */ + boolean isMember(String identifier); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/discord/client/JDADiscordClient.java b/common/src/main/java/com/cssbham/cssminecraft/common/discord/client/JDADiscordClient.java new file mode 100644 index 0000000..49814b7 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/discord/client/JDADiscordClient.java @@ -0,0 +1,101 @@ +package com.cssbham.cssminecraft.common.discord.client; + +import com.cssbham.cssminecraft.common.event.EventBus; +import com.cssbham.cssminecraft.common.event.events.DiscordMessageEvent; +import com.cssbham.cssminecraft.common.logger.Logger; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.ChunkingFilter; +import net.dv8tion.jda.api.utils.MemberCachePolicy; +import org.jetbrains.annotations.NotNull; + +public class JDADiscordClient extends ListenerAdapter implements DiscordClient { + + private final EventBus eventBus; + private final Logger logger; + private final String botToken; + private final long discordServerId; + private final long memberRoleId; + private final long bridgeChannelId; + + private JDA jda; + + public JDADiscordClient(EventBus eventBus, Logger logger, String botToken, long discordServerId, long memberRoleId, long bridgeChannelId) { + this.eventBus = eventBus; + this.logger = logger; + this.botToken = botToken; + this.discordServerId = discordServerId; + this.memberRoleId = memberRoleId; + this.bridgeChannelId = bridgeChannelId; + } + + @Override + public void initialise() { + if (null != this.jda) { + logger.debug("JDA already initialised, skipping initialisation request"); + return; + } + + logger.debug("Initialising JDA"); + + this.jda = JDABuilder.createDefault( + botToken + ).setMemberCachePolicy(MemberCachePolicy.ALL) + .setChunkingFilter(ChunkingFilter.ALL) + .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT) + .addEventListeners(this) + .build(); + } + + + @Override + public void shutdown() { + if (null == this.jda) { + logger.debug("JDA already stopped, skipping stop request"); + return; + } + + logger.debug("Shutting down JDA"); + + jda.shutdownNow(); + this.jda = null; + } + + @Override + public void onMessageReceived(@NotNull MessageReceivedEvent event) { + logger.debug(String.format("Message received from %s: %s", event.getAuthor().getName(), event.getMessage().getContentRaw())); + if (!event.isFromGuild() || event.getChannel().getIdLong() != bridgeChannelId || + event.isWebhookMessage() || + event.getMember() == null || + event.getAuthor().isBot() || + event.getMessage().isEdited()) { + return; + } + + eventBus.dispatch(new DiscordMessageEvent( + event.getMember().getEffectiveName(), + event.getMessage().getContentRaw(), + event.getMember().getColorRaw() + )); + } + + @Override + public boolean isMember(String identifier) { + Guild guild = jda.getGuildById(discordServerId); + if (null == guild) return false; + + Member member = guild.getMembers().stream() + .filter(m -> m.getUser().getName().equalsIgnoreCase(identifier)) + .findFirst() + .orElse(null); + if (null == member) return false; + + return member.getRoles().stream().anyMatch(r -> r.getIdLong() == memberRoleId); + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/discord/webhook/DiscordWebHookClient.java b/common/src/main/java/com/cssbham/cssminecraft/common/discord/webhook/DiscordWebHookClient.java new file mode 100644 index 0000000..88f65df --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/discord/webhook/DiscordWebHookClient.java @@ -0,0 +1,65 @@ +package com.cssbham.cssminecraft.common.discord.webhook; + +import club.minnced.discord.webhook.send.WebhookMessageBuilder; +import com.cssbham.cssminecraft.common.event.EventHandler; + +import club.minnced.discord.webhook.WebhookClient; +import club.minnced.discord.webhook.WebhookClientBuilder; +import okhttp3.OkHttpClient; + +import java.util.Objects; + +public class DiscordWebHookClient implements WebHookClient { + + private final String webHookUrl; + private final String avatarServiceUrl; + + private WebhookClient webhook = null; + + public DiscordWebHookClient(String webHookUrl, String avatarServiceUrl) { + this.webHookUrl = webHookUrl; + this.avatarServiceUrl = avatarServiceUrl; + } + + @Override + public void initialise() { + if (null != this.webhook) { + return; + } + + this.webhook = new WebhookClientBuilder(webHookUrl) + .setThreadFactory(Thread::new) + .setDaemon(true) + .setWait(true) + .setHttpClient(new OkHttpClient()) + .build(); + } + + @Override + public void shutdown() { + if (null == this.webhook) { + return; + } + + if (this.webhook.isShutdown()) { + this.webhook = null; + return; + } + + this.webhook.close(); + this.webhook = null; + } + + @Override + public void sendMessageAsMinecraftUser(String avatarName, String displayName, String message) { + try { + webhook.send(new WebhookMessageBuilder() + .setAvatarUrl(String.format(avatarServiceUrl, avatarName)) + .setUsername(displayName) + .setContent(message) + .build()); + } catch (Exception ignored) { + } + // https://github.com/DV8FromTheWorld/JDA/issues/1761 + } +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/discord/webhook/WebHookClient.java b/common/src/main/java/com/cssbham/cssminecraft/common/discord/webhook/WebHookClient.java new file mode 100644 index 0000000..de2dffc --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/discord/webhook/WebHookClient.java @@ -0,0 +1,21 @@ +package com.cssbham.cssminecraft.common.discord.webhook; + +/** + * Abstraction for webhook clients. + */ +public interface WebHookClient { + + void initialise(); + + void shutdown(); + + /** + * Send a message to the endpoint as a Minecraft player. + * + * @param avatarName the name of the Minecraft avatar + * @param displayName senders display name + * @param message message content + */ + void sendMessageAsMinecraftUser(String avatarName, String displayName, String message); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/event/Event.java b/common/src/main/java/com/cssbham/cssminecraft/common/event/Event.java new file mode 100644 index 0000000..4b81492 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/event/Event.java @@ -0,0 +1,8 @@ +package com.cssbham.cssminecraft.common.event; + +/** + * An event. + */ +public interface Event { + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/event/EventBus.java b/common/src/main/java/com/cssbham/cssminecraft/common/event/EventBus.java new file mode 100644 index 0000000..aa4f83e --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/event/EventBus.java @@ -0,0 +1,23 @@ +package com.cssbham.cssminecraft.common.event; + +/** + * Base interface for an event bus. + */ +public interface EventBus { + + /** + * Dispatch an event. + * + * @param event the event to dispatch + */ + void dispatch(Event event); + + /** + * Subscribe to an event. + * + * @param event the event to subscribe to + * @param handler the event handler + */ + void subscribe(Class event, EventHandler handler); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/event/EventHandler.java b/common/src/main/java/com/cssbham/cssminecraft/common/event/EventHandler.java new file mode 100644 index 0000000..ae43007 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/event/EventHandler.java @@ -0,0 +1,7 @@ +package com.cssbham.cssminecraft.common.event; + +public abstract class EventHandler { + + public abstract void handle(E event); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/event/PlatformEventAdapter.java b/common/src/main/java/com/cssbham/cssminecraft/common/event/PlatformEventAdapter.java new file mode 100644 index 0000000..db14002 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/event/PlatformEventAdapter.java @@ -0,0 +1,11 @@ +package com.cssbham.cssminecraft.common.event; + +/** + * Interface for platforms to map platform-specific events to the + * common {@link EventBus} + */ +public interface PlatformEventAdapter { + + void bindPlatformToEventBus(EventBus eventBus); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/event/SimpleEventBus.java b/common/src/main/java/com/cssbham/cssminecraft/common/event/SimpleEventBus.java new file mode 100644 index 0000000..065c0d3 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/event/SimpleEventBus.java @@ -0,0 +1,39 @@ +package com.cssbham.cssminecraft.common.event; + +import com.cssbham.cssminecraft.common.logger.Logger; + +import java.util.*; + +/** + * A simple event bus implementation. + */ +public class SimpleEventBus implements EventBus { + + private final Map, List>> handlers = new HashMap<>(); + private final Logger logger; + + public SimpleEventBus(Logger logger) { + this.logger = logger; + } + + public void dispatch(Event event) { + var handlers = this.handlers.getOrDefault(event.getClass(), new ArrayList<>()); + logger.debug(String.format("Event dispatch: %s", event.getClass().getName())); + + for (EventHandler handler : handlers) { + try { + handler.handle(event); + } catch (Exception e) { + logger.severe(String.format("Error in handler %s", handler.getClass().getName())); + } + } + } + + public void subscribe(Class event, EventHandler handler) { + if (!this.handlers.containsKey(event)) { + this.handlers.put(event, new ArrayList<>()); + } + + this.handlers.get(event).add(handler); + } +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/event/events/DiscordMessageEvent.java b/common/src/main/java/com/cssbham/cssminecraft/common/event/events/DiscordMessageEvent.java new file mode 100644 index 0000000..cc04102 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/event/events/DiscordMessageEvent.java @@ -0,0 +1,15 @@ +package com.cssbham.cssminecraft.common.event.events; + +import com.cssbham.cssminecraft.common.event.Event; + +/** + * An event which should be dispatched when a discord message is received. + * This event SHOULD be dispatched async. + * + * @param sender name of sender + * @param message message content + * @param senderColour the senders role colour on discord + */ +public record DiscordMessageEvent(String sender, String message, int senderColour) implements Event { + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/event/events/PlayerJoinEvent.java b/common/src/main/java/com/cssbham/cssminecraft/common/event/events/PlayerJoinEvent.java new file mode 100644 index 0000000..9ceb9dd --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/event/events/PlayerJoinEvent.java @@ -0,0 +1,17 @@ +package com.cssbham.cssminecraft.common.event.events; + +import com.cssbham.cssminecraft.common.event.Event; + +import java.util.UUID; + +/** + * An event which should be dispatched when a player joins. + * + * @param sender UUID of joining player + * @param username username of joining player + * @param displayName display name of joining player + * @param newPlayerCount new player count + */ +public record PlayerJoinEvent(UUID sender, String username, String displayName, int newPlayerCount) implements Event { + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/event/events/PlayerQuitEvent.java b/common/src/main/java/com/cssbham/cssminecraft/common/event/events/PlayerQuitEvent.java new file mode 100644 index 0000000..6ca0918 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/event/events/PlayerQuitEvent.java @@ -0,0 +1,17 @@ +package com.cssbham.cssminecraft.common.event.events; + +import com.cssbham.cssminecraft.common.event.Event; + +import java.util.UUID; + +/** + * An event which should be dispatched when a player quits. + * + * @param sender UUID of leaving player + * @param username username of leaving player + * @param displayName display name of leaving player + * @param newPlayerCount new player count + */ +public record PlayerQuitEvent(UUID sender, String username, String displayName, int newPlayerCount) implements Event { + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/event/events/ServerMessageEvent.java b/common/src/main/java/com/cssbham/cssminecraft/common/event/events/ServerMessageEvent.java new file mode 100644 index 0000000..4634a39 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/event/events/ServerMessageEvent.java @@ -0,0 +1,17 @@ +package com.cssbham.cssminecraft.common.event.events; + +import com.cssbham.cssminecraft.common.event.Event; + +import java.util.UUID; + +/** + * An event which should be dispatched when a chat message is sent. + * + * @param sender UUID of sender + * @param username username of sender + * @param displayName display name of sender + * @param message message content + */ +public record ServerMessageEvent(UUID sender, String username, String displayName, String message) implements Event { + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/executor/AsyncServerExecutor.java b/common/src/main/java/com/cssbham/cssminecraft/common/executor/AsyncServerExecutor.java new file mode 100644 index 0000000..c4d1960 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/executor/AsyncServerExecutor.java @@ -0,0 +1,42 @@ +package com.cssbham.cssminecraft.common.executor; + +import com.cssbham.cssminecraft.common.logger.Logger; + +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Abstract implementation of server executor with a thread pool for + * async tasks. Subclasses should implement the sync method around the + * server scheduler. + */ +public abstract class AsyncServerExecutor implements ServerExecutor { + + private final ThreadPoolExecutor executor; + private final Logger logger; + + public AsyncServerExecutor(Logger logger) { + this.executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(); + this.logger = logger; + } + + @Override + public void doAsync(Runnable runnable) { + executor.submit(runnable); + } + + @Override + public void shutdown() { + this.executor.shutdown(); + try { + if (!this.executor.awaitTermination(30, TimeUnit.SECONDS)) { + logger.severe("Async executor timed out while awaiting shutdown!"); + } + } catch (InterruptedException e) { + logger.warning("Interrupted while awaiting async executor termination"); + e.printStackTrace(); + } + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/executor/ServerExecutor.java b/common/src/main/java/com/cssbham/cssminecraft/common/executor/ServerExecutor.java new file mode 100644 index 0000000..d780add --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/executor/ServerExecutor.java @@ -0,0 +1,27 @@ +package com.cssbham.cssminecraft.common.executor; + +/** + * An executor which can interface with the server main thread. + */ +public interface ServerExecutor { + + /** + * Run synchronously on main thread + * + * @param runnable task to run + */ + void doSync(Runnable runnable); + + /** + * Run asynchronously outside the main thread + * + * @param runnable task tp run + */ + void doAsync(Runnable runnable); + + /** + * Shut down thread pool for asynchronous tasks + */ + void shutdown(); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/handler/AbstractPlayerJoinLeaveEventHandler.java b/common/src/main/java/com/cssbham/cssminecraft/common/handler/AbstractPlayerJoinLeaveEventHandler.java new file mode 100644 index 0000000..2408f21 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/handler/AbstractPlayerJoinLeaveEventHandler.java @@ -0,0 +1,16 @@ +package com.cssbham.cssminecraft.common.handler; + +import com.cssbham.cssminecraft.common.event.Event; +import com.cssbham.cssminecraft.common.event.EventHandler; + +public abstract class AbstractPlayerJoinLeaveEventHandler extends EventHandler { + + protected String getPlayerCountMessage(int count) { + return switch (count) { + case 0 -> "there are now no players online"; + case 1 -> "there is now 1 player online"; + default -> "there are now " + count + " players online"; + }; + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/handler/DiscordMessageEventHandler.java b/common/src/main/java/com/cssbham/cssminecraft/common/handler/DiscordMessageEventHandler.java new file mode 100644 index 0000000..c703ef0 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/handler/DiscordMessageEventHandler.java @@ -0,0 +1,29 @@ +package com.cssbham.cssminecraft.common.handler; + +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import com.cssbham.cssminecraft.common.event.EventHandler; +import com.cssbham.cssminecraft.common.event.events.DiscordMessageEvent; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; + +public class DiscordMessageEventHandler extends EventHandler { + + private final ServerChatAdapter serverChatAdapter; + + public DiscordMessageEventHandler(ServerChatAdapter serverChatAdapter) { + this.serverChatAdapter = serverChatAdapter; + } + + @Override + public void handle(DiscordMessageEvent event) { + serverChatAdapter.broadcastMessage(buildMessageComponent(event.sender(), event.senderColour(), event.message())); + } + + private Component buildMessageComponent(String name, int colour, String message) { + return Component.text("[Discord] ").color(TextColor.color(115, 138, 189)) + .append(Component.text(name).color(TextColor.color(0xFFFFFF & colour))) + .append(Component.text(" > ").color(NamedTextColor.WHITE)) + .append(Component.text(message).color(NamedTextColor.WHITE)); + } +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/handler/PlayerJoinEventHandler.java b/common/src/main/java/com/cssbham/cssminecraft/common/handler/PlayerJoinEventHandler.java new file mode 100644 index 0000000..7edb2fb --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/handler/PlayerJoinEventHandler.java @@ -0,0 +1,22 @@ +package com.cssbham.cssminecraft.common.handler; + +import com.cssbham.cssminecraft.common.discord.DiscordClientService; +import com.cssbham.cssminecraft.common.event.EventHandler; +import com.cssbham.cssminecraft.common.event.events.PlayerJoinEvent; +import com.cssbham.cssminecraft.common.event.events.ServerMessageEvent; + +public class PlayerJoinEventHandler extends AbstractPlayerJoinLeaveEventHandler { + + private final DiscordClientService discordClientService; + + public PlayerJoinEventHandler(DiscordClientService discordClientService) { + this.discordClientService = discordClientService; + } + + @Override + public void handle(PlayerJoinEvent event) { + String joinMessage = String.format("__*has joined the server, %s*__", getPlayerCountMessage(event.newPlayerCount())); + this.discordClientService.getWebHookClient().sendMessageAsMinecraftUser(event.username(), event.displayName(), joinMessage); + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/handler/PlayerQuitEventHandler.java b/common/src/main/java/com/cssbham/cssminecraft/common/handler/PlayerQuitEventHandler.java new file mode 100644 index 0000000..c7938d5 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/handler/PlayerQuitEventHandler.java @@ -0,0 +1,22 @@ +package com.cssbham.cssminecraft.common.handler; + +import com.cssbham.cssminecraft.common.discord.DiscordClientService; +import com.cssbham.cssminecraft.common.event.EventHandler; +import com.cssbham.cssminecraft.common.event.events.PlayerJoinEvent; +import com.cssbham.cssminecraft.common.event.events.PlayerQuitEvent; + +public class PlayerQuitEventHandler extends AbstractPlayerJoinLeaveEventHandler { + + private final DiscordClientService discordClientService; + + public PlayerQuitEventHandler(DiscordClientService discordClientService) { + this.discordClientService = discordClientService; + } + + @Override + public void handle(PlayerQuitEvent event) { + String joinMessage = String.format("__*has left the server, %s*__", getPlayerCountMessage(event.newPlayerCount())); + this.discordClientService.getWebHookClient().sendMessageAsMinecraftUser(event.username(), event.displayName(), joinMessage); + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/handler/ServerMessageEventHandler.java b/common/src/main/java/com/cssbham/cssminecraft/common/handler/ServerMessageEventHandler.java new file mode 100644 index 0000000..2b8c5b2 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/handler/ServerMessageEventHandler.java @@ -0,0 +1,23 @@ +package com.cssbham.cssminecraft.common.handler; + +import com.cssbham.cssminecraft.common.discord.DiscordClientService; +import com.cssbham.cssminecraft.common.event.Event; +import com.cssbham.cssminecraft.common.event.EventHandler; +import com.cssbham.cssminecraft.common.event.events.ServerMessageEvent; +import net.dv8tion.jda.api.utils.MarkdownSanitizer; + +public class ServerMessageEventHandler extends EventHandler { + + private final DiscordClientService discordClientService; + + public ServerMessageEventHandler(DiscordClientService discordClientService) { + this.discordClientService = discordClientService; + } + + @Override + public void handle(ServerMessageEvent event) { + String sanitisedMessage = MarkdownSanitizer.sanitize(event.message()).replace("@", "@\u200B"); + this.discordClientService.getWebHookClient().sendMessageAsMinecraftUser(event.username(), event.displayName(), sanitisedMessage); + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/logger/AbstractLogger.java b/common/src/main/java/com/cssbham/cssminecraft/common/logger/AbstractLogger.java new file mode 100644 index 0000000..9257dee --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/logger/AbstractLogger.java @@ -0,0 +1,60 @@ +package com.cssbham.cssminecraft.common.logger; + +/** + * Abstract implementation of logger, implementing the server logging + * level logic. Platform-specific implementations should wrap the server + * logger. + */ +public abstract class AbstractLogger implements Logger { + + private Logger.LoggingLevel serverLoggingLevel = LoggingLevel.INFO; + + @Override + public Logger.LoggingLevel getServerLoggingLevel() { + return serverLoggingLevel; + } + + @Override + public void setServerLoggingLevel(Logger.LoggingLevel serverLoggingLevel) { + this.serverLoggingLevel = serverLoggingLevel; + } + + @Override + public void log(String str, Logger.LoggingLevel level) { + if (serverLoggingLevel.getNumericVerbosity() < level.getNumericVerbosity()) { + return; + } + switch (level) { + case DEBUG -> logInfo("DEBUG: " + str); + case INFO -> logInfo(str); + case ERROR -> logError(str); + case WARNING -> logWarning(str); + } + } + + @Override + public void debug(String str) { + log(str, Logger.LoggingLevel.DEBUG); + } + + @Override + public void info(String str) { + log(str, Logger.LoggingLevel.INFO); + } + + @Override + public void warning(String str) { + log(str, Logger.LoggingLevel.WARNING); + } + + @Override + public void severe(String str) { + log(str, Logger.LoggingLevel.ERROR); + } + + protected abstract void logInfo(String string); + + protected abstract void logError(String string); + + protected abstract void logWarning(String string); +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/logger/Logger.java b/common/src/main/java/com/cssbham/cssminecraft/common/logger/Logger.java new file mode 100644 index 0000000..9034037 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/logger/Logger.java @@ -0,0 +1,88 @@ +package com.cssbham.cssminecraft.common.logger; + +/** + * Base interface for plugin logger. Implementations should + * wrap the server or plugin specific logger. + */ +public interface Logger { + + /** + * Get the logging level. + * + * @return logging level + */ + LoggingLevel getServerLoggingLevel(); + + /** + * Set the logging level. + * + * @param serverLoggingLevel the new logging level + */ + void setServerLoggingLevel(LoggingLevel serverLoggingLevel); + + /** + * Log a message. + * + * @param str the message to log + * @param level the severity of the message + */ + void log(String str, LoggingLevel level); + + /** + * Log a debug message. + * + * @param str the message to log + */ + void debug(String str); + + /** + * Log an informational message. + * + * @param str the message to log + */ + void info(String str); + + /** + * Log a warning message. + * + * @param str the message to log + */ + void warning(String str); + + /** + * Log an error message. + * + * @param str the message to log + */ + void severe(String str); + + /** + * Represents a numeric logging level, where lower values are more + * severe. + */ + enum LoggingLevel { + ERROR(0), + WARNING(1), + INFO(2), + DEBUG(3); + + private final int numericVerbosity; + + LoggingLevel(int number) { + numericVerbosity = number; + } + + public int getNumericVerbosity() { + return numericVerbosity; + } + + public static LoggingLevel fromNumber(int number) { + for (LoggingLevel level : LoggingLevel.values()) { + if (level.getNumericVerbosity() == number) { + return level; + } + } + return LoggingLevel.INFO; + } + } +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/permission/LuckPermsPermissionPluginService.java b/common/src/main/java/com/cssbham/cssminecraft/common/permission/LuckPermsPermissionPluginService.java new file mode 100644 index 0000000..d77e783 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/permission/LuckPermsPermissionPluginService.java @@ -0,0 +1,31 @@ +package com.cssbham.cssminecraft.common.permission; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.node.Node; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * {@link PermissionPluginService} implementation for LuckPerms + */ +public class LuckPermsPermissionPluginService implements PermissionPluginService { + + @Override + public CompletableFuture grantMemberRole(UUID player) { + LuckPerms perms = LuckPermsProvider.get(); + return perms.getUserManager().modifyUser(player, + user -> { + user.data().add(Node.builder("group.member").build()); + user.data().remove(Node.builder("group.guest").build()); + } + ); + } + + @Override + public boolean isAvailable() { + return true; + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/permission/PermissionPluginService.java b/common/src/main/java/com/cssbham/cssminecraft/common/permission/PermissionPluginService.java new file mode 100644 index 0000000..dcf874b --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/permission/PermissionPluginService.java @@ -0,0 +1,27 @@ +package com.cssbham.cssminecraft.common.permission; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * A permission plugin service. This service wraps the function calls + * of a permissions plugin. + */ +public interface PermissionPluginService { + + /** + * Grant the Member role to a player + * + * @param player the player uuid + * @return a future + */ + CompletableFuture grantMemberRole(UUID player); + + /** + * Get whether the service is usable + * + * @return true if it can be used, false otherwise + */ + boolean isAvailable(); + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/permission/PermissionPluginServiceFactory.java b/common/src/main/java/com/cssbham/cssminecraft/common/permission/PermissionPluginServiceFactory.java new file mode 100644 index 0000000..01e4cdd --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/permission/PermissionPluginServiceFactory.java @@ -0,0 +1,38 @@ +package com.cssbham.cssminecraft.common.permission; + +import java.lang.reflect.InvocationTargetException; + +/** + * Factory methods for getting a {@link PermissionPluginService} + */ +public class PermissionPluginServiceFactory { + + public static PermissionPluginService any() { + String[] plugins = new String[]{ "LuckPerms" }; + for (String plugin : plugins) { + try { + return forPlugin(plugin); + } catch (RuntimeException ignored) { } + } + return new StubPermissionPluginService(); + } + + public static PermissionPluginService forPlugin(String plugin) { + try { + return switch (plugin) { + case "LuckPerms" -> checkClassAndBuild("net.luckperms.api.LuckPermsProvider", LuckPermsPermissionPluginService.class); + default -> new StubPermissionPluginService(); + }; + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + throw new RuntimeException(String.format("Permission plugin %s is not available", plugin)); + } + } + + private static PermissionPluginService checkClassAndBuild(String className, Class clazz) + throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + Class.forName(className); + return clazz.getDeclaredConstructor().newInstance(); + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/permission/StubPermissionPluginService.java b/common/src/main/java/com/cssbham/cssminecraft/common/permission/StubPermissionPluginService.java new file mode 100644 index 0000000..bc2f5cc --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/permission/StubPermissionPluginService.java @@ -0,0 +1,21 @@ +package com.cssbham.cssminecraft.common.permission; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * A stub permission plugin service, for when no plugin is available + */ +public class StubPermissionPluginService implements PermissionPluginService { + + @Override + public CompletableFuture grantMemberRole(UUID player) { + throw new UnsupportedOperationException("No permission plugin available!"); + } + + @Override + public boolean isAvailable() { + return false; + } + +} diff --git a/common/src/main/java/com/cssbham/cssminecraft/common/util/CommandUtil.java b/common/src/main/java/com/cssbham/cssminecraft/common/util/CommandUtil.java new file mode 100644 index 0000000..9588cc1 --- /dev/null +++ b/common/src/main/java/com/cssbham/cssminecraft/common/util/CommandUtil.java @@ -0,0 +1,12 @@ +package com.cssbham.cssminecraft.common.util; + +public final class CommandUtil { + + /** + * A list of all command labels. + */ + // figure out a better way for this + public static final String[] ALL_COMMANDS = new String[]{ "makegreen", "mg", "green" }; + + private CommandUtil() { } +} diff --git a/fabric/build.gradle b/fabric/build.gradle new file mode 100644 index 0000000..c65cb1d --- /dev/null +++ b/fabric/build.gradle @@ -0,0 +1,66 @@ +import net.fabricmc.loom.task.RemapJarTask + +plugins { + id "fabric-loom" version "1.7-SNAPSHOT" + id "java" +} + +processResources { + duplicatesStrategy = duplicatesStrategy.INCLUDE + from(sourceSets.main.resources.srcDirs) { + include "fabric.mod.json" + expand("version": project.version) + } +} + +repositories { + maven { url = "https://maven.fabricmc.net/" } +} + +dependencies { + minecraft "com.mojang:minecraft:${minecraft_version}" + mappings "net.fabricmc:yarn:${yarn_mappings}" + modImplementation "net.fabricmc:fabric-loader:${loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_version}" + + implementation ("net.kyori:adventure-api:4.17.0") { + exclude(module: "adventure-bom") + exclude(module: "annotations") + } + implementation ("net.kyori:adventure-text-serializer-gson:4.17.0") { + exclude(module: "adventure-bom") + exclude(module: "adventure-api") + exclude(module: "annotations") + exclude(module: "auto-service-annotations") + exclude(module: "gson") + } + + implementation project(path: ":common", configuration: "shadow") +} + +shadowJar { + dependencies { + include(project(":common")) + include(dependency("net.kyori:.*")) + } + + relocate "net.kyori", "com.cssbham.cssminecraft.lib.adventure" + + exclude "/mappings/*" + + archiveFileName = "cssminecraft-fabric-${project.version}-no-map.jar" +} + +tasks.register("remapShadowJar", RemapJarTask) { + dependsOn tasks.shadowJar + input = tasks.shadowJar.archiveFile + addNestedDependencies = true + archiveFileName = "cssminecraft-fabric-${project.version}.jar" +} + +tasks.assemble.dependsOn tasks.remapShadowJar + +artifacts { + archives remapShadowJar + shadow shadowJar +} diff --git a/fabric/gradle.properties b/fabric/gradle.properties new file mode 100644 index 0000000..5b783a3 --- /dev/null +++ b/fabric/gradle.properties @@ -0,0 +1,4 @@ +minecraft_version=1.21.1 +yarn_mappings=1.21.1+build.3 +loader_version=0.15.11 +fabric_version=0.102.1+1.21.1 diff --git a/fabric/src/main/java/com/cssbham/cssminecraft/fabric/CSSMinecraftLoader.java b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/CSSMinecraftLoader.java new file mode 100644 index 0000000..bede6c2 --- /dev/null +++ b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/CSSMinecraftLoader.java @@ -0,0 +1,37 @@ +package com.cssbham.cssminecraft.fabric; + +import net.fabricmc.api.DedicatedServerModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.minecraft.server.MinecraftServer; + +/** + * Entrypoint for Fabric + */ +public class CSSMinecraftLoader implements DedicatedServerModInitializer { + + private final FabricCSSMinecraftPlugin plugin; + + public CSSMinecraftLoader() { + this.plugin = new FabricCSSMinecraftPlugin(); + } + + @Override + public void onInitializeServer() { + ServerLifecycleEvents.SERVER_STARTING.register(this::onStart); + ServerLifecycleEvents.SERVER_STOPPING.register(this::onStop); + } + + private void onStart(MinecraftServer server) { + this.plugin.setServer(server); + try { + this.plugin.enable(); + } catch (Exception e) { + this.plugin.getLogger().severe("Mod initialisation failed - disabling"); + this.plugin.disable(); + } + } + + private void onStop(MinecraftServer server) { + this.plugin.disable(); + } +} diff --git a/fabric/src/main/java/com/cssbham/cssminecraft/fabric/FabricCSSMinecraftPlugin.java b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/FabricCSSMinecraftPlugin.java new file mode 100644 index 0000000..dea7b8e --- /dev/null +++ b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/FabricCSSMinecraftPlugin.java @@ -0,0 +1,75 @@ +package com.cssbham.cssminecraft.fabric; + +import com.cssbham.cssminecraft.common.command.CommandService; +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import com.cssbham.cssminecraft.fabric.adapter.FabricServerChatAdapter; +import com.cssbham.cssminecraft.fabric.command.FabricCommandService; +import com.cssbham.cssminecraft.fabric.executor.FabricServerExecutor; +import com.cssbham.cssminecraft.fabric.listener.FabricEventAdapter; +import com.cssbham.cssminecraft.fabric.logger.FabricLogger; +import com.cssbham.cssminecraft.common.AbstractCSSMinecraftPlugin; +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import com.cssbham.cssminecraft.common.logger.Logger; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.server.MinecraftServer; + +import java.nio.file.Path; + +/** + * Implementation of CSS Minecraft Plugin for Fabric + */ +public class FabricCSSMinecraftPlugin extends AbstractCSSMinecraftPlugin { + + public static final String MOD_ID = "cssminecraft"; + private final FabricLogger logger; + private FabricServerChatAdapter serverChatAdapter; + private FabricServerExecutor executor; + private FabricCommandService commandService; + + private MinecraftServer server; + + public FabricCSSMinecraftPlugin() { + this.logger = new FabricLogger(MOD_ID); + } + + @Override + public void enable() { + this.serverChatAdapter = new FabricServerChatAdapter(server); + this.executor = new FabricServerExecutor(logger, server); + this.commandService = new FabricCommandService(logger, executor, serverChatAdapter, server); + + super.enable(); + + FabricEventAdapter eventAdapter = new FabricEventAdapter(executor); + eventAdapter.bindPlatformToEventBus(super.getEventBus()); + } + + @Override + public Logger getLogger() { + return logger; + } + + @Override + public ServerChatAdapter provideServerChatAdapter() { + return serverChatAdapter; + } + + @Override + public Path provideConfigurationPath() { + return FabricLoader.getInstance().getConfigDir().resolve(MOD_ID).resolve("config.yml"); + } + + @Override + public ServerExecutor provideServerExecutor() { + return executor; + } + + @Override + public CommandService provideCommandService() { + return commandService; + } + + public void setServer(MinecraftServer server) { + this.server = server; + } +} diff --git a/fabric/src/main/java/com/cssbham/cssminecraft/fabric/adapter/FabricServerChatAdapter.java b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/adapter/FabricServerChatAdapter.java new file mode 100644 index 0000000..6eb5c20 --- /dev/null +++ b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/adapter/FabricServerChatAdapter.java @@ -0,0 +1,43 @@ +package com.cssbham.cssminecraft.fabric.adapter; + +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.minecraft.registry.DynamicRegistryManager; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +import java.util.UUID; + +public class FabricServerChatAdapter implements ServerChatAdapter { + + private final MinecraftServer server; + + public FabricServerChatAdapter(MinecraftServer server) { + this.server = server; + } + + @Override + public void broadcastMessage(Component message) { + server.getPlayerManager().broadcast(componentToText(message), false); + } + + @Override + public void sendMessageToPlayer(UUID user, Component component) { + ServerPlayerEntity player = server.getPlayerManager().getPlayer(user); + if (null != player) { + player.sendMessage(componentToText(component)); + } + } + + @Override + public void sendMessageToConsole(Component component) { + server.getCommandSource().sendMessage(componentToText(component)); + } + + public Text componentToText(Component component) { + return Text.Serialization.fromJsonTree(GsonComponentSerializer.gson().serializeToTree(component), DynamicRegistryManager.EMPTY); + } + +} diff --git a/fabric/src/main/java/com/cssbham/cssminecraft/fabric/command/FabricCommandService.java b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/command/FabricCommandService.java new file mode 100644 index 0000000..5bf4499 --- /dev/null +++ b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/command/FabricCommandService.java @@ -0,0 +1,67 @@ +package com.cssbham.cssminecraft.fabric.command; + +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import com.cssbham.cssminecraft.common.command.AbstractCommandService; +import com.cssbham.cssminecraft.common.command.CommandContext; +import com.cssbham.cssminecraft.common.command.CommandSender; +import com.cssbham.cssminecraft.common.command.CommandService; +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import com.cssbham.cssminecraft.common.logger.Logger; +import com.cssbham.cssminecraft.common.util.CommandUtil; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; + +import static net.minecraft.server.command.CommandManager.*; +import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; +import static com.mojang.brigadier.arguments.StringArgumentType.getString; + +import java.util.UUID; + +public class FabricCommandService extends AbstractCommandService { + + private final ServerChatAdapter chatAdapter; + + public FabricCommandService(Logger logger, ServerExecutor executor, ServerChatAdapter chatAdapter, MinecraftServer server) { + super(logger, executor); + + this.chatAdapter = chatAdapter; + + for (String label : CommandUtil.ALL_COMMANDS) { + server.getCommandManager().getDispatcher().register(literal(label) + .executes(context -> { + super.execute( + getCommandSenderForSource(context.getSource()), + new CommandContext(label, new String[0]) + ); + return 1; + }) + .then(argument("args", greedyString()) + .executes(context -> { + super.execute( + getCommandSenderForSource(context.getSource()), + new CommandContext(label, getString(context, "args").split(" ")) + ); + return 1; + }))); + } + } + + private CommandSender getCommandSenderForSource(ServerCommandSource source) { + if (source.isExecutedByPlayer()) { + return new CommandSender( + chatAdapter, + source.getPlayer().getUuid(), + source.getName(), + false + ); + } else { + return new CommandSender( + chatAdapter, + new UUID(0, 0), + source.getName(), + true + ); + } + } +} diff --git a/fabric/src/main/java/com/cssbham/cssminecraft/fabric/executor/FabricServerExecutor.java b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/executor/FabricServerExecutor.java new file mode 100644 index 0000000..1530466 --- /dev/null +++ b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/executor/FabricServerExecutor.java @@ -0,0 +1,21 @@ +package com.cssbham.cssminecraft.fabric.executor; + +import com.cssbham.cssminecraft.common.executor.AsyncServerExecutor; +import com.cssbham.cssminecraft.common.logger.Logger; +import net.minecraft.server.MinecraftServer; + +public class FabricServerExecutor extends AsyncServerExecutor { + + private final MinecraftServer server; + + public FabricServerExecutor(Logger logger, MinecraftServer server) { + super(logger); + this.server = server; + } + + @Override + public void doSync(Runnable runnable) { + server.executeSync(runnable); + } + +} diff --git a/fabric/src/main/java/com/cssbham/cssminecraft/fabric/listener/FabricEventAdapter.java b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/listener/FabricEventAdapter.java new file mode 100644 index 0000000..4ff3f02 --- /dev/null +++ b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/listener/FabricEventAdapter.java @@ -0,0 +1,63 @@ +package com.cssbham.cssminecraft.fabric.listener; + +import com.cssbham.cssminecraft.common.event.Event; +import com.cssbham.cssminecraft.common.event.EventBus; +import com.cssbham.cssminecraft.common.event.PlatformEventAdapter; +import com.cssbham.cssminecraft.common.event.events.PlayerJoinEvent; +import com.cssbham.cssminecraft.common.event.events.PlayerQuitEvent; +import com.cssbham.cssminecraft.common.event.events.ServerMessageEvent; +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.network.ServerPlayerEntity; + +public class FabricEventAdapter implements PlatformEventAdapter { + + private final ServerExecutor executor; + + public FabricEventAdapter(ServerExecutor executor) { + this.executor = executor; + } + + @Override + public void bindPlatformToEventBus(EventBus eventBus) { + ServerMessageEvents.CHAT_MESSAGE.register((message, player, parameters) -> { + String name = player.getName().getString(); + + dispatchEvent(eventBus, new ServerMessageEvent( + player.getUuid(), + player.getName().getString(), + (null == player.getDisplayName()) ? name : player.getDisplayName().getString(), + message.getSignedContent() + )); + }); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + ServerPlayerEntity player = handler.getPlayer(); + String name = player.getName().getString(); + + dispatchEvent(eventBus, new PlayerJoinEvent( + player.getUuid(), + player.getName().getString(), + (null == player.getDisplayName()) ? name : player.getDisplayName().getString(), + server.getCurrentPlayerCount() + 1 + )); + }); + + ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { + ServerPlayerEntity player = handler.getPlayer(); + String name = player.getName().getString(); + + dispatchEvent(eventBus, new PlayerQuitEvent( + player.getUuid(), + player.getName().getString(), + (null == player.getDisplayName()) ? name : player.getDisplayName().getString(), + server.getCurrentPlayerCount() - 1 + )); + }); + } + + private void dispatchEvent(EventBus eventBus, Event event) { + executor.doAsync(() -> eventBus.dispatch(event)); + } +} diff --git a/fabric/src/main/java/com/cssbham/cssminecraft/fabric/logger/FabricLogger.java b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/logger/FabricLogger.java new file mode 100644 index 0000000..8adf4ae --- /dev/null +++ b/fabric/src/main/java/com/cssbham/cssminecraft/fabric/logger/FabricLogger.java @@ -0,0 +1,29 @@ +package com.cssbham.cssminecraft.fabric.logger; + +import com.cssbham.cssminecraft.common.logger.AbstractLogger; +import org.slf4j.LoggerFactory; + +public class FabricLogger extends AbstractLogger { + + private final org.slf4j.Logger logger; + + public FabricLogger(String name) { + this.logger = LoggerFactory.getLogger(name); + } + + @Override + protected void logInfo(String string) { + logger.info(string); + } + + @Override + protected void logError(String string) { + logger.error(string); + } + + @Override + protected void logWarning(String string) { + logger.warn(string); + } + +} diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..590ab9f --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "id": "cssminecraft", + "version": "${version}", + "name": "CSSMinecraft", + "description": "CSS' Minecraft plugin", + "authors": [ + "LMBishop" + ], + "environment": "server", + "entrypoints": { + "server": [ + "com.cssbham.cssminecraft.fabric.CSSMinecraftLoader" + ] + }, + "depends": { + "fabricloader": ">=0.15.11", + "minecraft": "~1.21", + "java": ">=21", + "fabric-api": "*" + }, + "recommends": { + "luckperms": "*" + } +} \ No newline at end of file diff --git a/forge/build.gradle b/forge/build.gradle new file mode 100644 index 0000000..180f41c --- /dev/null +++ b/forge/build.gradle @@ -0,0 +1,48 @@ +plugins { + id "net.minecraftforge.gradle" version "[6.0,6.2)" + id "java" +} + +processResources { + duplicatesStrategy = duplicatesStrategy.INCLUDE + from(sourceSets.main.resources.srcDirs) { + include "**/mods.toml" + expand("version": project.version) + } +} + +minecraft { + mappings channel: "official", version: minecraftVersion +} + +dependencies { + minecraft ("net.minecraftforge:forge:${minecraftVersion}-${forgeVersion}") + + implementation ("net.kyori:adventure-api:4.17.0") { + exclude(module: "adventure-bom") + exclude(module: "annotations") + } + implementation ("net.kyori:adventure-text-serializer-gson:4.17.0") { + exclude(module: "adventure-bom") + exclude(module: "adventure-api") + exclude(module: "annotations") + exclude(module: "auto-service-annotations") + exclude(module: "gson") + } + + implementation project(path: ":common", configuration: "shadow") +} + + +shadowJar { + dependencies { + include(project(":common")) + include(dependency("net.kyori:.*")) + } + + relocate "net.kyori", "com.cssbham.cssminecraft.lib.adventure" + + archiveFileName = "cssminecraft-forge-${project.version}.jar" + + minimize() +} diff --git a/forge/gradle.properties b/forge/gradle.properties new file mode 100644 index 0000000..94b859e --- /dev/null +++ b/forge/gradle.properties @@ -0,0 +1,2 @@ +minecraftVersion=1.21.1 +forgeVersion=52.0.2 \ No newline at end of file diff --git a/forge/src/main/java/com/cssbham/cssminecraft/forge/CSSMinecraftLoader.java b/forge/src/main/java/com/cssbham/cssminecraft/forge/CSSMinecraftLoader.java new file mode 100644 index 0000000..2c97f44 --- /dev/null +++ b/forge/src/main/java/com/cssbham/cssminecraft/forge/CSSMinecraftLoader.java @@ -0,0 +1,35 @@ +package com.cssbham.cssminecraft.forge; + +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.server.ServerStartingEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.fml.common.Mod; + +/** + * Entrypoint for Forge + */ +@Mod(value = "cssminecraft") +public class CSSMinecraftLoader { + + private final ForgeCSSMinecraftPlugin plugin; + + public CSSMinecraftLoader() { + this.plugin = new ForgeCSSMinecraftPlugin(); + MinecraftForge.EVENT_BUS.addListener(this::onServerStarted); + } + + public void onServerStarted(ServerStartingEvent event) { + this.plugin.setServer(event.getServer()); + try { + this.plugin.enable(); + } catch (Exception e) { + this.plugin.getLogger().severe("Mod initialisation failed - disabling"); + this.plugin.disable(); + } + } + + public void onServerStopping(ServerStoppingEvent event) { + this.plugin.disable(); + } + +} diff --git a/forge/src/main/java/com/cssbham/cssminecraft/forge/ForgeCSSMinecraftPlugin.java b/forge/src/main/java/com/cssbham/cssminecraft/forge/ForgeCSSMinecraftPlugin.java new file mode 100644 index 0000000..ad48b2d --- /dev/null +++ b/forge/src/main/java/com/cssbham/cssminecraft/forge/ForgeCSSMinecraftPlugin.java @@ -0,0 +1,75 @@ +package com.cssbham.cssminecraft.forge; + +import com.cssbham.cssminecraft.common.command.CommandService; +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import com.cssbham.cssminecraft.forge.adapter.ForgeServerChatAdapter; +import com.cssbham.cssminecraft.forge.command.ForgeCommandService; +import com.cssbham.cssminecraft.forge.executor.ForgeServerExecutor; +import com.cssbham.cssminecraft.forge.listener.ForgeEventAdapter; +import com.cssbham.cssminecraft.forge.logger.ForgeLogger; +import com.cssbham.cssminecraft.common.AbstractCSSMinecraftPlugin; +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import com.cssbham.cssminecraft.common.logger.Logger; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.fml.loading.FMLPaths; + +import java.nio.file.Path; + +/** + * Implementation of CSS Minecraft Plugin for Forge + */ +public class ForgeCSSMinecraftPlugin extends AbstractCSSMinecraftPlugin { + + public static final String MOD_ID = "cssminecraft"; + private final ForgeLogger logger; + private ForgeServerChatAdapter serverChatAdapter; + + private MinecraftServer server; + private ForgeServerExecutor executor; + private ForgeCommandService commandService; + + public ForgeCSSMinecraftPlugin() { + this.logger = new ForgeLogger(MOD_ID); + } + + @Override + public void enable() { + this.serverChatAdapter = new ForgeServerChatAdapter(server); + this.executor = new ForgeServerExecutor(logger, server); + this.commandService = new ForgeCommandService(logger, executor, serverChatAdapter, server); + + super.enable(); + + ForgeEventAdapter eventAdapter = new ForgeEventAdapter(server, executor); + eventAdapter.bindPlatformToEventBus(super.getEventBus()); + } + + @Override + public Logger getLogger() { + return logger; + } + + @Override + public ServerChatAdapter provideServerChatAdapter() { + return serverChatAdapter; + } + + @Override + public Path provideConfigurationPath() { + return FMLPaths.CONFIGDIR.get().resolve(MOD_ID).resolve("config.yml"); + } + + @Override + public ServerExecutor provideServerExecutor() { + return executor; + } + + @Override + public CommandService provideCommandService() { + return commandService; + } + + public void setServer(MinecraftServer server) { + this.server = server; + } +} diff --git a/forge/src/main/java/com/cssbham/cssminecraft/forge/adapter/ForgeServerChatAdapter.java b/forge/src/main/java/com/cssbham/cssminecraft/forge/adapter/ForgeServerChatAdapter.java new file mode 100644 index 0000000..dd7860b --- /dev/null +++ b/forge/src/main/java/com/cssbham/cssminecraft/forge/adapter/ForgeServerChatAdapter.java @@ -0,0 +1,42 @@ +package com.cssbham.cssminecraft.forge.adapter; + +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.minecraft.core.RegistryAccess; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +import java.util.UUID; + +public class ForgeServerChatAdapter implements ServerChatAdapter { + + private final MinecraftServer server; + + public ForgeServerChatAdapter(MinecraftServer server) { + this.server = server; + } + + @Override + public void broadcastMessage(Component message) { + server.getPlayerList().broadcastSystemMessage(componentToMinecraftComponent(message), false); + } + + @Override + public void sendMessageToPlayer(UUID user, Component component) { + ServerPlayer player = server.getPlayerList().getPlayer(user); + if (null != player) { + player.sendSystemMessage(componentToMinecraftComponent(component)); + } + } + + @Override + public void sendMessageToConsole(Component component) { + server.sendSystemMessage(componentToMinecraftComponent(component)); + } + + public net.minecraft.network.chat.Component componentToMinecraftComponent(Component component) { + return net.minecraft.network.chat.Component.Serializer.fromJson(GsonComponentSerializer.gson().serializeToTree(component), RegistryAccess.EMPTY); + } + +} diff --git a/forge/src/main/java/com/cssbham/cssminecraft/forge/command/ForgeCommandService.java b/forge/src/main/java/com/cssbham/cssminecraft/forge/command/ForgeCommandService.java new file mode 100644 index 0000000..d64b01e --- /dev/null +++ b/forge/src/main/java/com/cssbham/cssminecraft/forge/command/ForgeCommandService.java @@ -0,0 +1,68 @@ +package com.cssbham.cssminecraft.forge.command; + +import com.cssbham.cssminecraft.common.adapter.ServerChatAdapter; +import com.cssbham.cssminecraft.common.command.AbstractCommandService; +import com.cssbham.cssminecraft.common.command.CommandContext; +import com.cssbham.cssminecraft.common.command.CommandSender; +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import com.cssbham.cssminecraft.common.logger.Logger; +import com.cssbham.cssminecraft.common.util.CommandUtil; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; + +import java.util.UUID; + +import static com.mojang.brigadier.arguments.StringArgumentType.getString; +import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; +import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; +import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument; + +public class ForgeCommandService extends AbstractCommandService { + + private final ServerChatAdapter chatAdapter; + + public ForgeCommandService(Logger logger, ServerExecutor executor, ServerChatAdapter chatAdapter, MinecraftServer server) { + super(logger, executor); + + this.chatAdapter = chatAdapter; + + for (String label : CommandUtil.ALL_COMMANDS) { + // this is "unsafe" only because brigadier is not obfuscated + server.getCommands().getDispatcher().register((LiteralArgumentBuilder) literal(label) + .executes(context -> { + super.execute( + getCommandSenderForSource((CommandSourceStack) context.getSource()), + new CommandContext(label, new String[0]) + ); + return 1; + }) + .then(argument("args", greedyString()) + .executes(context -> { + super.execute( + getCommandSenderForSource((CommandSourceStack) context.getSource()), + new CommandContext(label, getString(context, "args").split(" ")) + ); + return 1; + }))); + } + } + + private CommandSender getCommandSenderForSource(CommandSourceStack source) { + if (source.isPlayer()) { + return new CommandSender( + chatAdapter, + source.getPlayer().getUUID(), + source.getTextName(), + false + ); + } else { + return new CommandSender( + chatAdapter, + new UUID(0, 0), + source.getTextName(), + true + ); + } + } +} diff --git a/forge/src/main/java/com/cssbham/cssminecraft/forge/executor/ForgeServerExecutor.java b/forge/src/main/java/com/cssbham/cssminecraft/forge/executor/ForgeServerExecutor.java new file mode 100644 index 0000000..76a1416 --- /dev/null +++ b/forge/src/main/java/com/cssbham/cssminecraft/forge/executor/ForgeServerExecutor.java @@ -0,0 +1,21 @@ +package com.cssbham.cssminecraft.forge.executor; + +import com.cssbham.cssminecraft.common.executor.AsyncServerExecutor; +import com.cssbham.cssminecraft.common.logger.Logger; +import net.minecraft.server.MinecraftServer; + +public class ForgeServerExecutor extends AsyncServerExecutor { + + private final MinecraftServer server; + + public ForgeServerExecutor(Logger logger, MinecraftServer server) { + super(logger); + this.server = server; + } + + @Override + public void doSync(Runnable runnable) { + server.executeIfPossible(runnable); + } + +} diff --git a/forge/src/main/java/com/cssbham/cssminecraft/forge/listener/ForgeEventAdapter.java b/forge/src/main/java/com/cssbham/cssminecraft/forge/listener/ForgeEventAdapter.java new file mode 100644 index 0000000..51ec243 --- /dev/null +++ b/forge/src/main/java/com/cssbham/cssminecraft/forge/listener/ForgeEventAdapter.java @@ -0,0 +1,83 @@ +package com.cssbham.cssminecraft.forge.listener; + +import com.cssbham.cssminecraft.common.event.Event; +import com.cssbham.cssminecraft.common.event.EventBus; +import com.cssbham.cssminecraft.common.event.PlatformEventAdapter; +import com.cssbham.cssminecraft.common.event.events.PlayerJoinEvent; +import com.cssbham.cssminecraft.common.event.events.PlayerQuitEvent; +import com.cssbham.cssminecraft.common.event.events.ServerMessageEvent; +import com.cssbham.cssminecraft.common.executor.ServerExecutor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.ServerChatEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +import java.util.Objects; + +public class ForgeEventAdapter implements PlatformEventAdapter { + + private final MinecraftServer server; + private final ServerExecutor executor; + + public ForgeEventAdapter(MinecraftServer server, ServerExecutor executor) { + this.server = server; + this.executor = executor; + } + + private EventBus eventBus; + + @Override + public void bindPlatformToEventBus(EventBus eventBus) { + this.eventBus = eventBus; + + MinecraftForge.EVENT_BUS.register(this); + } + + private void dispatchEvent(Event event) { + Objects.requireNonNull(event, "event bus not bound"); + + executor.doAsync(() -> eventBus.dispatch(event)); + } + + @SubscribeEvent + public void onChat(ServerChatEvent event) { + ServerPlayer player = event.getPlayer(); + String name = event.getUsername(); + + dispatchEvent(new ServerMessageEvent( + player.getUUID(), + name, + (null == player.getDisplayName()) ? name : player.getDisplayName().getString(), + event.getRawText() + )); + } + + @SubscribeEvent + public void onLogin(PlayerEvent.PlayerLoggedInEvent event) { + Player player = event.getEntity(); + String name = player.getName().getString(); + + dispatchEvent(new PlayerJoinEvent( + player.getUUID(), + name, + (null == player.getDisplayName()) ? name : player.getDisplayName().getString(), + server.getPlayerCount() + )); + } + + @SubscribeEvent + public void onLogout(PlayerEvent.PlayerLoggedOutEvent event) { + Player player = event.getEntity(); + String name = player.getName().getString(); + + dispatchEvent(new PlayerQuitEvent( + player.getUUID(), + name, + (null == player.getDisplayName()) ? name : player.getDisplayName().getString(), + server.getPlayerCount() - 1 + )); + } +} diff --git a/forge/src/main/java/com/cssbham/cssminecraft/forge/logger/ForgeLogger.java b/forge/src/main/java/com/cssbham/cssminecraft/forge/logger/ForgeLogger.java new file mode 100644 index 0000000..959989c --- /dev/null +++ b/forge/src/main/java/com/cssbham/cssminecraft/forge/logger/ForgeLogger.java @@ -0,0 +1,29 @@ +package com.cssbham.cssminecraft.forge.logger; + +import com.cssbham.cssminecraft.common.logger.AbstractLogger; +import org.apache.logging.log4j.LogManager; + +public class ForgeLogger extends AbstractLogger { + + private final org.apache.logging.log4j.Logger logger; + + public ForgeLogger(String name) { + this.logger = LogManager.getLogger(name); + } + + @Override + protected void logInfo(String string) { + logger.info(string); + } + + @Override + protected void logError(String string) { + logger.error(string); + } + + @Override + protected void logWarning(String string) { + logger.warn(string); + } + +} diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..2dcbc73 --- /dev/null +++ b/forge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,22 @@ +# https://docs.minecraftforge.net/en/latest/gettingstarted/modfiles/ +modLoader="javafml" +loaderVersion="[46,)" +license="todo" +showAsResourcePack=false + +[[mods]] +modId="cssminecraft" +version="${version}" +displayName="CSSMinecraft" +logoFile="logo.png" +authors="LMBishop" +description=''' + CSS' Minecraft plugin + ''' + +[[dependencies.cssminecraft]] +modId="luckperms" +mandatory=false +versionRange="" +ordering="AFTER" +side="SERVER" diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..649cce1 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx2G +org.gradle.parallel=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..2c35211 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..09523c0 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# 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 -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 + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9b42019 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 920fc8e..0000000 --- a/pom.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - 4.0.0 - - com.cssbham - CSS-Minecraft - 0.0.1 - jar - - CSS Minecraft - - The core plugin for CSS' Minecraft Server. - - 1.8 - UTF-8 - - https://github.com/CSSUoB/CSS-Minecraft - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - ${java.version} - ${java.version} - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.4 - - - package - - shade - - - false - - - - - - - - src/main/resources - true - - - - - - - spigotmc-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - - - sonatype - https://oss.sonatype.org/content/groups/public/ - - - dv8tion - m2-dv8tion - https://m2.dv8tion.net/releases - - - - - - org.spigotmc - spigot-api - 1.20.2-R0.1-SNAPSHOT - provided - - - net.luckperms - api - 5.4 - provided - - - net.dv8tion - JDA - 5.0.0-beta.17 - - - club.minnced - opus-java - - - - - club.minnced - discord-webhooks - 0.8.2 - - - diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..b9d5520 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = "https://maven.fabricmc.net/" } + maven { url = "https://maven.minecraftforge.net/" } + } +} + +rootProject.name = "css-minecraft" + +include( + "common", + "bukkit", + "fabric", + "forge" +) \ No newline at end of file diff --git a/src/main/java/com/cssbham/minecraftcore/MinecraftCore.java b/src/main/java/com/cssbham/minecraftcore/MinecraftCore.java deleted file mode 100644 index a6c89d7..0000000 --- a/src/main/java/com/cssbham/minecraftcore/MinecraftCore.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.cssbham.minecraftcore; - -import com.cssbham.minecraftcore.commands.CommandMakeGreen; -import com.cssbham.minecraftcore.discord.DiscordBridge; -import org.bukkit.Server; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.AsyncPlayerChatEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.plugin.java.JavaPlugin; - -public final class MinecraftCore extends JavaPlugin implements Listener { - - private DiscordBridge discordBridge; - - @Override - @SuppressWarnings("ConstantConditions") - public void onEnable() { - saveDefaultConfig(); - if (discordBridge != null) { - discordBridge.shutdown(); - } - try { - discordBridge = new DiscordBridge(this); - } catch (Exception e) { - return; - } - - this.getServer().getPluginManager().registerEvents(this, this); - this.getCommand("makegreen").setExecutor(new CommandMakeGreen(discordBridge)); - this.getLogger().info("Plugin has been enabled."); - - } - - @Override - public void onDisable() { - this.getLogger().warning("Plugin has been disabled."); - try { - discordBridge.shutdown(); - discordBridge = null; - } catch (Exception ignored) { - } - // Plugin shutdown logic - } - - @EventHandler - public void onPlayerChat(AsyncPlayerChatEvent event) { - discordBridge.sendSanitisedMessageToDiscord(event.getPlayer(), event.getMessage()); - } - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - discordBridge.sendMessageToDiscord(event.getPlayer(), - "__*has joined the server, " + getOnlineMessage(event.getPlayer().getServer(), false) + "*__"); - } - - @EventHandler - public void onPlayerQuit(PlayerQuitEvent event) { - discordBridge.sendMessageToDiscord(event.getPlayer(), - "__*has left the server, " + getOnlineMessage(event.getPlayer().getServer(), true) + "*__"); - } - - private String getOnlineMessage(Server server, boolean leaving) { - int amount = server.getOnlinePlayers().size(); - if (leaving) { - amount--; - } - // This shouldn't? happen. - if (amount < 0) { - amount = 0; - } - switch (amount) { - case 0: { - return "there are now no players online."; - } - case 1: { - return "there is now 1 player online."; - } - default: { - return "there are now " + amount + " players online."; - } - } - } -} diff --git a/src/main/java/com/cssbham/minecraftcore/commands/CommandMakeGreen.java b/src/main/java/com/cssbham/minecraftcore/commands/CommandMakeGreen.java deleted file mode 100644 index dd355ff..0000000 --- a/src/main/java/com/cssbham/minecraftcore/commands/CommandMakeGreen.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.cssbham.minecraftcore.commands; - -import com.cssbham.minecraftcore.discord.DiscordBridge; -import com.cssbham.minecraftcore.util.MessageUtil; -import net.luckperms.api.LuckPerms; -import net.luckperms.api.LuckPermsProvider; -import net.luckperms.api.node.Node; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -public class CommandMakeGreen implements CommandExecutor { - - private final DiscordBridge discordBridge; - - public CommandMakeGreen(DiscordBridge discordBridge) { - this.discordBridge = discordBridge; - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, - @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player)) { - sender.sendMessage("Only players can execute this command."); - return true; - } - String arg = String.join(" ", args); - if (!arg.matches("[a-z0-9._]{2,32}|.{2,32}#[0-9]{4}")) { - return false; - } - - if (discordBridge.isMember(arg)) { - LuckPerms perms = LuckPermsProvider.get(); - perms.getUserManager().modifyUser(((Player) sender).getUniqueId(), - user -> { - user.data().add(Node.builder("group.member").build()); - user.data().remove(Node.builder("group.guest").build()); - } - ); - sender.sendMessage(MessageUtil.getMemberGreen() + "Congratulations, you are now green!"); - } else { - sender.sendMessage(MessageUtil.getCSSPrefix() + - "§cIf you are a member, please link your account in Discord!\n" + - "Or you can buy membership at https://cssbham.com/join" - ); - } - return true; - } -} diff --git a/src/main/java/com/cssbham/minecraftcore/discord/DiscordBridge.java b/src/main/java/com/cssbham/minecraftcore/discord/DiscordBridge.java deleted file mode 100644 index 74f3ba6..0000000 --- a/src/main/java/com/cssbham/minecraftcore/discord/DiscordBridge.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.cssbham.minecraftcore.discord; - -import club.minnced.discord.webhook.WebhookClient; -import club.minnced.discord.webhook.WebhookClientBuilder; -import club.minnced.discord.webhook.send.WebhookMessageBuilder; -import com.cssbham.minecraftcore.MinecraftCore; -import com.cssbham.minecraftcore.util.MessageUtil; -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.JDABuilder; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import net.dv8tion.jda.api.requests.GatewayIntent; -import net.dv8tion.jda.api.utils.ChunkingFilter; -import net.dv8tion.jda.api.utils.MemberCachePolicy; -import okhttp3.OkHttpClient; -import org.bukkit.ChatColor; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import javax.security.auth.login.LoginException; - -public class DiscordBridge extends ListenerAdapter { - - private final Long MEMBER_ROLE_ID; - private final Long BRIDGE_CHANNEL_ID; - private final Long DISCORD_SERVER_ID; - private final String AVATAR; - - private JDA jda = null; - private MinecraftCore core = null; - private WebhookClient webhook = null; - private boolean shutdown = false; - - public DiscordBridge(MinecraftCore core) throws LoginException, ClassCastException { - - FileConfiguration configuration = core.getConfig(); - core.reloadConfig(); - MEMBER_ROLE_ID = Long.parseLong(configuration.getString("MEMBER_ROLE_ID")); - BRIDGE_CHANNEL_ID = Long.parseLong(configuration.getString("BRIDGE_CHANNEL_ID")); - DISCORD_SERVER_ID = Long.parseLong(configuration.getString("DISCORD_SERVER_ID")); - AVATAR = configuration.getString("AVATAR_SERVICE"); - final String BOT_TOKEN = configuration.getString("BOT_TOKEN"); - final String WEBHOOK_URL = configuration.getString("WEBHOOK_URL"); - - this.core = core; - this.jda = JDABuilder.createDefault( - BOT_TOKEN - ).setMemberCachePolicy(MemberCachePolicy.ALL) - .setChunkingFilter(ChunkingFilter.ALL) - .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT).addEventListeners(this).build(); - this.webhook = new WebhookClientBuilder(WEBHOOK_URL) - .setThreadFactory(Thread::new) - .setDaemon(true) - .setWait(true) - .setHttpClient(new OkHttpClient()) - .build(); - } - - @Override - public void onMessageReceived(@NotNull MessageReceivedEvent event) { - if (!event.isFromGuild() || shutdown || event.getChannel().getIdLong() != BRIDGE_CHANNEL_ID || - event.isWebhookMessage() || - event.getMember() == null || - event.getAuthor().isBot() || - event.getMessage().isEdited()) { - return; - } - String hexColor = String.format("#%06X", (0xFFFFFF & event.getMember().getColorRaw())).toLowerCase(); - - String output = String.format("%s%s %s§r §7>§r %s", - MessageUtil.getDiscordPrefix(), - MessageUtil.getChatColor(hexColor), - event.getMember().getEffectiveName(), - MessageUtil.sanitise(event.getMessage().getContentRaw()) - ); - core.getServer().broadcastMessage(output); - } - - public void sendSanitisedMessageToDiscord(Player player, String message) { - this.sendMessageToDiscord(player, MessageUtil.sanitise(message)); - } - - public void sendMessageToDiscord(Player player, String message) { - if (shutdown) return; - try { - webhook.send(new WebhookMessageBuilder() - .setAvatarUrl(String.format(AVATAR, player.getName())) - .setUsername(ChatColor.stripColor(player.getDisplayName())) - .setContent(message) - .build()); - } catch (Exception ignored) { - } - // https://github.com/DV8FromTheWorld/JDA/issues/1761 - } - - public boolean isMember(String identifier) { - Guild g = jda.getGuildById(DISCORD_SERVER_ID); - if (g == null) return false; - Member m = g.getMembers().stream() - .filter(mm -> - (mm.getUser().getName() + "#" + mm.getUser().getDiscriminator()).equalsIgnoreCase(identifier) || - mm.getUser().getName().equalsIgnoreCase(identifier) - ).findFirst().orElse(null); - if (m == null) return false; - return m.getRoles().stream().anyMatch(r -> r.getIdLong() == MEMBER_ROLE_ID); - } - - public void shutdown() { - shutdown = true; - jda.shutdownNow(); - webhook.close(); - } -} diff --git a/src/main/java/com/cssbham/minecraftcore/util/MessageUtil.java b/src/main/java/com/cssbham/minecraftcore/util/MessageUtil.java deleted file mode 100644 index f992ecb..0000000 --- a/src/main/java/com/cssbham/minecraftcore/util/MessageUtil.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.cssbham.minecraftcore.util; - -import net.dv8tion.jda.api.utils.MarkdownSanitizer; -import net.md_5.bungee.api.ChatColor; - -public class MessageUtil { - - public static String getDiscordPrefix() { - return ChatColor.of("#738abd") + "[Discord]" + ChatColor.RESET; - } - - public static String getCSSPrefix() { - return ChatColor.of("#4a9efe") + "[CSS]" + ChatColor.RESET; - } - - public static String getMemberGreen() { - return MessageUtil.getChatColor("#03e421"); - } - - public static String getChatColor(String color) { - return ChatColor.of(color).toString(); - } - - public static String sanitise(String message) { - return ChatColor.stripColor(MarkdownSanitizer.sanitize(message).replace("@", "(at)")); - } -} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml deleted file mode 100644 index af18bf3..0000000 --- a/src/main/resources/config.yml +++ /dev/null @@ -1,6 +0,0 @@ -MEMBER_ROLE_ID: "000000000000000000" -BRIDGE_CHANNEL_ID: "000000000000000000" -DISCORD_SERVER_ID: "000000000000000000" -WEBHOOK_URL: "" -AVATAR_SERVICE: "" -BOT_TOKEN: "" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml deleted file mode 100644 index 750230d..0000000 --- a/src/main/resources/plugin.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: CSSMinecraft -version: '${project.version}' -main: com.cssbham.minecraftcore.MinecraftCore -api-version: 1.17 -prefix: CSS -authors: [ RaineTheBoosted ] -description: The core plugin for CSS' Minecraft Server. -website: https://github.com/CSSUoB/CSS-Minecraft -depend: [ LuckPerms ] -commands: - makegreen: - description: Make yourself green by verifying your CSS membership. - usage: / [Discord Username] - aliases: [ mg, green ]