diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 1acb82a..0ced84d 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -13,6 +13,10 @@ on: - 'fix/**' workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest @@ -79,14 +83,17 @@ jobs: smoke-tests-paper: strategy: matrix: - version: - - 1.16.5 - - 1.19.2 - - 1.19.4 - - 1.20.1 - - 1.21.1 - - 1.21.8 - - 1.21.11 + include: + - version: 1.16.5 + - version: 1.19.2 + - version: 1.19.4 + skip_command_io: true + - version: 1.20.1 + skip_command_io: true + - version: 1.21.1 + - version: 1.21.8 + - version: 1.21.11 + - version: 26.1.2 runs-on: ubuntu-latest needs: build @@ -105,4 +112,69 @@ jobs: cache-read-only: false - name: Run smoke test + env: + SKIP_COMMAND_IO: ${{ matrix.skip_command_io && '1' || '' }} run: ./scripts/smoke-test-server.sh spigot:runServer -Pspigot.run_minecraft_version=${{ matrix.version }} + + smoke-tests-minestom: + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v5 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: false + + - name: Run smoke test + run: ./scripts/smoke-test-server.sh minestom:runServer -Pmodded.versions_mode=NONE + + smoke-tests-velocity: + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v5 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: false + + - name: Run smoke test + run: ./scripts/smoke-test-proxy.sh velocity:runVelocity -Pmodded.versions_mode=NONE + + smoke-tests-bungee: + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v5 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: false + + - name: Run smoke test + run: ./scripts/smoke-test-proxy.sh bungee:runWaterfall -Pmodded.versions_mode=NONE diff --git a/api/common/src/main/kotlin/su/plo/slib/api/chat/converter/MessageTextConverter.kt b/api/common/src/main/kotlin/su/plo/slib/api/chat/converter/MessageTextConverter.kt new file mode 100644 index 0000000..d3e32a2 --- /dev/null +++ b/api/common/src/main/kotlin/su/plo/slib/api/chat/converter/MessageTextConverter.kt @@ -0,0 +1,33 @@ +package su.plo.slib.api.chat.converter + +import com.mojang.brigadier.Message +import su.plo.slib.api.chat.component.McTextComponent +import su.plo.slib.api.service.lazyService + +/** + * Converts a [McTextComponent] into a platform-specific [Message] suitable for use with brigadier + * (e.g. as the message inside a `CommandSyntaxException`). + * + * Implementations preserve literal and translatable components where the platform allows — typically + * either by producing a native [Message] implementation (Vanilla `Component`, Velocity's + * `VelocityBrigadierMessage`) or by wrapping the component in a platform-specific [Message] that + * the command error-handling path recognizes and unwraps. + */ +interface MessageTextConverter { + + /** + * Converts a [McTextComponent] into a brigadier [Message]. + * + * @param text The [McTextComponent] to convert. + * @return A [Message] representing the converted text component. + */ + fun convert(text: McTextComponent): Message + + companion object { + private val provider: MessageTextConverter by lazyService() + + @JvmStatic + fun converter(): MessageTextConverter = + provider + } +} diff --git a/api/common/src/main/kotlin/su/plo/slib/api/command/McCommandManager.kt b/api/common/src/main/kotlin/su/plo/slib/api/command/McCommandManager.kt index e5475c8..8d1f418 100644 --- a/api/common/src/main/kotlin/su/plo/slib/api/command/McCommandManager.kt +++ b/api/common/src/main/kotlin/su/plo/slib/api/command/McCommandManager.kt @@ -1,7 +1,10 @@ package su.plo.slib.api.command -import com.google.common.collect.ImmutableMap -import com.google.common.collect.Maps +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.tree.LiteralCommandNode +import su.plo.slib.api.command.brigadier.McBrigadierSource /** * Manages universal commands for multiple server implementations. @@ -13,61 +16,78 @@ import com.google.common.collect.Maps */ abstract class McCommandManager { - protected val commandByName: MutableMap = Maps.newHashMap() + /** + * Retrieves a read-only map of registered commands. + * + * @return A map containing the registered commands with their names as keys. + */ + abstract val registeredCommands: Map - protected var registered = false + /** + * Retrieves a read-only list of registered brigadier command nodes. + * + * @return A list of registered brigadier command nodes. + */ + abstract val registeredBrigadierCommands: List> /** - * Registers a command with its name and optional aliases. + * Registers a brigadier command. * - * @param name The primary name of the command. * @param command The instance of the command to register. - * @param aliases Optional alias names for the command. * @throws IllegalStateException If attempting to register commands after commands have already been registered. * @throws IllegalArgumentException If a command with the same name or alias already exists. */ - @Synchronized - fun register(name: String, command: T, vararg aliases: String) { - check(!registered) { "register after commands registration is not supported" } - require(!commandByName.containsKey(name)) { "Command with name '$name' already exist" } - - for (alias in aliases) { - require(!commandByName.containsKey(alias)) { "Command with name '$alias' already exist" } - } - - commandByName[name] = command - for (alias in aliases) { - commandByName[alias] = command - } + fun register(command: LiteralArgumentBuilder) { + register(command.build()) } /** - * Retrieves a read-only map of registered commands. + * Registers a brigadier command. * - * @return A map containing the registered commands with their names as keys. + * @param command The instance of the command to register. + * @throws IllegalStateException If attempting to register commands after commands have already been registered. + * @throws IllegalArgumentException If a command with the same name or alias already exists. + */ + abstract fun register(command: LiteralCommandNode) + + /** + * Registers a command with its name and optional aliases. + * + * @param name The primary name of the command. + * @param command The instance of the command to register. + * @param aliases Optional alias names for the command. + * @throws IllegalStateException If attempting to register commands after commands have already been registered. + * @throws IllegalArgumentException If a command with the same name or alias already exists. */ - @get:Synchronized - val registeredCommands: Map - get() = ImmutableMap.copyOf(commandByName) + abstract fun register(name: String, command: T, vararg aliases: String) /** * Clears all registered commands and resets the registration state. */ - @Synchronized - fun clear() { - commandByName.clear() - registered = false - } + abstract fun clear() /** * Gets a command source by server-specific instance. * * The [source] parameter represents the server-specific command source instance: * - For Velocity `com.velocitypowered.api.command.CommandSource` - * - For BungeeCord `// todo` + * - For BungeeCord `net.md_5.bungee.api.CommandSender` + * - For Spigot/Paper `org.bukkit.command.CommandSender` + * - For Minestom `net.minestom.server.command.CommandSender` + * - For modded servers (Fabric/Forge/NeoForge) `net.minecraft.commands.CommandSourceStack` * * @param source The server-specific command source instance. * @return A [McCommandSource] instance corresponding to the provided command source instance. */ abstract fun getCommandSource(source: Any): McCommandSource + + companion object { + @JvmStatic + fun literal(name: String): LiteralArgumentBuilder = + LiteralArgumentBuilder.literal(name) + + @JvmStatic + fun argument(name: String, argument: ArgumentType): RequiredArgumentBuilder = + RequiredArgumentBuilder.argument(name, argument) + } } diff --git a/api/common/src/main/kotlin/su/plo/slib/api/command/brigadier/ArgumentResolver.kt b/api/common/src/main/kotlin/su/plo/slib/api/command/brigadier/ArgumentResolver.kt new file mode 100644 index 0000000..e78f1ae --- /dev/null +++ b/api/common/src/main/kotlin/su/plo/slib/api/command/brigadier/ArgumentResolver.kt @@ -0,0 +1,19 @@ +package su.plo.slib.api.command.brigadier + +/** + * Defers argument resolution until command execution. + * + * Used with [CustomArgumentType] to parse arguments into selectors at parse time, + * then resolve them to actual objects at execution time using the command source. + * + * @param T the resolved type + */ +fun interface ArgumentResolver { + /** + * Resolves the argument using the command [source]. + * + * @param source the command source + * @return the resolved value + */ + fun resolve(source: McBrigadierSource): T +} diff --git a/api/common/src/main/kotlin/su/plo/slib/api/command/brigadier/CustomArgumentType.kt b/api/common/src/main/kotlin/su/plo/slib/api/command/brigadier/CustomArgumentType.kt new file mode 100644 index 0000000..f93ed0e --- /dev/null +++ b/api/common/src/main/kotlin/su/plo/slib/api/command/brigadier/CustomArgumentType.kt @@ -0,0 +1,35 @@ +package su.plo.slib.api.command.brigadier + +import com.mojang.brigadier.arguments.ArgumentType +import org.jetbrains.annotations.ApiStatus + +/** + * An argument type that wraps a native argument type. + * + * The native type is sent to the client for client-side completions and syntax validation, + * while the server uses custom parsing logic to produce the parsed type. + * + * @param PARSED The custom type produced by server-side parsing + * @param NATIVE The native type sent to the client + */ +interface CustomArgumentType : ArgumentType { + /** + * The native argument type sent to the client. + */ + val nativeType: ArgumentType + + /** + * Whether native suggestions should be used. + * + * Set to `false` is you want to implement custom [listSuggestions]. + */ + fun useNativeSuggestions(): Boolean = + true + + /** + * This is controlled client-side and can't be changed server-side. + */ + @ApiStatus.NonExtendable + override fun getExamples(): Collection = + nativeType.examples +} diff --git a/api/common/src/main/kotlin/su/plo/slib/api/command/brigadier/McBrigadierSource.kt b/api/common/src/main/kotlin/su/plo/slib/api/command/brigadier/McBrigadierSource.kt new file mode 100644 index 0000000..3b2dad9 --- /dev/null +++ b/api/common/src/main/kotlin/su/plo/slib/api/command/brigadier/McBrigadierSource.kt @@ -0,0 +1,30 @@ +package su.plo.slib.api.command.brigadier + +import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.entity.McEntity + +interface McBrigadierSource { + /** + * Gets the command source that initiated/triggered the execution of a command. + */ + val source: McCommandSource + + /** + * Gets the entity executing this command. + */ + val executor: McEntity? + + /** + * Gets the server's implementation instance for this source. + * + * The return type may vary depending on the server platform: + * - For servers (Paper/Fabric/Forge/NeoForge): [net.minecraft.commands.CommandSourceStack] + * - For Minestom: [net.minestom.server.command.CommandSender] + * - For BungeeCord: [net.md_5.bungee.api.CommandSender] + * - For Velocity: [com.velocitypowered.api.command.CommandSource] + * + * @return The server's implementation object associated with this source. + * @param T The expected type of the server's implementation instance. + */ + fun getInstance(): T +} diff --git a/api/common/src/main/kotlin/su/plo/slib/api/service/LazyService.kt b/api/common/src/main/kotlin/su/plo/slib/api/service/LazyService.kt new file mode 100644 index 0000000..adb39d8 --- /dev/null +++ b/api/common/src/main/kotlin/su/plo/slib/api/service/LazyService.kt @@ -0,0 +1,14 @@ +package su.plo.slib.api.service + +import java.util.ServiceLoader + +inline fun lazyService(): Lazy = + lazy { + // some loaders can't find service by class's classloader, + // some can't find it by context class loader + // so we're just trying both + ServiceLoader.load(T::class.java).firstOrNull() + ?: ServiceLoader.load(T::class.java, T::class.java.classLoader).firstOrNull() + ?: throw IllegalStateException("${T::class.java} not found is classpath") + } + diff --git a/api/server/src/main/kotlin/su/plo/slib/api/server/McServerLib.kt b/api/server/src/main/kotlin/su/plo/slib/api/server/McServerLib.kt index 698292c..fe430d3 100644 --- a/api/server/src/main/kotlin/su/plo/slib/api/server/McServerLib.kt +++ b/api/server/src/main/kotlin/su/plo/slib/api/server/McServerLib.kt @@ -1,15 +1,15 @@ package su.plo.slib.api.server import su.plo.slib.api.McLib -import su.plo.slib.api.server.channel.McServerChannelManager import su.plo.slib.api.command.McCommand import su.plo.slib.api.command.McCommandManager -import su.plo.slib.api.server.entity.McServerEntity import su.plo.slib.api.entity.player.McGameProfile +import su.plo.slib.api.server.channel.McServerChannelManager +import su.plo.slib.api.server.entity.McServerEntity import su.plo.slib.api.server.entity.player.McServerPlayer import su.plo.slib.api.server.scheduler.McServerScheduler import su.plo.slib.api.server.world.McServerWorld -import java.util.* +import java.util.UUID interface McServerLib : McLib { @@ -113,7 +113,7 @@ interface McServerLib : McLib { * Creates a new [McServerEntity] instance of wrapped [instance]. * * The [instance] parameter represents the server-specific entity instance: - * - For Bukkit: [org.bukkit.entity.LivingEntity] + * - For Bukkit: [org.bukkit.entity.Entity] * - For modded servers (Fabric/Forge): [net.minecraft.world.entity.Entity] * * @return The entity. diff --git a/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McArgumentTypes.kt b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McArgumentTypes.kt new file mode 100644 index 0000000..6501195 --- /dev/null +++ b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McArgumentTypes.kt @@ -0,0 +1,64 @@ +package su.plo.slib.api.server.command.brigadier + +import com.mojang.brigadier.arguments.ArgumentType +import org.jetbrains.annotations.ApiStatus +import su.plo.slib.api.service.lazyService + +/** + * Vanilla Minecraft argument types. + */ +object McArgumentTypes { + + /** + * Returns an argument type that selects a single entity. + */ + @JvmStatic + fun entity(): ArgumentType = provider.entity() + + /** + * Returns an argument type that selects multiple entities. + */ + @JvmStatic + fun entities(): ArgumentType = provider.entities() + + /** + * Returns an argument type that selects a single player. + */ + @JvmStatic + fun player(): ArgumentType = provider.player() + + /** + * Returns an argument type that selects multiple players. + */ + @JvmStatic + fun players(): ArgumentType = provider.players() + + /** + * Returns an argument type that resolves multiple game profiles. + */ + @JvmStatic + fun gameProfiles(): ArgumentType = provider.gameProfiles() + + /** + * Returns an argument type that resolves position. + */ + @JvmStatic + fun position(): ArgumentType = provider.position() + + private val provider: Provider by lazyService() + + @ApiStatus.Internal + interface Provider { + fun entity(): ArgumentType + + fun entities(): ArgumentType + + fun player(): ArgumentType + + fun players(): ArgumentType + + fun gameProfiles(): ArgumentType + + fun position(): ArgumentType + } +} diff --git a/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McEntitiesArgumentResolver.kt b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McEntitiesArgumentResolver.kt new file mode 100644 index 0000000..b5eea69 --- /dev/null +++ b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McEntitiesArgumentResolver.kt @@ -0,0 +1,9 @@ +package su.plo.slib.api.server.command.brigadier + +import su.plo.slib.api.command.brigadier.ArgumentResolver +import su.plo.slib.api.server.entity.McServerEntity + +/** + * An [ArgumentResolver] that resolves multiple entities. + */ +fun interface McEntitiesArgumentResolver : ArgumentResolver> diff --git a/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McEntityArgumentResolver.kt b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McEntityArgumentResolver.kt new file mode 100644 index 0000000..fe25ff6 --- /dev/null +++ b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McEntityArgumentResolver.kt @@ -0,0 +1,9 @@ +package su.plo.slib.api.server.command.brigadier + +import su.plo.slib.api.command.brigadier.ArgumentResolver +import su.plo.slib.api.server.entity.McServerEntity + +/** + * An [ArgumentResolver] that resolves a single entity. + */ +fun interface McEntityArgumentResolver : ArgumentResolver diff --git a/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McGameProfilesArgumentResolver.kt b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McGameProfilesArgumentResolver.kt new file mode 100644 index 0000000..a3fa9f8 --- /dev/null +++ b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McGameProfilesArgumentResolver.kt @@ -0,0 +1,9 @@ +package su.plo.slib.api.server.command.brigadier + +import su.plo.slib.api.command.brigadier.ArgumentResolver +import su.plo.slib.api.entity.player.McGameProfile + +/** + * An [ArgumentResolver] that resolves multiple game profiles. + */ +fun interface McGameProfilesArgumentResolver : ArgumentResolver> diff --git a/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McPlayerArgumentResolver.kt b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McPlayerArgumentResolver.kt new file mode 100644 index 0000000..32e3af1 --- /dev/null +++ b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McPlayerArgumentResolver.kt @@ -0,0 +1,9 @@ +package su.plo.slib.api.server.command.brigadier + +import su.plo.slib.api.command.brigadier.ArgumentResolver +import su.plo.slib.api.server.entity.player.McServerPlayer + +/** + * An [ArgumentResolver] that resolves a single player. + */ +fun interface McPlayerArgumentResolver : ArgumentResolver diff --git a/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McPlayersArgumentResolver.kt b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McPlayersArgumentResolver.kt new file mode 100644 index 0000000..f770825 --- /dev/null +++ b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/McPlayersArgumentResolver.kt @@ -0,0 +1,9 @@ +package su.plo.slib.api.server.command.brigadier + +import su.plo.slib.api.command.brigadier.ArgumentResolver +import su.plo.slib.api.server.entity.player.McServerPlayer + +/** + * An [ArgumentResolver] that resolves multiple players. + */ +fun interface McPlayersArgumentResolver : ArgumentResolver> diff --git a/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/ServerPos3dResolver.kt b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/ServerPos3dResolver.kt new file mode 100644 index 0000000..5b09e24 --- /dev/null +++ b/api/server/src/main/kotlin/su/plo/slib/api/server/command/brigadier/ServerPos3dResolver.kt @@ -0,0 +1,9 @@ +package su.plo.slib.api.server.command.brigadier + +import su.plo.slib.api.command.brigadier.ArgumentResolver +import su.plo.slib.api.server.position.ServerPos3d + +/** + * An [ArgumentResolver] that resolves [ServerPos3d]. + */ +fun interface ServerPos3dResolver : ArgumentResolver diff --git a/api/server/src/main/kotlin/su/plo/slib/api/server/position/ServerPos3d.kt b/api/server/src/main/kotlin/su/plo/slib/api/server/position/ServerPos3d.kt index b23becd..95d2603 100644 --- a/api/server/src/main/kotlin/su/plo/slib/api/server/position/ServerPos3d.kt +++ b/api/server/src/main/kotlin/su/plo/slib/api/server/position/ServerPos3d.kt @@ -87,4 +87,7 @@ class ServerPos3d @JvmOverloads constructor( return pos } + + override fun toString(): String = + "ServerPos3d(world=${worldReference.get()}, x=$x, y=$y, z=$z, yaw=$yaw, pitch=$pitch)" } diff --git a/build.gradle.kts b/build.gradle.kts index 1fc0616..039c189 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,7 @@ subprojects { implementation(rootProject.libs.kotlinx.coroutines.jdk8) implementation(rootProject.libs.guava) + api(rootProject.libs.brigadier) } tasks { @@ -105,6 +106,8 @@ allprojects { maven("https://oss.sonatype.org/content/repositories/snapshots") + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.plasmoverse.com/snapshots") maven("https://repo.plo.su") diff --git a/bungee/build.gradle.kts b/bungee/build.gradle.kts index 677ed72..8b26616 100644 --- a/bungee/build.gradle.kts +++ b/bungee/build.gradle.kts @@ -1,7 +1,13 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.kotlin.dsl.register + plugins { id("su.plo.slib.shadow-platform") + alias(libs.plugins.run.waterfall) } +val testShadowBundle: Configuration by configurations.creating + repositories { maven("https://repo.codemc.org/repository/maven-public/") maven("https://repo.papermc.io/repository/maven-public/") @@ -12,6 +18,9 @@ dependencies { testCompileOnly(libs.bungee.api) compileOnly(libs.bungee.proxy) + testCompileOnly(testFixtures(project(":common-proxy"))) + testShadowBundle(testFixtures(project(":common-proxy"))) + compileOnly(project(":common")) compileOnly(project(":common-integration")) listOf( @@ -24,6 +33,7 @@ dependencies { } compileOnly(libs.adventure.bungee) + testCompileOnly(libs.adventure.bungee) shadow(libs.adventure.bungee) { exclude("org.jetbrains", "annotations") exclude("net.kyori", "adventure-api") @@ -36,6 +46,12 @@ dependencies { } } +runWaterfallExtension { + disablePluginJarDetection() +} + +java.toolchain.languageVersion.set(JavaLanguageVersion.of(11)) + tasks { shadowJar { archiveClassifier = "all" @@ -53,6 +69,27 @@ tasks { from(project(":common-integration").sourceSets.main.get().output) } + val testJar = + register("testJar", ShadowJar::class) { + configurations = listOf(testShadowBundle) + + archiveClassifier.set("test") + + relocate("net.kyori", "su.plo.slib.libs.adventure") + + from(zipTree(finalJar.get().archiveFile)) + from(sourceSets.test.get().output) + } + + runWaterfall { + waterfallVersion("1.21") + pluginJars.from(testJar) + + javaLauncher = project.javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(21) + } + } + build { dependsOn(finalJar) } diff --git a/bungee/src/main/kotlin/su/plo/slib/bungee/chat/ComponentToMessageConverter.kt b/bungee/src/main/kotlin/su/plo/slib/bungee/chat/ComponentToMessageConverter.kt new file mode 100644 index 0000000..77b0044 --- /dev/null +++ b/bungee/src/main/kotlin/su/plo/slib/bungee/chat/ComponentToMessageConverter.kt @@ -0,0 +1,10 @@ +package su.plo.slib.bungee.chat + +import com.mojang.brigadier.Message +import su.plo.slib.api.chat.component.McTextComponent +import su.plo.slib.api.chat.converter.MessageTextConverter + +class ComponentToMessageConverter : MessageTextConverter { + override fun convert(text: McTextComponent): Message = + McTextMessage(text) +} diff --git a/bungee/src/main/kotlin/su/plo/slib/bungee/chat/McTextMessage.kt b/bungee/src/main/kotlin/su/plo/slib/bungee/chat/McTextMessage.kt new file mode 100644 index 0000000..7830e4d --- /dev/null +++ b/bungee/src/main/kotlin/su/plo/slib/bungee/chat/McTextMessage.kt @@ -0,0 +1,10 @@ +package su.plo.slib.bungee.chat + +import com.mojang.brigadier.Message +import su.plo.slib.api.chat.component.McTextComponent + +class McTextMessage( + val component: McTextComponent, +) : Message { + override fun getString(): String = component.toString() +} diff --git a/bungee/src/main/kotlin/su/plo/slib/bungee/command/BungeeCommandManager.kt b/bungee/src/main/kotlin/su/plo/slib/bungee/command/BungeeCommandManager.kt index aeb0b99..37253af 100644 --- a/bungee/src/main/kotlin/su/plo/slib/bungee/command/BungeeCommandManager.kt +++ b/bungee/src/main/kotlin/su/plo/slib/bungee/command/BungeeCommandManager.kt @@ -7,15 +7,17 @@ import net.md_5.bungee.api.event.ChatEvent import net.md_5.bungee.api.plugin.Listener import net.md_5.bungee.api.plugin.Plugin import net.md_5.bungee.event.EventHandler -import su.plo.slib.api.command.McCommandManager import su.plo.slib.api.command.McCommandSource import su.plo.slib.api.proxy.command.McProxyCommand import su.plo.slib.api.proxy.event.command.McProxyCommandExecuteEvent import su.plo.slib.bungee.BungeeProxyLib +import su.plo.slib.bungee.command.brigadier.BungeeBrigadierCommand +import su.plo.slib.command.AbstractCommandManager +import su.plo.slib.command.proxied class BungeeCommandManager( private val minecraftProxy: BungeeProxyLib -) : McCommandManager(), Listener { +) : AbstractCommandManager(), Listener { @EventHandler fun onChat(event: ChatEvent) { @@ -28,9 +30,17 @@ class BungeeCommandManager( @Synchronized fun registerCommands(plugin: Plugin, proxyServer: ProxyServer) { - commandByName.forEach { (name: String, command: McProxyCommand) -> + registerCommands { name, command -> proxyServer.pluginManager.registerCommand(plugin, BungeeCommand(minecraftProxy, this, command, name)) } + + registerBrigadierCommands { command -> + proxyServer.pluginManager.registerCommand( + plugin, + BungeeBrigadierCommand(this, command.proxied({ it }, { it })), + ) + } + registered = true } diff --git a/bungee/src/main/kotlin/su/plo/slib/bungee/command/brigadier/BungeeBrigadierCommand.kt b/bungee/src/main/kotlin/su/plo/slib/bungee/command/brigadier/BungeeBrigadierCommand.kt new file mode 100644 index 0000000..ee25a14 --- /dev/null +++ b/bungee/src/main/kotlin/su/plo/slib/bungee/command/brigadier/BungeeBrigadierCommand.kt @@ -0,0 +1,63 @@ +package su.plo.slib.bungee.command.brigadier + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.tree.LiteralCommandNode +import net.md_5.bungee.api.CommandSender +import net.md_5.bungee.api.plugin.Command +import net.md_5.bungee.api.plugin.TabExecutor +import su.plo.slib.api.chat.component.McTextComponent +import su.plo.slib.api.chat.style.McTextStyle +import su.plo.slib.api.command.brigadier.McBrigadierSource +import su.plo.slib.bungee.chat.McTextMessage +import su.plo.slib.bungee.command.BungeeCommandManager + +class BungeeBrigadierCommand( + private val commandManager: BungeeCommandManager, + private val command: LiteralCommandNode, +) : Command(command.literal), TabExecutor { + private val dispatcher = CommandDispatcher() + + init { + dispatcher.root.addChild(command) + } + + override fun execute(sender: CommandSender, arguments: Array) { + val context = BungeeBrigadierSource(commandManager.getCommandSource(sender), instance = sender) + val input = listOf(command.literal, *arguments).joinToString(" ") + + try { + dispatcher.execute(input, context) + } catch (e: CommandSyntaxException) { + val rawMessage = e.rawMessage + val messageArg = + if (rawMessage is McTextMessage) rawMessage.component + else McTextComponent.literal(rawMessage.string) + + context.source.sendMessage( + McTextComponent.translatable( + "command.context.parse_error", + messageArg, + McTextComponent.literal(e.cursor.toString()), + McTextComponent.literal(e.context), + ).withStyle(McTextStyle.RED) + ) + } catch (e: Exception) { + context.source.sendMessage( + McTextComponent.literal(e.message ?: "Unknown error").withStyle(McTextStyle.RED) + ) + } + } + + override fun onTabComplete(sender: CommandSender, arguments: Array): Iterable { + val context = BungeeBrigadierSource(commandManager.getCommandSource(sender), instance = sender) + val input = listOf(command.literal, *arguments).joinToString(" ") + + return dispatcher.getCompletionSuggestions( + dispatcher.parse(input, context), + input.length, + ) + .thenApply { suggestions -> suggestions.list.map { it.text } } + .get() + } +} diff --git a/bungee/src/main/kotlin/su/plo/slib/bungee/command/brigadier/BungeeBrigadierSource.kt b/bungee/src/main/kotlin/su/plo/slib/bungee/command/brigadier/BungeeBrigadierSource.kt new file mode 100644 index 0000000..7e44eae --- /dev/null +++ b/bungee/src/main/kotlin/su/plo/slib/bungee/command/brigadier/BungeeBrigadierSource.kt @@ -0,0 +1,16 @@ +package su.plo.slib.bungee.command.brigadier + +import net.md_5.bungee.api.CommandSender +import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.command.brigadier.McBrigadierSource +import su.plo.slib.api.entity.McEntity + +data class BungeeBrigadierSource( + override val source: McCommandSource, + override val executor: McEntity? = null, + private val instance: CommandSender, +) : McBrigadierSource { + @Suppress("UNCHECKED_CAST") + override fun getInstance(): T = + instance as T +} diff --git a/bungee/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter b/bungee/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter new file mode 100644 index 0000000..8898785 --- /dev/null +++ b/bungee/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter @@ -0,0 +1 @@ +su.plo.slib.bungee.chat.ComponentToMessageConverter diff --git a/bungee/src/test/kotlin/BungeePlugin.kt b/bungee/src/test/kotlin/BungeePlugin.kt deleted file mode 100644 index e42ea1c..0000000 --- a/bungee/src/test/kotlin/BungeePlugin.kt +++ /dev/null @@ -1,20 +0,0 @@ -import net.md_5.bungee.api.plugin.Plugin -import su.plo.slib.api.proxy.event.command.McProxyCommandsRegisterEvent -import su.plo.slib.bungee.BungeeProxyLib - -class BungeePlugin : Plugin() { - - init { - McProxyCommandsRegisterEvent.registerListener { commandManager, minecraftServer -> - // register commands here - // commandManager.register("pepega", PepegaCommand()) - } - } - - private lateinit var minecraftServer: BungeeProxyLib - - override fun onEnable() { - // you need to initialize lib here - minecraftServer = BungeeProxyLib(this) - } -} diff --git a/bungee/src/test/kotlin/su/plo/slib/bungee/TestBungeePlugin.kt b/bungee/src/test/kotlin/su/plo/slib/bungee/TestBungeePlugin.kt new file mode 100644 index 0000000..90e62d8 --- /dev/null +++ b/bungee/src/test/kotlin/su/plo/slib/bungee/TestBungeePlugin.kt @@ -0,0 +1,12 @@ +package su.plo.slib.bungee + +import net.md_5.bungee.api.plugin.Plugin +import su.plo.slib.proxy.TestProxy + +class TestBungeePlugin : Plugin() { + private val testProxy = TestProxy() + + override fun onEnable() { + val minecraftServer = BungeeProxyLib(this) + } +} diff --git a/bungee/src/test/resources/bungee.yml b/bungee/src/test/resources/bungee.yml new file mode 100644 index 0000000..0ea24b3 --- /dev/null +++ b/bungee/src/test/resources/bungee.yml @@ -0,0 +1,3 @@ +name: slib-bungee-test +version: dev +main: su.plo.slib.bungee.TestBungeePlugin diff --git a/common-proxy/build.gradle.kts b/common-proxy/build.gradle.kts new file mode 100644 index 0000000..c1f9156 --- /dev/null +++ b/common-proxy/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + `java-test-fixtures` +} + +dependencies { + testFixturesImplementation(kotlin("stdlib-jdk8")) + + testFixturesImplementation(project(":api:api-common")) + testFixturesImplementation(project(":api:api-proxy")) + + testFixturesImplementation(libs.adventure.api) +} diff --git a/common-proxy/src/testFixtures/kotlin/su/plo/slib/proxy/TestProxy.kt b/common-proxy/src/testFixtures/kotlin/su/plo/slib/proxy/TestProxy.kt new file mode 100644 index 0000000..2bbe458 --- /dev/null +++ b/common-proxy/src/testFixtures/kotlin/su/plo/slib/proxy/TestProxy.kt @@ -0,0 +1,78 @@ +package su.plo.slib.proxy + +import com.mojang.brigadier.Command +import net.kyori.adventure.key.Key +import net.kyori.adventure.translation.GlobalTranslator +import net.kyori.adventure.translation.TranslationStore +import su.plo.slib.api.command.McCommandManager +import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.event.player.McPlayerJoinEvent +import su.plo.slib.api.event.player.McPlayerQuitEvent +import su.plo.slib.api.logging.McLoggerFactory +import su.plo.slib.api.proxy.command.McProxyCommand +import su.plo.slib.api.proxy.event.command.McProxyCommandsRegisterEvent +import su.plo.slib.proxy.command.UuidArgumentType +import java.text.MessageFormat +import java.util.Locale +import java.util.UUID + +class TestProxy { + private var logger = McLoggerFactory.createLogger("TestProxy") + + init { + McPlayerJoinEvent.registerListener { player -> + logger.info("Player ${player.name} joined the server") + } + + McPlayerQuitEvent.registerListener { player -> + logger.info("Player ${player.name} quit the server") + } + + McProxyCommandsRegisterEvent.registerListener { commands, minecraftProxy -> + commands.register("ping", object : McProxyCommand { + override fun execute( + source: McCommandSource, + arguments: Array, + ) { + source.sendMessage("Pong") + } + }) + + commands.register( + McCommandManager.literal("brigadier-ping") + .executes { + val source = it.source.source + source.sendMessage("Pong") + + Command.SINGLE_SUCCESS + } + ) + + commands.register( + McCommandManager.literal("brigadier-custom-type") + .then( + McCommandManager.argument("uuid", UuidArgumentType()) + .executes { + val uuid = it.getArgument("uuid", UUID::class.java) + + it.source.source.sendMessage(uuid.toString()) + + Command.SINGLE_SUCCESS + } + ) + ) + } + + registerVanillaTranslations() + } + + // Bungee/Velocity doesn't ship vanilla translations, so translation keys leak to the console as raw ids + // Register the minimum set the smoke tests need + private fun registerVanillaTranslations() { + val store = TranslationStore.messageFormat(Key.key("slib", "test")) + store.defaultLocale(Locale.US) + store.register("command.context.parse_error", Locale.US, MessageFormat("{0} at position {1}: {2}")) + store.register("argument.uuid.invalid", Locale.US, MessageFormat("Invalid UUID")) + GlobalTranslator.translator().addSource(store) + } +} diff --git a/common-proxy/src/testFixtures/kotlin/su/plo/slib/proxy/command/UuidArgumentType.kt b/common-proxy/src/testFixtures/kotlin/su/plo/slib/proxy/command/UuidArgumentType.kt new file mode 100644 index 0000000..114a04c --- /dev/null +++ b/common-proxy/src/testFixtures/kotlin/su/plo/slib/proxy/command/UuidArgumentType.kt @@ -0,0 +1,43 @@ +package su.plo.slib.proxy.command + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import su.plo.slib.api.chat.component.McTextComponent +import su.plo.slib.api.chat.converter.MessageTextConverter +import su.plo.slib.api.command.brigadier.CustomArgumentType +import java.util.UUID +import java.util.concurrent.CompletableFuture + +class UuidArgumentType : CustomArgumentType { + override val nativeType: ArgumentType = StringArgumentType.string() + + override fun useNativeSuggestions(): Boolean = false + + private val invalidUuid = SimpleCommandExceptionType( + MessageTextConverter.converter().convert( + McTextComponent.translatable( + "argument.uuid.invalid", + ) + ) + ) + + override fun parse(reader: StringReader): UUID { + val input = reader.readString() + + try { + return UUID.fromString(input) + } catch (_: IllegalArgumentException) { + throw invalidUuid.createWithContext(reader) + } + } + + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture = Suggestions.empty() +} diff --git a/common-server/src/testFixtures/kotlin/su/plo/slib/server/TestServer.kt b/common-server/src/testFixtures/kotlin/su/plo/slib/server/TestServer.kt index 1c16f7e..9cfb85d 100644 --- a/common-server/src/testFixtures/kotlin/su/plo/slib/server/TestServer.kt +++ b/common-server/src/testFixtures/kotlin/su/plo/slib/server/TestServer.kt @@ -1,18 +1,30 @@ package su.plo.slib.server +import com.mojang.brigadier.Command +import com.mojang.brigadier.arguments.IntegerArgumentType import su.plo.slib.api.command.McCommand +import su.plo.slib.api.command.McCommandManager import su.plo.slib.api.command.McCommandSource import su.plo.slib.api.event.player.McPlayerJoinEvent import su.plo.slib.api.event.player.McPlayerQuitEvent import su.plo.slib.api.logging.McLoggerFactory import su.plo.slib.api.server.McServerLib +import su.plo.slib.api.server.command.brigadier.McArgumentTypes +import su.plo.slib.api.server.command.brigadier.McEntitiesArgumentResolver +import su.plo.slib.api.server.command.brigadier.McEntityArgumentResolver +import su.plo.slib.api.server.command.brigadier.McGameProfilesArgumentResolver +import su.plo.slib.api.server.command.brigadier.McPlayerArgumentResolver +import su.plo.slib.api.server.command.brigadier.McPlayersArgumentResolver +import su.plo.slib.api.server.command.brigadier.ServerPos3dResolver import su.plo.slib.api.server.event.command.McServerCommandsRegisterEvent import su.plo.slib.api.server.event.player.McPlayerRegisterChannelsEvent +import su.plo.slib.server.command.UuidArgumentType +import java.util.UUID class TestServer( val minecraftServer: McServerLib, ) { - private var logger = McLoggerFactory.createLogger("TestMod") + private var logger = McLoggerFactory.createLogger("TestServer") val channelKey = "slib:channels/test" @@ -30,15 +42,163 @@ class TestServer( } McServerCommandsRegisterEvent.registerListener { commands, _ -> - commands.register("ping", object : McCommand { - override fun execute( - source: McCommandSource, - arguments: Array, - ) { - source.sendMessage("Pong") - } - }) - logger.info("Command 'ping' registered") + commands.register( + "ping", + object : McCommand { + override fun execute( + source: McCommandSource, + arguments: Array, + ) { + source.sendMessage("Pong") + } + }, + ) + + commands.register( + McCommandManager.literal("brigadier-custom-type") + .then( + McCommandManager.argument("uuid", UuidArgumentType()) + .executes { + val uuid = it.getArgument("uuid", UUID::class.java) + + it.source.source.sendMessage(uuid.toString()) + + Command.SINGLE_SUCCESS + } + ) + ) + + commands.register( + McCommandManager.literal("brigadier-entity-selector") + .then( + McCommandManager.literal("entity") + .then( + McCommandManager.argument("target", McArgumentTypes.entity()) + .executes { + val resolver = it.getArgument( + "target", + McEntityArgumentResolver::class.java, + ) + val entity = resolver.resolve(it.source) + + val source = it.source + source.source.sendMessage("Found entity: $entity; Source: ${source.source}; Executor: ${source.executor}") + + Command.SINGLE_SUCCESS + } + ) + ) + .then( + McCommandManager.literal("entities") + .then( + McCommandManager.argument("target", McArgumentTypes.entities()) + .executes { + val resolver = it.getArgument( + "target", + McEntitiesArgumentResolver::class.java, + ) + val entities = resolver.resolve(it.source) + + val source = it.source + source.source.sendMessage("Found entities: $entities; Source: ${source.source}; Executor: ${source.executor}") + + Command.SINGLE_SUCCESS + } + ) + ) + .then( + McCommandManager.literal("player") + .then( + McCommandManager.argument("target", McArgumentTypes.player()) + .executes { + val resolver = it.getArgument( + "target", + McPlayerArgumentResolver::class.java, + ) + val player = resolver.resolve(it.source) + + val source = it.source + source.source.sendMessage("Found player: $player; Source: ${source.source}; Executor: ${source.executor}") + + Command.SINGLE_SUCCESS + } + ) + ) + .then( + McCommandManager.literal("players") + .then( + McCommandManager.argument("target", McArgumentTypes.players()) + .executes { + val resolver = it.getArgument( + "target", + McPlayersArgumentResolver::class.java, + ) + val players = resolver.resolve(it.source) + + val source = it.source + source.source.sendMessage("Found players: $players; Source: ${source.source}; Executor: ${source.executor}") + + Command.SINGLE_SUCCESS + } + ) + ) + ) + + commands.register( + McCommandManager.literal("brigadier-game-profiles-selector") + .then( + McCommandManager.argument("targets", McArgumentTypes.gameProfiles()) + .executes { + val resolver = it.getArgument( + "targets", + McGameProfilesArgumentResolver::class.java, + ) + val gameProfiles = resolver.resolve(it.source) + + val source = it.source + source.source.sendMessage("Found game profiles: $gameProfiles; Source: ${source.source}; Executor: ${source.executor}") + + Command.SINGLE_SUCCESS + } + ) + ) + + commands.register( + McCommandManager.literal("brigadier-position-selector") + .then( + McCommandManager.argument("position", McArgumentTypes.position()) + .executes { + val resolver = it.getArgument( + "position", + ServerPos3dResolver::class.java, + ) + val position = resolver.resolve(it.source) + + val source = it.source + source.source.sendMessage("Position: $position; Source: ${source.source}; Executor: ${source.executor}") + + Command.SINGLE_SUCCESS + } + ) + ) + + commands.register( + McCommandManager.literal("brigadier-multi-arg") + .then( + McCommandManager.argument("a", IntegerArgumentType.integer()) + .then( + McCommandManager.argument("b", IntegerArgumentType.integer()) + .executes { + val a = it.getArgument("a", Integer::class.java) + val b = it.getArgument("b", Integer::class.java) + + it.source.source.sendMessage("Multi-arg: a=$a, b=$b") + + Command.SINGLE_SUCCESS + } + ) + ) + ) } } diff --git a/common-server/src/testFixtures/kotlin/su/plo/slib/server/command/UuidArgumentType.kt b/common-server/src/testFixtures/kotlin/su/plo/slib/server/command/UuidArgumentType.kt new file mode 100644 index 0000000..228a505 --- /dev/null +++ b/common-server/src/testFixtures/kotlin/su/plo/slib/server/command/UuidArgumentType.kt @@ -0,0 +1,43 @@ +package su.plo.slib.server.command + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import su.plo.slib.api.chat.component.McTextComponent +import su.plo.slib.api.chat.converter.MessageTextConverter +import su.plo.slib.api.command.brigadier.CustomArgumentType +import java.util.UUID +import java.util.concurrent.CompletableFuture + +class UuidArgumentType : CustomArgumentType { + override val nativeType: ArgumentType = StringArgumentType.string() + + override fun useNativeSuggestions(): Boolean = false + + private val invalidUuid = SimpleCommandExceptionType( + MessageTextConverter.converter().convert( + McTextComponent.translatable( + "argument.uuid.invalid", + ) + ) + ) + + override fun parse(reader: StringReader): UUID { + val input = reader.readString() + + try { + return UUID.fromString(input) + } catch (_: IllegalArgumentException) { + throw invalidUuid.createWithContext(reader) + } + } + + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture = Suggestions.empty() +} diff --git a/common/src/main/kotlin/su/plo/slib/command/AbstractCommandManager.kt b/common/src/main/kotlin/su/plo/slib/command/AbstractCommandManager.kt new file mode 100644 index 0000000..403c2d2 --- /dev/null +++ b/common/src/main/kotlin/su/plo/slib/command/AbstractCommandManager.kt @@ -0,0 +1,157 @@ +package su.plo.slib.command + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.common.collect.Maps +import com.mojang.brigadier.RedirectModifier +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.tree.ArgumentCommandNode +import com.mojang.brigadier.tree.CommandNode +import com.mojang.brigadier.tree.LiteralCommandNode +import su.plo.slib.api.command.McCommand +import su.plo.slib.api.command.McCommandManager +import su.plo.slib.api.command.brigadier.CustomArgumentType +import su.plo.slib.api.command.brigadier.McBrigadierSource +import su.plo.slib.api.logging.McLoggerFactory +import su.plo.slib.command.brigadier.buildCustom + +abstract class AbstractCommandManager : McCommandManager() { + private val logger = McLoggerFactory.createLogger("CommandManager") + + protected val commandByName: MutableMap = Maps.newHashMap() + protected var brigadierCommands: MutableList> = mutableListOf() + + protected var registered = false + + @get:Synchronized + override val registeredCommands: Map + get() = ImmutableMap.copyOf(commandByName) + + @get:Synchronized + override val registeredBrigadierCommands: List> + get() = ImmutableList.copyOf(brigadierCommands) + + @Synchronized + override fun register(command: LiteralCommandNode) { + check(!registered) { "register after commands registration is not supported" } + require(brigadierCommands.none { it.literal == command.literal }) { "Command with name '${command.literal}' already exist" } + + brigadierCommands.add(command) + } + + @Synchronized + override fun register(name: String, command: T, vararg aliases: String) { + check(!registered) { "register after commands registration is not supported" } + require(!commandByName.containsKey(name)) { "Command with name '$name' already exist" } + + for (alias in aliases) { + require(!commandByName.containsKey(alias)) { "Command with name '$alias' already exist" } + } + + commandByName[name] = command + for (alias in aliases) { + commandByName[alias] = command + } + } + + @Synchronized + override fun clear() { + commandByName.clear() + brigadierCommands.clear() + registered = false + } + + protected fun registerCommands(register: (String, T) -> Unit) { + commandByName.forEach { (name, command) -> + register(name, command) + logger.info("Command '$name' registered") + } + } + + protected fun registerBrigadierCommands(registerCommand: (LiteralCommandNode) -> Unit) { + brigadierCommands.forEach { command -> + registerCommand(command) + logger.info("Command '${command.literal}' registered") + } + } +} + +@Suppress("UNCHECKED_CAST") +fun CommandContext.copyFor(source: T): CommandContext = + (this as CommandContext).copyFor(source) + +fun LiteralCommandNode.proxied( + sourceFactory: (S) -> McBrigadierSource, + contextFactory: (CommandContext) -> CommandContext, +): LiteralCommandNode = + toProxyNode(sourceFactory, contextFactory) as LiteralCommandNode + +fun CommandNode.toProxyNode( + sourceFactory: (S) -> McBrigadierSource, + contextFactory: (CommandContext) -> CommandContext, +): CommandNode { + val node = + when (this) { + is LiteralCommandNode -> LiteralArgumentBuilder.literal(literal) + is ArgumentCommandNode -> + RequiredArgumentBuilder.argument(name, type as ArgumentType) + else -> throw IllegalArgumentException("Unsupported command node: $this") + } + + redirect?.let { redirect -> + val modifier = redirectModifier + + if (modifier == null) { + node.redirect(redirect.toProxyNode(sourceFactory, contextFactory)) + } else { + val proxiedModifier = RedirectModifier { context -> + val context = contextFactory(context) + modifier.apply(context).map { it.getInstance() } + } + + node.fork( + redirect.toProxyNode(sourceFactory, contextFactory), + proxiedModifier, + ) + } + } + + children + .map { it.toProxyNode(sourceFactory, contextFactory) } + .forEach { node.then(it) } + + requirement?.let { requirement -> + node.requires { sourceStack -> + val source = sourceFactory(sourceStack) + requirement.test(source) + } + } + + command?.let { command -> + node.executes { context -> + val context = contextFactory(context) + command.run(context) + } + } + + if (this is ArgumentCommandNode) { + val node = node as RequiredArgumentBuilder + if (this.customSuggestions != null) { + node.suggests { context, builder -> + val context = contextFactory(context) + listSuggestions(context, builder) + } + } + } + + if (node is RequiredArgumentBuilder && node.type is CustomArgumentType<*, *>) { + @Suppress("UNCHECKED_CAST") + return (node as RequiredArgumentBuilder).buildCustom() + } + + return node.build() +} diff --git a/common/src/main/kotlin/su/plo/slib/command/brigadier/CustomArgumentCommandNode.kt b/common/src/main/kotlin/su/plo/slib/command/brigadier/CustomArgumentCommandNode.kt new file mode 100644 index 0000000..9715ce9 --- /dev/null +++ b/common/src/main/kotlin/su/plo/slib/command/brigadier/CustomArgumentCommandNode.kt @@ -0,0 +1,79 @@ +package su.plo.slib.command.brigadier + +import com.mojang.brigadier.Command +import com.mojang.brigadier.RedirectModifier +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.context.CommandContextBuilder +import com.mojang.brigadier.context.ParsedArgument +import com.mojang.brigadier.suggestion.SuggestionProvider +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import com.mojang.brigadier.tree.ArgumentCommandNode +import com.mojang.brigadier.tree.CommandNode +import su.plo.slib.api.command.brigadier.CustomArgumentType +import java.util.concurrent.CompletableFuture +import java.util.function.Predicate + +class CustomArgumentCommandNode( + name: String, + val customArgumentType: CustomArgumentType, + command: Command, + requirement: Predicate, + redirect: CommandNode?, + modifier: RedirectModifier?, + forks: Boolean, + customSuggestions: SuggestionProvider?, +) : ArgumentCommandNode( + name, + customArgumentType.nativeType, + command, + requirement, + redirect, + modifier, + forks, + customSuggestions, +) { + override fun parse(reader: StringReader, contextBuilder: CommandContextBuilder) { + val start = reader.cursor + val result = customArgumentType.parse(reader) + ?: error("CustomArgumentType ${customArgumentType::class.java.name} returned null from parse; throw a CommandSyntaxException to signal a parse failure") + + val parsed = ParsedArgument(start, reader.cursor, result) + + contextBuilder.withArgument(name, parsed) + contextBuilder.withNode(this, parsed.range) + } +} + +fun RequiredArgumentBuilder.buildCustom(): CustomArgumentCommandNode { + @Suppress("UNCHECKED_CAST") + val type = type as CustomArgumentType + + val result = CustomArgumentCommandNode( + name, + type, + command, + requirement, + redirect, + redirectModifier, + isFork, + suggestionsProvider ?: + if (!type.useNativeSuggestions()) { + object : SuggestionProvider { + override fun getSuggestions( + context: CommandContext, + builder: SuggestionsBuilder, + ): CompletableFuture = + type.listSuggestions(context, builder) + } + } else { + null + }, + ) + + arguments.forEach { result.addChild(it) } + + return result +} diff --git a/gradle.properties b/gradle.properties index 15804ff..f8a7c9d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ # Version group=su.plo.slib -version=1.2.1 +version=1.3.0 # Gradle args org.gradle.jvmargs=-Xmx4G -Dkotlin.daemon.jvm.options=-Xmx512M diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 07dff40..69f0f52 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,58 +1,87 @@ [versions] +# kotlin kotlin = "2.3.10" kotlinx-coroutines = "1.10.2" -dokka = "2.1.0" -runpaper = "3.0.2" -architectury = "1.15.48" +# build tooling +dokka = "2.1.0" shadow = "9.3.2" asm = "9.9.1" +architectury = "1.15.48" +fletching-table = "0.1.0-alpha.22" +run-task = "3.0.2" +# common libraries annotations = "23.0.0" guava = "33.3.1-jre" slf4j = "1.7.30" semver4j = "6.0.0" +# platforms spigot = "1.16.5-R0.1-SNAPSHOT" folia = "1.20.1-R0.1-SNAPSHOT" velocity = "3.1.1" bungee = "1.21-R0.4-SNAPSHOT" +minestom = "2025.10.05-1.21.8" -minestom = "39d445482f" - +# minecraft libraries adventure = "4.21.0" -adventure-platform = "4.4.0" +adventure-platform = "4.4.1" +brigadier = "1.0.18" fabric-permissions = "0.3.1" +reflection-remapper = "0.1.2" + +# test runtime +log4j = "2.25.3" +slf4j-log4j = "2.0.17" +jline = "3.21.0" +terminalconsoleappender = "1.3.0" [libraries] -kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +# kotlin +kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "kotlinx-coroutines" } -annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" } -guava = { module = "com.google.guava:guava", version.ref = "guava" } -slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } -semver4j = { module = "org.semver4j:semver4j", version.ref = "semver4j" } +# builld tooling +shadow = { module = "com.gradleup.shadow:com.gradleup.shadow.gradle.plugin", version.ref = "shadow" } +asm = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } -spigot = { module = "org.spigotmc:spigot-api", version.ref = "spigot" } -folia = { module = "dev.folia:folia-api", version.ref = "folia" } -velocity = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" } -bungee-api = { module = "net.md-5:bungeecord-api", version.ref = "bungee" } -bungee-proxy = { module = "net.md-5:bungeecord-proxy", version.ref = "bungee" } +# common libraries +annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } +semver4j = { module = "org.semver4j:semver4j", version.ref = "semver4j" } -minestom = { module = "net.minestom:minestom-snapshots", version.ref = "minestom" } +# platforms +spigot = { module = "org.spigotmc:spigot-api", version.ref = "spigot" } +folia = { module = "dev.folia:folia-api", version.ref = "folia" } +velocity = { module = "com.velocitypowered:velocity-api", version.ref = "velocity" } +bungee-api = { module = "net.md-5:bungeecord-api", version.ref = "bungee" } +bungee-proxy = { module = "net.md-5:bungeecord-proxy", version.ref = "bungee" } +minestom = { module = "net.minestom:minestom", version.ref = "minestom" } -adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure" } -adventure-gson = { module = "net.kyori:adventure-text-serializer-gson", version.ref = "adventure" } -adventure-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure" } +# minecraft libraries +adventure-api = { module = "net.kyori:adventure-api", version.ref = "adventure" } +adventure-gson = { module = "net.kyori:adventure-text-serializer-gson", version.ref = "adventure" } +adventure-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure" } adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" } -adventure-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } -adventure-bungee = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" } -fabric-permissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabric-permissions" } +adventure-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" } +adventure-bungee = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" } +brigadier = { module = "com.mojang:brigadier", version.ref = "brigadier" } +fabric-permissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabric-permissions" } +reflectionremapper = { module = "xyz.jpenilla:reflection-remapper", version.ref = "reflection-remapper" } -shadow = { module = "com.gradleup.shadow:com.gradleup.shadow.gradle.plugin", version.ref = "shadow" } -asm = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } +# test runtime +log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } +slf4j-log4j12 = { module = "org.slf4j:slf4j-log4j12", version.ref = "slf4j-log4j" } +jline-reader = { module = "org.jline:jline-reader", version.ref = "jline" } +jline-terminal = { module = "org.jline:jline-terminal", version.ref = "jline" } +terminalconsoleappender = { module = "net.minecrell:terminalconsoleappender", version.ref = "terminalconsoleappender" } [plugins] -architectury = { id = "gg.essential.loom", version.ref = "architectury" } -dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } -runpaper = { id = "xyz.jpenilla.run-paper", version.ref = "runpaper"} +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } +architectury = { id = "gg.essential.loom", version.ref = "architectury" } +fletchingtable = { id = "dev.kikugie.fletching-table", version.ref = "fletching-table" } +run-paper = { id = "xyz.jpenilla.run-paper", version.ref = "run-task" } +run-velocity = { id = "xyz.jpenilla.run-velocity", version.ref = "run-task" } +run-waterfall = { id = "xyz.jpenilla.run-waterfall", version.ref = "run-task" } diff --git a/minestom/build.gradle.kts b/minestom/build.gradle.kts index db1ad66..fe0b926 100644 --- a/minestom/build.gradle.kts +++ b/minestom/build.gradle.kts @@ -13,12 +13,38 @@ dependencies { project(":common", "shadow") ).forEach { api(it) + testImplementation(it) shadow(it) { isTransitive = false } } + + testImplementation(libs.minestom) + testImplementation(testFixtures(project(":common-server"))) + + testImplementation(libs.log4j.core) + testImplementation(libs.slf4j.log4j12) + + testImplementation(libs.jline.reader) + testImplementation(libs.jline.terminal) + testImplementation(libs.terminalconsoleappender) } tasks { java { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) } + + register("runServer") { + group = "application" + + standardInput = System.`in` + + workingDir = layout.projectDirectory.dir("run").asFile + + doFirst { + workingDir.mkdirs() + } + + classpath = sourceSets["test"].runtimeClasspath + mainClass.set("su.plo.slib.minestom.TestMinestomServerKt") + } } diff --git a/minestom/src/main/kotlin/su/plo/slib/minestom/MinestomServerLib.kt b/minestom/src/main/kotlin/su/plo/slib/minestom/MinestomServerLib.kt index b9073b3..4011fb6 100644 --- a/minestom/src/main/kotlin/su/plo/slib/minestom/MinestomServerLib.kt +++ b/minestom/src/main/kotlin/su/plo/slib/minestom/MinestomServerLib.kt @@ -2,7 +2,7 @@ package su.plo.slib.minestom import com.google.common.collect.Maps import net.minestom.server.MinecraftServer -import net.minestom.server.entity.LivingEntity +import net.minestom.server.entity.Entity import net.minestom.server.entity.Player import net.minestom.server.event.instance.InstanceUnregisterEvent import net.minestom.server.event.player.PlayerDisconnectEvent @@ -23,8 +23,8 @@ import su.plo.slib.chat.AdventureComponentTextConverter import su.plo.slib.integration.IntegrationLoader import su.plo.slib.language.ServerTranslatorFactory import su.plo.slib.logging.Slf4jLogger -import su.plo.slib.minestom.channel.RegisterChannelHandler import su.plo.slib.minestom.channel.MinestomChannelManager +import su.plo.slib.minestom.channel.RegisterChannelHandler import su.plo.slib.minestom.command.MinestomCommandManager import su.plo.slib.minestom.entity.MinestomServerEntity import su.plo.slib.minestom.entity.MinestomServerPlayer @@ -34,7 +34,7 @@ import su.plo.slib.minestom.world.MinestomServerWorld import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException -import java.util.* +import java.util.UUID import java.util.function.Consumer class MinestomServerLib( @@ -43,6 +43,7 @@ class MinestomServerLib( init { McLoggerFactory.supplier = McLoggerFactory.Supplier { name -> Slf4jLogger(name) } + instance = this } private val worldByInstance: MutableMap = Maps.newConcurrentMap() @@ -137,7 +138,11 @@ class MinestomServerLib( } override fun getEntityByInstance(instance: Any): McServerEntity { - require(instance is LivingEntity) { "instance is not ${LivingEntity::class.java}" } + require(instance is Entity) { "instance is not ${Entity::class.java}" } + + if (instance is Player) { + return getPlayerByInstance(instance) + } return MinestomServerEntity( this, @@ -177,4 +182,8 @@ class MinestomServerLib( ) playerById.remove(event.player.uuid) } + + companion object { + lateinit var instance: MinestomServerLib + } } diff --git a/minestom/src/main/kotlin/su/plo/slib/minestom/chat/ComponentToMessageConverter.kt b/minestom/src/main/kotlin/su/plo/slib/minestom/chat/ComponentToMessageConverter.kt new file mode 100644 index 0000000..56c2872 --- /dev/null +++ b/minestom/src/main/kotlin/su/plo/slib/minestom/chat/ComponentToMessageConverter.kt @@ -0,0 +1,10 @@ +package su.plo.slib.minestom.chat + +import com.mojang.brigadier.Message +import su.plo.slib.api.chat.component.McTextComponent +import su.plo.slib.api.chat.converter.MessageTextConverter + +class ComponentToMessageConverter : MessageTextConverter { + override fun convert(text: McTextComponent): Message = + McTextMessage(text) +} diff --git a/minestom/src/main/kotlin/su/plo/slib/minestom/chat/McTextMessage.kt b/minestom/src/main/kotlin/su/plo/slib/minestom/chat/McTextMessage.kt new file mode 100644 index 0000000..26c6a07 --- /dev/null +++ b/minestom/src/main/kotlin/su/plo/slib/minestom/chat/McTextMessage.kt @@ -0,0 +1,10 @@ +package su.plo.slib.minestom.chat + +import com.mojang.brigadier.Message +import su.plo.slib.api.chat.component.McTextComponent + +class McTextMessage( + val component: McTextComponent, +) : Message { + override fun getString(): String = component.toString() +} diff --git a/minestom/src/main/kotlin/su/plo/slib/minestom/command/MinestomCommandManager.kt b/minestom/src/main/kotlin/su/plo/slib/minestom/command/MinestomCommandManager.kt index c6d19fb..04a883d 100644 --- a/minestom/src/main/kotlin/su/plo/slib/minestom/command/MinestomCommandManager.kt +++ b/minestom/src/main/kotlin/su/plo/slib/minestom/command/MinestomCommandManager.kt @@ -1,24 +1,63 @@ package su.plo.slib.minestom.command +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.arguments.BoolArgumentType +import com.mojang.brigadier.arguments.DoubleArgumentType +import com.mojang.brigadier.arguments.FloatArgumentType +import com.mojang.brigadier.arguments.IntegerArgumentType +import com.mojang.brigadier.arguments.LongArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.context.ParsedArgument +import com.mojang.brigadier.context.StringRange +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import com.mojang.brigadier.tree.ArgumentCommandNode +import com.mojang.brigadier.tree.LiteralCommandNode +import net.kyori.adventure.text.Component import net.minestom.server.MinecraftServer +import net.minestom.server.command.ArgumentParserType import net.minestom.server.command.CommandSender import net.minestom.server.command.builder.Command +import net.minestom.server.command.builder.CommandExecutor +import net.minestom.server.command.builder.arguments.Argument +import net.minestom.server.command.builder.exception.ArgumentSyntaxException +import net.minestom.server.command.builder.suggestion.SuggestionCallback +import net.minestom.server.command.builder.suggestion.SuggestionEntry import net.minestom.server.entity.Player +import net.minestom.server.network.NetworkBuffer +import su.plo.slib.api.chat.component.McTextComponent +import su.plo.slib.api.chat.style.McTextStyle import su.plo.slib.api.command.McCommand -import su.plo.slib.api.command.McCommandManager import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.command.brigadier.CustomArgumentType +import su.plo.slib.api.command.brigadier.McBrigadierSource +import su.plo.slib.api.entity.McEntity import su.plo.slib.api.server.McServerLib import su.plo.slib.api.server.event.command.McServerCommandsRegisterEvent +import su.plo.slib.command.AbstractCommandManager +import su.plo.slib.command.brigadier.CustomArgumentCommandNode +import su.plo.slib.command.proxied +import su.plo.slib.minestom.chat.McTextMessage +import su.plo.slib.minestom.command.brigadier.MinestomArgumentType +import su.plo.slib.minestom.command.brigadier.MinestomBrigadierSource class MinestomCommandManager( private val minecraftServer: McServerLib -) : McCommandManager() { +) : AbstractCommandManager() { + + // Minestom discards the thrown ArgumentSyntaxException's rich info when building its + // InvalidCommand, and the per-argument ArgumentCallback is orphan API that never fires. + // We stash the original CommandSyntaxException here so the defaultExecutor (which does fire + // on parse errors) can render the translatable message. + private val pendingParseError = ThreadLocal() @Synchronized fun registerCommands() { McServerCommandsRegisterEvent.invoker.onCommandsRegister(this, minecraftServer) - commandByName.forEach { (name, command) -> + registerCommands { name, command -> val cmd = Command(name) cmd.setDefaultExecutor { sender, context -> val source = getCommandSource(sender) @@ -33,6 +72,15 @@ class MinestomCommandManager( MinecraftServer.getCommandManager().register(cmd) } + registerBrigadierCommands { command -> + MinecraftServer.getCommandManager().register( + command.proxied( + { it }, + { it }, + ).toMinestom() + ) + } + registered = true } @@ -42,4 +90,172 @@ class MinestomCommandManager( return if (source is Player) minecraftServer.getPlayerByInstance(source) else MinestomDefaultCommandSource(minecraftServer.textConverter, source) } + + private fun LiteralCommandNode.toMinestom(): Command { + val minestomCommand = Command(name) + + children.filterIsInstance>() + .forEach { minestomCommand.addSubcommand(it.toMinestom()) } + + val literalExecutor = command?.toMinestom() + + children.filterIsInstance>() + .forEach { registerArgumentSyntaxes(minestomCommand, it, emptyList(), literalExecutor) } + + minestomCommand.defaultExecutor = defaultCommandExecutor(literalExecutor) + + return minestomCommand + } + + private fun registerArgumentSyntaxes( + minestomCommand: Command, + node: ArgumentCommandNode, + prefix: List>, + fallbackExecutor: CommandExecutor?, + ) { + val (argument, executor) = node.toMinestom() + val pathSoFar = prefix + argument + val argDescendants = node.children.filterIsInstance>() + + if (executor != null || argDescendants.isEmpty()) { + minestomCommand.addSyntax( + executor ?: fallbackExecutor ?: noopCommandExecutor(), + *pathSoFar.toTypedArray(), + ) + } + + argDescendants.forEach { child -> + registerArgumentSyntaxes(minestomCommand, child, pathSoFar, fallbackExecutor) + } + } + + private fun noopCommandExecutor(): CommandExecutor = + CommandExecutor { _, _ -> } + + private fun defaultCommandExecutor(fallback: CommandExecutor?): CommandExecutor = + CommandExecutor { sender, context -> + val error = pendingParseError.get() + if (error != null) { + pendingParseError.remove() + getCommandSource(sender).sendParseError(error) + return@CommandExecutor + } + fallback?.apply(sender, context) + } + + private fun ArgumentType<*>.toMinestomParserType(): ArgumentParserType = + when (this) { + is BoolArgumentType -> ArgumentParserType.BOOL + is DoubleArgumentType -> ArgumentParserType.DOUBLE + is FloatArgumentType -> ArgumentParserType.FLOAT + is IntegerArgumentType -> ArgumentParserType.INTEGER + is LongArgumentType -> ArgumentParserType.LONG + is StringArgumentType -> ArgumentParserType.STRING + is CustomArgumentType<*, *> -> nativeType.toMinestomParserType() + else -> throw IllegalArgumentException("Invalid argument type: $this") + } + + private fun ArgumentCommandNode.toMinestom(): Pair, CommandExecutor?> { + val argumentType = type + val executor = command?.toMinestom() + + if (argumentType is MinestomArgumentType) { + return argumentType.argumentBuilder.invoke(name) to executor + } + + val minestomArgument = object : Argument(name) { + @Suppress("UNCHECKED_CAST") + override fun parse(sender: CommandSender, input: String): T { + pendingParseError.remove() + return try { + if (this@toMinestom is CustomArgumentCommandNode<*, *, *>) { + (customArgumentType.parse(StringReader(input)) as T) + } else { + argumentType.parse(StringReader(input)) + } + } catch (e: CommandSyntaxException) { + pendingParseError.set(e) + throw ArgumentSyntaxException(e.message, input, -1) + } + } + + override fun parser(): ArgumentParserType = + argumentType.toMinestomParserType() + + override fun nodeProperties(): ByteArray? { + val effectiveType = (argumentType as? CustomArgumentType<*, *>)?.nativeType ?: argumentType + if (effectiveType is StringArgumentType) { + return NetworkBuffer.makeArray(NetworkBuffer.VAR_INT, effectiveType.type.ordinal) + } + + return super.nodeProperties() + } + } + + minestomArgument.suggestionCallback = SuggestionCallback { sender, context, suggestion -> + val brigadierContext = context.toBrigadier(sender, command) + val suggestions = listSuggestions(brigadierContext, SuggestionsBuilder(context.input, 0)).get() + + suggestions.list.forEach { + suggestion.addEntry( + SuggestionEntry(it.text, it.tooltip?.string?.let(Component::text)) + ) + } + } + + return minestomArgument to executor + } + + private fun com.mojang.brigadier.Command.toMinestom(): CommandExecutor = + CommandExecutor { sender, context -> + pendingParseError.remove() + val source = getCommandSource(sender) + val brigadierContext = context.toBrigadier(sender, this@toMinestom) + + try { + this@toMinestom.run(brigadierContext) + } catch (e: CommandSyntaxException) { + source.sendParseError(e) + } catch (e: Exception) { + source.sendMessage( + McTextComponent.literal(e.message ?: "Unknown error").withStyle(McTextStyle.RED) + ) + } + } + + private fun net.minestom.server.command.builder.CommandContext.toBrigadier( + sender: CommandSender, + command: com.mojang.brigadier.Command?, + ): CommandContext { + val source = getCommandSource(sender) + + return CommandContext( + MinestomBrigadierSource(source, source as? McEntity, sender), + input, + map.mapValues { ParsedArgument(0, 0, it.value) }, + command, + null, + emptyList(), + StringRange(0, 0), + null, + null, + false, + ) + } + + private fun McCommandSource.sendParseError(e: CommandSyntaxException) { + val rawMessage = e.rawMessage + val messageArg = + if (rawMessage is McTextMessage) rawMessage.component + else McTextComponent.literal(rawMessage.string) + + sendMessage( + McTextComponent.translatable( + "command.context.parse_error", + messageArg, + McTextComponent.literal(e.cursor.toString()), + McTextComponent.literal(e.context), + ).withStyle(McTextStyle.RED) + ) + } } diff --git a/minestom/src/main/kotlin/su/plo/slib/minestom/command/MinestomDefaultCommandSource.kt b/minestom/src/main/kotlin/su/plo/slib/minestom/command/MinestomDefaultCommandSource.kt index 0ceb32a..5ead7b8 100644 --- a/minestom/src/main/kotlin/su/plo/slib/minestom/command/MinestomDefaultCommandSource.kt +++ b/minestom/src/main/kotlin/su/plo/slib/minestom/command/MinestomDefaultCommandSource.kt @@ -1,11 +1,15 @@ package su.plo.slib.minestom.command +import net.kyori.adventure.text.Component import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer +import net.kyori.adventure.translation.GlobalTranslator +import net.kyori.adventure.translation.Translator import net.minestom.server.command.CommandSender import su.plo.slib.api.chat.component.McTextComponent import su.plo.slib.api.chat.converter.ServerTextConverter import su.plo.slib.api.command.McCommandSource import su.plo.slib.api.permission.PermissionTristate +import java.util.Locale class MinestomDefaultCommandSource( private val textConverter: ServerTextConverter<*>, @@ -13,17 +17,18 @@ class MinestomDefaultCommandSource( ) : McCommandSource { override fun sendMessage(text: McTextComponent) { - val json = textConverter.convertToJson(this, text) - val component = GsonComponentSerializer.gson().deserialize(json) - - source.sendMessage(component) + source.sendMessage(render(text)) } override fun sendActionBar(text: McTextComponent) { + source.sendActionBar(render(text)) + } + + private fun render(text: McTextComponent): Component { val json = textConverter.convertToJson(this, text) val component = GsonComponentSerializer.gson().deserialize(json) - - source.sendActionBar(component) + val locale = Translator.parseLocale(language) ?: Locale.US + return GlobalTranslator.render(component, locale) } override val language: String diff --git a/minestom/src/main/kotlin/su/plo/slib/minestom/command/brigadier/MinestomArgumentType.kt b/minestom/src/main/kotlin/su/plo/slib/minestom/command/brigadier/MinestomArgumentType.kt new file mode 100644 index 0000000..97b1d21 --- /dev/null +++ b/minestom/src/main/kotlin/su/plo/slib/minestom/command/brigadier/MinestomArgumentType.kt @@ -0,0 +1,13 @@ +package su.plo.slib.minestom.command.brigadier + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import net.minestom.server.command.builder.arguments.Argument + +data class MinestomArgumentType( + val argumentBuilder: (String) -> Argument, +) : ArgumentType { + override fun parse(reader: StringReader): S { + throw UnsupportedOperationException() + } +} diff --git a/minestom/src/main/kotlin/su/plo/slib/minestom/command/brigadier/MinestomBrigadierArguments.kt b/minestom/src/main/kotlin/su/plo/slib/minestom/command/brigadier/MinestomBrigadierArguments.kt new file mode 100644 index 0000000..2b3b535 --- /dev/null +++ b/minestom/src/main/kotlin/su/plo/slib/minestom/command/brigadier/MinestomBrigadierArguments.kt @@ -0,0 +1,107 @@ +package su.plo.slib.minestom.command.brigadier + +import com.mojang.brigadier.arguments.ArgumentType +import net.minestom.server.MinecraftServer +import net.minestom.server.command.builder.arguments.minecraft.ArgumentEntity +import net.minestom.server.command.builder.arguments.relative.ArgumentRelativeBlockPosition +import net.minestom.server.entity.Entity +import net.minestom.server.entity.Player +import su.plo.slib.api.entity.player.McGameProfile +import su.plo.slib.api.server.command.brigadier.McArgumentTypes +import su.plo.slib.api.server.command.brigadier.McEntitiesArgumentResolver +import su.plo.slib.api.server.command.brigadier.McEntityArgumentResolver +import su.plo.slib.api.server.command.brigadier.McGameProfilesArgumentResolver +import su.plo.slib.api.server.command.brigadier.McPlayerArgumentResolver +import su.plo.slib.api.server.command.brigadier.McPlayersArgumentResolver +import su.plo.slib.api.server.command.brigadier.ServerPos3dResolver +import su.plo.slib.api.server.entity.McServerEntity +import su.plo.slib.api.server.position.ServerPos3d +import su.plo.slib.minestom.MinestomServerLib + +class MinestomBrigadierArguments : McArgumentTypes.Provider { + private val serverLib by lazy { MinestomServerLib.instance } + + override fun entity(): ArgumentType = + argumentResolver( + MinestomArgumentType { name -> ArgumentEntity(name).singleEntity(true).onlyPlayers(false) } + ) { finder -> + McEntityArgumentResolver { source -> + val entity = finder.findFirstEntity((source as MinestomBrigadierSource).getInstance()) + ?: throw IllegalArgumentException("No entity found") + serverLib.getEntityByInstance(entity) + } + } + + override fun entities(): ArgumentType = + argumentResolver( + MinestomArgumentType { name -> ArgumentEntity(name).singleEntity(false).onlyPlayers(false) } + ) { finder -> + McEntitiesArgumentResolver { source -> + finder.find((source as MinestomBrigadierSource).getInstance()) + .map { serverLib.getEntityByInstance(it) } + } + } + + override fun player(): ArgumentType = + argumentResolver( + MinestomArgumentType { name -> ArgumentEntity(name).singleEntity(true).onlyPlayers(true) } + ) { finder -> + McPlayerArgumentResolver { source -> + val player = finder.findFirstPlayer((source as MinestomBrigadierSource).getInstance()) + ?: throw IllegalArgumentException("No player found") + serverLib.getPlayerByInstance(player) + } + } + + override fun players(): ArgumentType = + argumentResolver( + MinestomArgumentType { name -> ArgumentEntity(name).singleEntity(false).onlyPlayers(true) } + ) { finder -> + McPlayersArgumentResolver { source -> + finder.find((source as MinestomBrigadierSource).getInstance()) + .filterIsInstance() + .map { serverLib.getPlayerByInstance(it) } + } + } + + // todo: there's no way to get cached game profiles in minestom + override fun gameProfiles(): ArgumentType = + argumentResolver( + MinestomArgumentType { name -> ArgumentEntity(name).singleEntity(false).onlyPlayers(true) } + ) { finder -> + McGameProfilesArgumentResolver { source -> + finder.find((source as MinestomBrigadierSource).getInstance()) + .filterIsInstance() + .map { McGameProfile(it.uuid, it.username, emptyList()) } + } + } + + override fun position(): ArgumentType = + argumentResolver( + MinestomArgumentType { name -> ArgumentRelativeBlockPosition(name) } + ) { resolver -> + ServerPos3dResolver { source -> + val entity = source.executor as? McServerEntity + val entityPosition = entity?.getServerPosition() + + val position = resolver.from(entity?.getInstance()) + + ServerPos3d( + entity?.world, + position.x(), + position.y(), + position.z(), + entityPosition?.yaw ?: position.x().toFloat(), + entityPosition?.pitch ?: position.y().toFloat(), + ) + } + } + + private fun argumentResolver( + finderArgumentType: MinestomArgumentType, + resolverFactory: (S) -> T, + ): ArgumentType = + MinestomArgumentType { name -> + finderArgumentType.argumentBuilder(name).map { resolverFactory(it) } + } +} diff --git a/minestom/src/main/kotlin/su/plo/slib/minestom/command/brigadier/MinestomBrigadierSource.kt b/minestom/src/main/kotlin/su/plo/slib/minestom/command/brigadier/MinestomBrigadierSource.kt new file mode 100644 index 0000000..7c761a6 --- /dev/null +++ b/minestom/src/main/kotlin/su/plo/slib/minestom/command/brigadier/MinestomBrigadierSource.kt @@ -0,0 +1,16 @@ +package su.plo.slib.minestom.command.brigadier + +import net.minestom.server.command.CommandSender +import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.command.brigadier.McBrigadierSource +import su.plo.slib.api.entity.McEntity + +data class MinestomBrigadierSource( + override val source: McCommandSource, + override val executor: McEntity?, + private val instance: CommandSender, +) : McBrigadierSource { + @Suppress("UNCHECKED_CAST") + override fun getInstance(): T = + instance as T +} diff --git a/minestom/src/main/kotlin/su/plo/slib/minestom/entity/MinestomServerEntity.kt b/minestom/src/main/kotlin/su/plo/slib/minestom/entity/MinestomServerEntity.kt index 56e57cf..2535556 100644 --- a/minestom/src/main/kotlin/su/plo/slib/minestom/entity/MinestomServerEntity.kt +++ b/minestom/src/main/kotlin/su/plo/slib/minestom/entity/MinestomServerEntity.kt @@ -1,14 +1,14 @@ package su.plo.slib.minestom.entity -import net.minestom.server.entity.LivingEntity +import net.minestom.server.entity.Entity +import su.plo.slib.api.position.Pos3d import su.plo.slib.api.server.McServerLib import su.plo.slib.api.server.entity.McServerEntity -import su.plo.slib.api.position.Pos3d import su.plo.slib.api.server.position.ServerPos3d import su.plo.slib.api.server.world.McServerWorld -import java.util.* +import java.util.UUID -open class MinestomServerEntity( +open class MinestomServerEntity( protected val minecraftServer: McServerLib, protected val instance: E ) : McServerEntity { diff --git a/minestom/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter b/minestom/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter new file mode 100644 index 0000000..3e42941 --- /dev/null +++ b/minestom/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter @@ -0,0 +1 @@ +su.plo.slib.minestom.chat.ComponentToMessageConverter diff --git a/minestom/src/main/resources/META-INF/services/su.plo.slib.api.server.command.brigadier.McArgumentTypes$Provider b/minestom/src/main/resources/META-INF/services/su.plo.slib.api.server.command.brigadier.McArgumentTypes$Provider new file mode 100644 index 0000000..f742ba6 --- /dev/null +++ b/minestom/src/main/resources/META-INF/services/su.plo.slib.api.server.command.brigadier.McArgumentTypes$Provider @@ -0,0 +1 @@ +su.plo.slib.minestom.command.brigadier.MinestomBrigadierArguments diff --git a/minestom/src/test/kotlin/su/plo/slib/minestom/TestMinestomServer.kt b/minestom/src/test/kotlin/su/plo/slib/minestom/TestMinestomServer.kt new file mode 100644 index 0000000..2ca5838 --- /dev/null +++ b/minestom/src/test/kotlin/su/plo/slib/minestom/TestMinestomServer.kt @@ -0,0 +1,102 @@ +package su.plo.slib.minestom + +import net.kyori.adventure.key.Key +import net.kyori.adventure.translation.GlobalTranslator +import net.kyori.adventure.translation.TranslationStore +import net.minecrell.terminalconsole.SimpleTerminalConsole +import net.minestom.server.MinecraftServer +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.GameMode +import net.minestom.server.event.Event +import net.minestom.server.event.GlobalEventHandler +import net.minestom.server.event.player.AsyncPlayerConfigurationEvent +import net.minestom.server.event.player.PlayerSpawnEvent +import su.plo.slib.server.TestServer +import java.io.File +import java.text.MessageFormat +import java.util.Locale +import kotlin.concurrent.thread +import kotlin.system.exitProcess +import kotlin.time.measureTime + +private val logger = MinecraftServer.LOGGER + +fun main() { + val startTime = measureTime { startServer() } + logger.info("Done (%.3fs)!".format(startTime.inWholeMilliseconds.toDouble() / 1000)) +} + +fun startServer() { + val minecraftServer = MinecraftServer.init() + + registerVanillaTranslations() + + val minecraftServerLib = MinestomServerLib(File("slib-test")) + val testServer = TestServer(minecraftServerLib) + + minecraftServerLib.onInitialize() + testServer.registerChannels() + + val instanceManager = MinecraftServer.getInstanceManager() + val instanceContainer = instanceManager.createInstanceContainer() + + MinecraftServer.getGlobalEventHandler().apply { + addListener { event -> + val player = event.player + player.respawnPoint = Pos(0.0, 0.0, 0.0) + event.spawningInstance = instanceContainer + } + + addListener { event -> + val player = event.player + player.gameMode = GameMode.SPECTATOR + } + } + + minecraftServer.start("0.0.0.0", 25565) + + thread(isDaemon = true) { + Console().start() + } +} + +class Console : SimpleTerminalConsole() { + private val commandManager = MinecraftServer.getCommandManager() + + override fun isRunning(): Boolean = + MinecraftServer.isStarted() && !MinecraftServer.isStopping() + + override fun runCommand(command: String) { + try { + commandManager.execute(commandManager.consoleSender, command) + } catch (e: Throwable) { + logger.error("Failed to execute command: /$command", e) + } + } + + override fun shutdown() { + logger.info("Shutting down...") + try { + MinecraftServer.stopCleanly() + exitProcess(0) + } catch (e: Throwable) { + logger.error("An error occurred while shutting down", e) + exitProcess(1) + } + } + +} + +private inline fun GlobalEventHandler.addListener(crossinline listener: (T) -> Unit) { + addListener(T::class.java) { listener.invoke(it) } +} + +// Minestom doesn't ship vanilla translations, so translation keys leak to the console as raw ids +// Register the minimum set the smoke tests need +private fun registerVanillaTranslations() { + val store = TranslationStore.messageFormat(Key.key("slib", "test")) + store.defaultLocale(Locale.US) + store.register("command.context.parse_error", Locale.US, MessageFormat("{0} at position {1}: {2}")) + store.register("argument.uuid.invalid", Locale.US, MessageFormat("Invalid UUID")) + GlobalTranslator.translator().addSource(store) +} diff --git a/minestom/src/test/resources/log4j.properties b/minestom/src/test/resources/log4j.properties new file mode 100644 index 0000000..768df25 --- /dev/null +++ b/minestom/src/test/resources/log4j.properties @@ -0,0 +1,21 @@ +log4j.rootLogger=DEBUG, file, console + +log4j.appender.console=org.apache.log4j.ConsoleAppender +logrj.appender.console.Target=System.out +log4j.appender.console.layout=org.apache.log4j.PatternLayout +log4j.appender.console.layout.ConversionPattern=%-5p %c{1} - %m%n + +log4j.appender.file=org.apache.log4j.RollingFileAppender +log4j.appender.file.File=logs/latest.log +log4j.appender.file.Append=true +log4j.appender.file.ImmediateFlush=true +log4j.appender.file.MaxFileSize=10MB +log4j.appender.file.MaxBackupIndex=5 +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=%d %d{Z} [%t] %-5p (%F:%L) - %m%n + +log4j.logger.com.journaldev.log4j=WARN, file, console +log4j.logger.com.journaldev.log4j.logic=DEBUG, file, console + +log4j.additivity.com.journaldev.log4j=false +log4j.additivity.com.journaldev.log4j.logic=false diff --git a/modded/1.21.11-26.1.txt b/modded/1.21.11-26.1.txt index 8300c64..38f4861 100644 --- a/modded/1.21.11-26.1.txt +++ b/modded/1.21.11-26.1.txt @@ -3,3 +3,5 @@ playS2C clientboundPlay S2CPlayChannelEvents ClientboundPlayChannelEvents S2CConfigurationChannelEvents ClientboundConfigurationChannelEvents + +field_54403 this$0 diff --git a/modded/build.gradle.kts b/modded/build.gradle.kts index 3df1366..68e7e54 100644 --- a/modded/build.gradle.kts +++ b/modded/build.gradle.kts @@ -6,6 +6,7 @@ import net.fabricmc.loom.task.RemapJarTask plugins { id("com.gradleup.shadow") + alias(libs.plugins.fletchingtable) } val minecraftVersion = stonecutter.current.project.substringBefore('-') @@ -72,9 +73,9 @@ loom.mods.findByName("main")?.apply { mainResourceDirectory.set(sourceSets.test.get().output.resourcesDir) } -if (isForge && stonecutter.eval(minecraftVersion, "<1.20.2")) { +if (isForge) { loom.forge { - mixinConfig("slib-forge.mixins.json") + mixinConfig("slib.mixins.json") } } @@ -82,6 +83,12 @@ configurations { named("loomDevelopmentDependencies") { extendsFrom(configurations.getByName("implementation")) } } +fletchingTable { + j52j.register("main") { + extension("json", "*.json5") + } +} + dependencies { val minecraftVersionDep = (findProperty("deps.minecraft") as? String) ?: stonecutter.current.project.substringBefore('-') @@ -230,30 +237,10 @@ tasks { mergeServiceFiles() exclude("META-INF/*.kotlin_module") - // todo: neoforge should use its own mixin without refmap - if (isForge) { exclude("fabric.mod.json") - exclude("slib-no-refmap.mixins.json") - - if (stonecutter.eval(minecraftVersion, ">=1.20.2")) { - exclude("slib-forge.mixins.json") - } } else if (isNeoForge) { exclude("fabric.mod.json") - exclude("slib-forge.mixins.json") - exclude("slib.mixins.json") - - rename("slib-no-refmap.mixins.json", "slib.mixins.json") - } else { - exclude("slib-forge.mixins.json") - - if (stonecutter.eval(minecraftVersion, ">=26.1")) { - exclude("slib.mixins.json") - rename("slib-no-refmap.mixins.json", "slib.mixins.json") - } else { - exclude("slib-no-refmap.mixins.json") - } } } diff --git a/modded/src/main/java/su/plo/slib/mod/mixin/accessor/CommandSourceStackAccessor.java b/modded/src/main/java/su/plo/slib/mod/mixin/accessor/CommandSourceStackAccessor.java new file mode 100644 index 0000000..37da0f9 --- /dev/null +++ b/modded/src/main/java/su/plo/slib/mod/mixin/accessor/CommandSourceStackAccessor.java @@ -0,0 +1,13 @@ +package su.plo.slib.mod.mixin.accessor; + +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(CommandSourceStack.class) +public interface CommandSourceStackAccessor { + @Accessor("source") + @NotNull CommandSource slib_getSource(); +} diff --git a/modded/src/main/java/su/plo/slib/mod/mixin/accessor/ServerPlayerCommandSourceAccessor.java b/modded/src/main/java/su/plo/slib/mod/mixin/accessor/ServerPlayerCommandSourceAccessor.java new file mode 100644 index 0000000..44def00 --- /dev/null +++ b/modded/src/main/java/su/plo/slib/mod/mixin/accessor/ServerPlayerCommandSourceAccessor.java @@ -0,0 +1,14 @@ +//? if >=1.21.10 { +/*package su.plo.slib.mod.mixin.accessor; + +import net.minecraft.server.level.ServerPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(targets = "net.minecraft.server.level.ServerPlayer$3") +public interface ServerPlayerCommandSourceAccessor { + + @Accessor("field_54403") + ServerPlayer slib_getServerPlayer(); +} +*///?} diff --git a/modded/src/main/kotlin/su/plo/slib/mod/ModServerLib.kt b/modded/src/main/kotlin/su/plo/slib/mod/ModServerLib.kt index 37c3e68..b2a7524 100644 --- a/modded/src/main/kotlin/su/plo/slib/mod/ModServerLib.kt +++ b/modded/src/main/kotlin/su/plo/slib/mod/ModServerLib.kt @@ -114,6 +114,10 @@ object ModServerLib : McServerLib { override fun getEntityByInstance(instance: Any): McServerEntity { require(instance is Entity) { "instance is not " + Entity::class.java } + if (instance is ServerPlayer) { + return getPlayerByInstance(instance) + } + return ModServerEntity( this, instance diff --git a/modded/src/main/kotlin/su/plo/slib/mod/chat/ComponentTextConverter.kt b/modded/src/main/kotlin/su/plo/slib/mod/chat/ComponentTextConverter.kt index 4b98471..b9b9842 100644 --- a/modded/src/main/kotlin/su/plo/slib/mod/chat/ComponentTextConverter.kt +++ b/modded/src/main/kotlin/su/plo/slib/mod/chat/ComponentTextConverter.kt @@ -6,7 +6,10 @@ import su.plo.slib.api.chat.converter.McTextConverter import su.plo.slib.chat.AdventureComponentTextConverter //? if >=1.20.5 { -/*import com.google.gson.* +/*import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonParseException +import com.google.gson.JsonParser import com.mojang.serialization.JsonOps import net.minecraft.network.chat.ComponentSerialization *///?} @@ -28,7 +31,7 @@ object ComponentTextConverter : McTextConverter { ?.let { ComponentSerialization.CODEC.parse(JsonOps.INSTANCE, it).getOrThrow(::JsonParseException) } - ?: throw JsonParseException("JsonParser return null smh") + ?: throw JsonParseException("JsonParser returned null") *///?} else { Component.Serializer.fromJson(json)!! //?} @@ -41,4 +44,4 @@ object ComponentTextConverter : McTextConverter { *///?} else { Component.Serializer.toJson(text) //?} -} \ No newline at end of file +} diff --git a/modded/src/main/kotlin/su/plo/slib/mod/chat/ComponentToMessageConverter.kt b/modded/src/main/kotlin/su/plo/slib/mod/chat/ComponentToMessageConverter.kt new file mode 100644 index 0000000..64f3d55 --- /dev/null +++ b/modded/src/main/kotlin/su/plo/slib/mod/chat/ComponentToMessageConverter.kt @@ -0,0 +1,10 @@ +package su.plo.slib.mod.chat + +import com.mojang.brigadier.Message +import su.plo.slib.api.chat.component.McTextComponent +import su.plo.slib.api.chat.converter.MessageTextConverter + +class ComponentToMessageConverter : MessageTextConverter { + override fun convert(text: McTextComponent): Message = + ComponentTextConverter.convert(text) +} diff --git a/modded/src/main/kotlin/su/plo/slib/mod/command/ModCommandManager.kt b/modded/src/main/kotlin/su/plo/slib/mod/command/ModCommandManager.kt index d0e241d..c0f3afc 100644 --- a/modded/src/main/kotlin/su/plo/slib/mod/command/ModCommandManager.kt +++ b/modded/src/main/kotlin/su/plo/slib/mod/command/ModCommandManager.kt @@ -1,33 +1,67 @@ package su.plo.slib.mod.command import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.context.CommandContext import net.minecraft.commands.CommandSourceStack import net.minecraft.world.entity.player.Player -import su.plo.slib.api.server.McServerLib import su.plo.slib.api.command.McCommand -import su.plo.slib.api.command.McCommandManager import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.command.brigadier.McBrigadierSource +import su.plo.slib.api.server.McServerLib +import su.plo.slib.command.AbstractCommandManager +import su.plo.slib.command.copyFor +import su.plo.slib.command.proxied +import su.plo.slib.mod.command.brigadier.ModBrigadierSource +import su.plo.slib.mod.mixin.accessor.CommandSourceStackAccessor + +//? if >=1.21.10 { +/*import su.plo.slib.mod.mixin.accessor.ServerPlayerCommandSourceAccessor +*///?} class ModCommandManager( private val minecraftServer: McServerLib -) : McCommandManager() { +) : AbstractCommandManager() { @Synchronized fun registerCommands(dispatcher: CommandDispatcher) { - commandByName.forEach { (name, command) -> + registerCommands { name, command -> val modCommand = ModCommand(minecraftServer, this, command) modCommand.register(dispatcher, name) } + + @Suppress("UNCHECKED_CAST") + registerBrigadierCommands { command -> + dispatcher.root.addChild( + command.proxied( + ModBrigadierSource::from, + { it.toMc() }, + ) + ) + } + this.registered = true } - override fun getCommandSource(source: Any): McCommandSource { - require(source is CommandSourceStack) { "source is not " + CommandSourceStack::class.java } + override fun getCommandSource(sourceStack: Any): McCommandSource { + require(sourceStack is CommandSourceStack) { "source is not " + CommandSourceStack::class.java } + require(sourceStack is CommandSourceStackAccessor) { "source is not " + CommandSourceStackAccessor::class.java } + + val source = sourceStack.slib_getSource() - val entity = source.entity + //? if >=1.21.10 { + /*val player = (source as? ServerPlayerCommandSourceAccessor) + ?.let { minecraftServer.getPlayerByInstance(it.slib_getServerPlayer()) } + *///?} else { + val player = (source as? Player)?.let { minecraftServer.getPlayerByInstance(it) } + //?} - return if (entity is Player) { - minecraftServer.getPlayerByInstance(entity) - } else ModDefaultCommandSource(minecraftServer, source) + return player ?: ModDefaultCommandSource(minecraftServer, sourceStack) } } + +fun CommandContext.toSourceStack(): CommandContext = + copyFor(source.getInstance() as CommandSourceStack) + +fun CommandContext.toMc(): CommandContext = + copyFor(ModBrigadierSource.from(source)) + diff --git a/modded/src/main/kotlin/su/plo/slib/mod/command/ModDefaultCommandSource.kt b/modded/src/main/kotlin/su/plo/slib/mod/command/ModDefaultCommandSource.kt index c71a0b2..86e1858 100644 --- a/modded/src/main/kotlin/su/plo/slib/mod/command/ModDefaultCommandSource.kt +++ b/modded/src/main/kotlin/su/plo/slib/mod/command/ModDefaultCommandSource.kt @@ -1,15 +1,15 @@ package su.plo.slib.mod.command import net.minecraft.commands.CommandSourceStack -import su.plo.slib.api.server.McServerLib import su.plo.slib.api.chat.component.McTextComponent import su.plo.slib.api.command.McCommandSource import su.plo.slib.api.permission.PermissionTristate +import su.plo.slib.api.server.McServerLib import su.plo.slib.mod.chat.ComponentTextConverter class ModDefaultCommandSource( private val minecraftServer: McServerLib, - private val source: CommandSourceStack + private val source: CommandSourceStack, ) : McCommandSource { override val language: String @@ -19,11 +19,11 @@ class ModDefaultCommandSource( val json = minecraftServer.textConverter.convertToJson(this, text) val component = ComponentTextConverter.convertFromJson(json) - //? if >=1.19 { - source.sendSystemMessage(component) - //?} else { - /*source.sendSuccess(component, true); - *///?} + //? if >=1.20 { + /*source.sendSuccess({ component }, true) + *///?} else { + source.sendSuccess(component, true) + //?} } override fun sendActionBar(text: McTextComponent) = diff --git a/modded/src/main/kotlin/su/plo/slib/mod/command/brigadier/ModBrigadierArguments.kt b/modded/src/main/kotlin/su/plo/slib/mod/command/brigadier/ModBrigadierArguments.kt new file mode 100644 index 0000000..b06a752 --- /dev/null +++ b/modded/src/main/kotlin/su/plo/slib/mod/command/brigadier/ModBrigadierArguments.kt @@ -0,0 +1,90 @@ +package su.plo.slib.mod.command.brigadier + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import net.minecraft.commands.CommandSourceStack +import net.minecraft.commands.arguments.EntityArgument +import net.minecraft.commands.arguments.GameProfileArgument +import net.minecraft.commands.arguments.coordinates.BlockPosArgument +import su.plo.slib.api.command.brigadier.CustomArgumentType +import su.plo.slib.api.server.command.brigadier.McArgumentTypes +import su.plo.slib.api.server.command.brigadier.McEntitiesArgumentResolver +import su.plo.slib.api.server.command.brigadier.McEntityArgumentResolver +import su.plo.slib.api.server.command.brigadier.McGameProfilesArgumentResolver +import su.plo.slib.api.server.command.brigadier.McPlayerArgumentResolver +import su.plo.slib.api.server.command.brigadier.McPlayersArgumentResolver +import su.plo.slib.api.server.command.brigadier.ServerPos3dResolver +import su.plo.slib.api.server.entity.McServerEntity +import su.plo.slib.api.server.position.ServerPos3d +import su.plo.slib.mod.ModServerLib +import su.plo.slib.mod.extension.toMcGameProfile + +class ModBrigadierArguments : McArgumentTypes.Provider { + private val serverLib by lazy { ModServerLib } + + override fun entity(): ArgumentType = + argumentResolver(EntityArgument.entity()) { selector -> + McEntityArgumentResolver { source -> + serverLib.getEntityByInstance(selector.findSingleEntity(source.getInstance())) + } + } + + override fun entities(): ArgumentType = + argumentResolver(EntityArgument.entities()) { selector -> + McEntitiesArgumentResolver { source -> + selector.findEntities(source.getInstance()).map { serverLib.getEntityByInstance(it) } + } + } + + override fun player(): ArgumentType = + argumentResolver(EntityArgument.player()) { selector -> + McPlayerArgumentResolver { source -> + serverLib.getPlayerByInstance(selector.findSinglePlayer(source.getInstance())) + } + } + + override fun gameProfiles(): ArgumentType = + argumentResolver(GameProfileArgument.gameProfile()) { selector -> + McGameProfilesArgumentResolver { source -> + selector.getNames(source.getInstance()) + .map { it.toMcGameProfile() } + } + } + + override fun players(): ArgumentType = + argumentResolver(EntityArgument.players()) { selector -> + McPlayersArgumentResolver { source -> + selector.findPlayers(source.getInstance()).map { serverLib.getPlayerByInstance(it) } + } + } + + override fun position(): ArgumentType = + argumentResolver(BlockPosArgument.blockPos()) { coordinates -> + ServerPos3dResolver { source -> + val stack = source.getInstance() + val position = coordinates.getPosition(stack) + val rotation = coordinates.getRotation(stack) + + val world = (source.executor as? McServerEntity)?.world + + ServerPos3d( + world, + position.x(), + position.y(), + position.z(), + rotation.y, + rotation.x, + ) + } + } + + private fun argumentResolver( + nativeType: ArgumentType, + resolverFactory: (S) -> T, + ): CustomArgumentType = + object : CustomArgumentType { + override val nativeType = nativeType + + override fun parse(reader: StringReader) = resolverFactory(nativeType.parse(reader)) + } +} diff --git a/modded/src/main/kotlin/su/plo/slib/mod/command/brigadier/ModBrigadierSource.kt b/modded/src/main/kotlin/su/plo/slib/mod/command/brigadier/ModBrigadierSource.kt new file mode 100644 index 0000000..0ad8c67 --- /dev/null +++ b/modded/src/main/kotlin/su/plo/slib/mod/command/brigadier/ModBrigadierSource.kt @@ -0,0 +1,30 @@ +package su.plo.slib.mod.command.brigadier + +import net.minecraft.commands.CommandSourceStack +import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.command.brigadier.McBrigadierSource +import su.plo.slib.api.entity.McEntity +import su.plo.slib.mod.ModServerLib + +data class ModBrigadierSource( + override val source: McCommandSource, + override val executor: McEntity?, + private val instance: CommandSourceStack, +) : McBrigadierSource { + + @Suppress("UNCHECKED_CAST") + override fun getInstance(): T = + instance as T + + companion object { + private val minecraftServer by lazy { ModServerLib } + + fun from(sourceStack: CommandSourceStack): ModBrigadierSource { + val executor = sourceStack.entity?.let { minecraftServer.getEntityByInstance(it) } + + val source = minecraftServer.commandManager.getCommandSource(sourceStack) + + return ModBrigadierSource(source, executor, sourceStack) + } + } +} diff --git a/modded/src/main/kotlin/su/plo/slib/mod/extension/GameProfile.kt b/modded/src/main/kotlin/su/plo/slib/mod/extension/GameProfile.kt index 8b7d4ea..a98f743 100644 --- a/modded/src/main/kotlin/su/plo/slib/mod/extension/GameProfile.kt +++ b/modded/src/main/kotlin/su/plo/slib/mod/extension/GameProfile.kt @@ -3,6 +3,10 @@ package su.plo.slib.mod.extension import com.mojang.authlib.GameProfile import su.plo.slib.api.entity.player.McGameProfile +//? if >=1.21.9 { +/*import net.minecraft.server.players.NameAndId +*///?} + fun GameProfile.toMcGameProfile(): McGameProfile = McGameProfile( id, @@ -11,3 +15,12 @@ fun GameProfile.toMcGameProfile(): McGameProfile = McGameProfile.Property(it.name, it.value, it.signature) } ) + +//? if >=1.21.9 { +/*fun NameAndId.toMcGameProfile(): McGameProfile = + McGameProfile( + id, + name, + emptyList(), + ) +*///?} diff --git a/modded/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter b/modded/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter new file mode 100644 index 0000000..e52000e --- /dev/null +++ b/modded/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter @@ -0,0 +1 @@ +su.plo.slib.mod.chat.ComponentToMessageConverter diff --git a/modded/src/main/resources/META-INF/services/su.plo.slib.api.server.command.brigadier.McArgumentTypes$Provider b/modded/src/main/resources/META-INF/services/su.plo.slib.api.server.command.brigadier.McArgumentTypes$Provider new file mode 100644 index 0000000..0ebbe72 --- /dev/null +++ b/modded/src/main/resources/META-INF/services/su.plo.slib.api.server.command.brigadier.McArgumentTypes$Provider @@ -0,0 +1 @@ +su.plo.slib.mod.command.brigadier.ModBrigadierArguments diff --git a/modded/src/main/resources/slib-forge.mixins.json b/modded/src/main/resources/slib-forge.mixins.json deleted file mode 100644 index cc0a5a2..0000000 --- a/modded/src/main/resources/slib-forge.mixins.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "required": true, - "minVersion": "0.7", - "package": "su.plo.slib.mod.mixin", - "mixins": [ - "MixinServerGamePacketListenerImpl" - ], - "client": [ - ], - "injectors": { - "defaultRequire": 1 - }, - "refmap": "slib-${loader}-${mcVersion}-modded_${mcVersion}-${loader}-refmap.json" -} diff --git a/modded/src/main/resources/slib-no-refmap.mixins.json b/modded/src/main/resources/slib-no-refmap.mixins.json deleted file mode 100644 index 3213a8f..0000000 --- a/modded/src/main/resources/slib-no-refmap.mixins.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "required": true, - "minVersion": "0.7", - "package": "su.plo.slib.mod.mixin", - "mixins": [ - "MixinPlayerList", - "MixinServerPlayer" - ], - "client": [ - ], - "injectors": { - "defaultRequire": 1 - } -} diff --git a/modded/src/main/resources/slib.mixins.json b/modded/src/main/resources/slib.mixins.json deleted file mode 100644 index 59bcd46..0000000 --- a/modded/src/main/resources/slib.mixins.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "required": true, - "minVersion": "0.7", - "package": "su.plo.slib.mod.mixin", - "mixins": [ - "MixinPlayerList", - "MixinServerPlayer" - ], - "client": [ - ], - "injectors": { - "defaultRequire": 1 - }, - "refmap": "slib-${loader}-${mcVersion}-modded_${mcVersion}-${loader}-refmap.json" -} diff --git a/modded/src/main/resources/slib.mixins.json5 b/modded/src/main/resources/slib.mixins.json5 new file mode 100644 index 0000000..7960c13 --- /dev/null +++ b/modded/src/main/resources/slib.mixins.json5 @@ -0,0 +1,24 @@ +{ + "required": true, + "minVersion": "0.7", + "package": "su.plo.slib.mod.mixin", + "mixins": [ + "MixinPlayerList", + "MixinServerPlayer", + "accessor.CommandSourceStackAccessor" + //? if forge && <1.20.2 { + /*,"MixinServerGamePacketListenerImpl" + *///?} + //? if >=1.21.10 { + /*,"accessor.ServerPlayerCommandSourceAccessor" + *///?} + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } + //? if !neoforge && <26.1 { + ,"refmap": "slib-${loader}-${mcVersion}-modded_${mcVersion}-${loader}-refmap.json", + //?} +} diff --git a/modded/src/test/resources/META-INF/neoforge.mods.toml b/modded/src/test/resources/META-INF/neoforge.mods.toml index f6039a8..b4a8d1c 100644 --- a/modded/src/test/resources/META-INF/neoforge.mods.toml +++ b/modded/src/test/resources/META-INF/neoforge.mods.toml @@ -3,7 +3,7 @@ loaderVersion = "[2,)" license = "MIT" [[mixins]] -config="slib-no-refmap.mixins.json" +config="slib.mixins.json" [[mods]] modId = "slib_test_mod" diff --git a/modded/versions/26.1-fabric/gradle.properties b/modded/versions/26.1-fabric/gradle.properties index 32016a9..5cd97e0 100644 --- a/modded/versions/26.1-fabric/gradle.properties +++ b/modded/versions/26.1-fabric/gradle.properties @@ -1,4 +1,4 @@ loom.platform=fabric -deps.minecraft=26.1-rc-1 -deps.fabric_api=0.143.14+26.1 +deps.minecraft=26.1.2 +deps.fabric_api=0.146.1+26.1.2 diff --git a/modded/versions/26.1-neoforge/gradle.properties b/modded/versions/26.1-neoforge/gradle.properties index 1553930..5456c5a 100644 --- a/modded/versions/26.1-neoforge/gradle.properties +++ b/modded/versions/26.1-neoforge/gradle.properties @@ -1,4 +1,4 @@ loom.platform=neoforge -deps.minecraft=26.1-pre-3 -deps.neoforge=26.1.0.0-alpha.15+pre-3 +deps.minecraft=26.1.2 +deps.neoforge=26.1.2.22-beta diff --git a/scripts/smoke-test-proxy.sh b/scripts/smoke-test-proxy.sh new file mode 100755 index 0000000..ea012d9 --- /dev/null +++ b/scripts/smoke-test-proxy.sh @@ -0,0 +1,3 @@ +#!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/smoke-test.sh" proxy "$@" diff --git a/scripts/smoke-test-server.sh b/scripts/smoke-test-server.sh index 7b34b2b..bb64def 100755 --- a/scripts/smoke-test-server.sh +++ b/scripts/smoke-test-server.sh @@ -1,70 +1,3 @@ #!/bin/bash -set -e - -if [[ -z "$1" ]]; then - echo "Usage: $0 " - echo "Example: $0 modded:1.21-neoforge:runServer -Pmodded.versions_dev=1.21-neoforge" - echo "Example: $0 spigot:runServer -Pspigot.run_minecraft_version=1.16.5 -Pspigot.run_java_version=21" - exit 1 -fi - -COMMAND="$*" -PATTERNS=("Done \(.*\)!" "Command 'ping' registered") -LOGFILE=$(mktemp) -FOUND_DIR=$(mktemp -d) -trap "rm -rf $FOUND_DIR $LOGFILE" EXIT - -TIMEOUT=${CI:+600} -TIMEOUT=${TIMEOUT:-180} -echo "Testing: $COMMAND" - -# Run server in background, write to file -START_TIME=$(date +%s) -timeout $TIMEOUT ./gradlew $COMMAND \ - --console=plain \ - --no-daemon > "$LOGFILE" 2>&1 & -PID=$! - -# Poll the log file -while kill -0 $PID 2>/dev/null; do - for i in "${!PATTERNS[@]}"; do - if [[ ! -f "$FOUND_DIR/$i" ]] && grep -Eq "${PATTERNS[$i]}" "$LOGFILE"; then - touch "$FOUND_DIR/$i" - MATCH=$(grep -Eo "${PATTERNS[$i]}" "$LOGFILE" | head -1) - [[ -z "$CI" ]] && printf "\r\033[K" - echo "Found '${PATTERNS[$i]}' -> $MATCH" - fi - done - - FOUND_COUNT=$(ls "$FOUND_DIR" 2>/dev/null | wc -l) - ELAPSED=$(($(date +%s) - START_TIME)) - - if [[ $FOUND_COUNT -eq ${#PATTERNS[@]} ]]; then - kill $PID 2>/dev/null - - echo "=== Server output ===" - cat "$LOGFILE" - - echo "=== Test result ===" - echo "All ${#PATTERNS[@]} patterns matched in ${ELAPSED}s" - - exit 0 - fi - - [[ -z "$CI" ]] && printf "\rWaiting... %ds/%ds (%d/%d patterns found)" "$ELAPSED" "$TIMEOUT" "$FOUND_COUNT" "${#PATTERNS[@]}" - sleep 1 -done -[[ -z "$CI" ]] && echo - -EXIT_CODE=0 -wait $PID || EXIT_CODE=$? - -echo "=== Server output ===" -cat "$LOGFILE" - -if [[ $EXIT_CODE -eq 124 ]]; then - echo "FAILED: Server startup timed out" -else - echo "FAILED: Server exited (code $EXIT_CODE) before all patterns found" -fi -exit 1 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/smoke-test.sh" server "$@" diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..3c0014a --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,189 @@ +#!/bin/bash +set -e + +if [[ -z "$2" ]]; then + echo "Usage: $0 " + echo "Example: $0 server modded:1.21-neoforge:runServer -Pmodded.versions_dev=1.21-neoforge" + echo "Example: $0 server spigot:runServer -Pspigot.run_minecraft_version=1.16.5" + echo "Example: $0 proxy velocity:runVelocity" + echo "Example: $0 proxy bungee:runWaterfall" + exit 1 +fi + +ENV_TYPE="$1" +shift +COMMAND="$*" + +case "$ENV_TYPE" in + server) + PATTERNS=( + "Done \\(.*\\)!" + "Command 'ping' registered" + "Command 'brigadier-entity-selector' registered" + "Command 'brigadier-position-selector' registered" + "Command 'brigadier-game-profiles-selector' registered" + "Command 'brigadier-custom-type' registered" + "Command 'brigadier-multi-arg' registered" + ) + COMMAND_INPUTS=( + "brigadier-custom-type invalid-uuid" + "brigadier-entity-selector entities @e" + "brigadier-entity-selector players @a" + "brigadier-position-selector 100 100 100" + "brigadier-multi-arg 7 13" + ) + COMMAND_OUTPUT_PATTERNS=( + "Invalid UUID" + "Found entities:" + "Found players:" + "Position: ServerPos3d\\(world=null, x=100.0, y=100.0, z=100.0, yaw=100.0, pitch=100.0\\)" + "Multi-arg: a=7, b=13" + ) + ;; + proxy) + PATTERNS=( + "Listening on" + "Command 'ping' registered" + "Command 'brigadier-ping' registered" + "Command 'brigadier-custom-type' registered" + ) + COMMAND_INPUTS=( + "brigadier-custom-type invalid-uuid" + ) + COMMAND_OUTPUT_PATTERNS=( + "Invalid UUID" + ) + ;; + *) + echo "Error: Invalid environment type '$ENV_TYPE'. Must be 'server' or 'proxy'" + exit 1 + ;; +esac + +if [[ ${#COMMAND_INPUTS[@]} -ne ${#COMMAND_OUTPUT_PATTERNS[@]} ]]; then + echo "Error: COMMAND_INPUTS and COMMAND_OUTPUT_PATTERNS must have the same length" + exit 1 +fi + +LOGFILE=$(mktemp) +FOUND_DIR=$(mktemp -d) +STDIN_PIPE=$(mktemp -u) +mkfifo "$STDIN_PIPE" +trap "rm -rf $FOUND_DIR $LOGFILE $STDIN_PIPE" EXIT + +# Open read+write so this side doesn't block waiting for a peer, and so gradle +# doesn't see EOF before we send anything. +exec 3<>"$STDIN_PIPE" + +TIMEOUT=${CI:+600} +TIMEOUT=${TIMEOUT:-180} +CMD_TIMEOUT=${CMD_TIMEOUT:-5} +echo "Testing [$ENV_TYPE]: $COMMAND" + +# Run in background, write to file +START_TIME=$(date +%s) +timeout $TIMEOUT ./gradlew $COMMAND \ + --console=plain \ + --no-daemon < "$STDIN_PIPE" > "$LOGFILE" 2>&1 & +PID=$! + +dump_and_fail() { + local msg="$1" + [[ -z "$CI" ]] && echo + echo "=== $ENV_TYPE output ===" + cat "$LOGFILE" + echo "=== Test result ===" + echo "FAILED: $msg" + kill $PID 2>/dev/null || true + exit 1 +} + +# Phase 1: wait for startup patterns. +STARTUP_OK=0 +while kill -0 $PID 2>/dev/null; do + for i in "${!PATTERNS[@]}"; do + if [[ ! -f "$FOUND_DIR/$i" ]] && grep -Eq "${PATTERNS[$i]}" "$LOGFILE"; then + touch "$FOUND_DIR/$i" + MATCH=$(grep -Eo "${PATTERNS[$i]}" "$LOGFILE" | head -1) + [[ -z "$CI" ]] && printf "\r\033[K" + echo "Found '${PATTERNS[$i]}' -> $MATCH" + fi + done + + FOUND_COUNT=$(ls "$FOUND_DIR" 2>/dev/null | wc -l) + ELAPSED=$(($(date +%s) - START_TIME)) + + if [[ $FOUND_COUNT -eq ${#PATTERNS[@]} ]]; then + [[ -z "$CI" ]] && printf "\r\033[K" + echo "All ${#PATTERNS[@]} startup patterns matched in ${ELAPSED}s" + STARTUP_OK=1 + break + fi + + [[ -z "$CI" ]] && printf "\rWaiting... %ds/%ds (%d/%d patterns found)" "$ELAPSED" "$TIMEOUT" "$FOUND_COUNT" "${#PATTERNS[@]}" + sleep 1 +done + +if [[ $STARTUP_OK -eq 0 ]]; then + EXIT_CODE=0 + wait $PID || EXIT_CODE=$? + if [[ $EXIT_CODE -eq 124 ]]; then + dump_and_fail "Startup timed out" + else + dump_and_fail "Process exited (code $EXIT_CODE) before all startup patterns found" + fi +fi + +# Phase 2: send each command, wait for its expected output. +# Paper 1.19.3 through 1.20.5 route stdin through CraftServer.dispatchCommand, +# which only hits the Bukkit command map (no brigadier fallback until 1.20.6). +# Set SKIP_COMMAND_IO=1 on affected versions to keep the startup checks while +# opting out of the stdin round-trip. +if [[ -n "$SKIP_COMMAND_IO" ]]; then + echo "Skipping command I/O phase (SKIP_COMMAND_IO=$SKIP_COMMAND_IO)" + COMMAND_INPUTS=() +fi + +for i in "${!COMMAND_INPUTS[@]}"; do + INPUT="${COMMAND_INPUTS[$i]}" + PATTERN="${COMMAND_OUTPUT_PATTERNS[$i]}" + BEFORE=$(wc -l < "$LOGFILE") + + echo "Sending: $INPUT" + echo "$INPUT" >&3 + + CMD_START=$(date +%s) + MATCHED=0 + while kill -0 $PID 2>/dev/null; do + if tail -n +$((BEFORE + 1)) "$LOGFILE" | grep -Eq "$PATTERN"; then + MATCH=$(tail -n +$((BEFORE + 1)) "$LOGFILE" | grep -Eo "$PATTERN" | head -1) + [[ -z "$CI" ]] && printf "\r\033[K" + echo "Found '$PATTERN' -> $MATCH" + MATCHED=1 + break + fi + + CMD_ELAPSED=$(($(date +%s) - CMD_START)) + if [[ $CMD_ELAPSED -ge $CMD_TIMEOUT ]]; then + dump_and_fail "Command '$INPUT' did not produce pattern '$PATTERN' within ${CMD_TIMEOUT}s" + fi + + [[ -z "$CI" ]] && printf "\rWaiting for '%s'... %ds/%ds" "$PATTERN" "$CMD_ELAPSED" "$CMD_TIMEOUT" + sleep 1 + done + [[ -z "$CI" ]] && echo + + if [[ $MATCHED -eq 0 ]]; then + EXIT_CODE=0 + wait $PID || EXIT_CODE=$? + dump_and_fail "Process exited (code $EXIT_CODE) before pattern '$PATTERN' appeared for '$INPUT'" + fi +done + +echo "=== $ENV_TYPE output ===" +cat "$LOGFILE" +echo "=== Test result ===" +TOTAL_ELAPSED=$(($(date +%s) - START_TIME)) +echo "All ${#PATTERNS[@]} startup patterns and ${#COMMAND_INPUTS[@]} command patterns matched in ${TOTAL_ELAPSED}s" +kill $PID 2>/dev/null || true +exit 0 diff --git a/settings.gradle.kts b/settings.gradle.kts index fa5386d..d3f0dd2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,6 +19,8 @@ pluginManagement { maven("https://repo.plo.su") maven("https://repo.plasmoverse.com/snapshots") + maven("https://maven.kikugie.dev/snapshots") + maven("https://repo.essential.gg/repository/maven-public") } } @@ -46,6 +48,7 @@ file("api").listFilesOrdered { include("common") include("common-integration") include("common-server") +include("common-proxy") include("spigot") include("minestom") include("velocity") diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index 0ce285b..2caa95e 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -3,7 +3,7 @@ import org.semver4j.Semver plugins { id("su.plo.slib.shadow-platform") - alias(libs.plugins.runpaper) + alias(libs.plugins.run.paper) } val testShadowBundle: Configuration by configurations.creating @@ -12,6 +12,12 @@ dependencies { compileOnly(libs.spigot) testCompileOnly(libs.spigot) + compileOnly(libs.semver4j) + shadow(libs.semver4j) + + compileOnly(libs.reflectionremapper) + shadow(libs.reflectionremapper) + testCompileOnly(testFixtures(project(":common-server"))) testShadowBundle(testFixtures(project(":common-server"))) @@ -45,13 +51,17 @@ repositories { tasks { java { - toolchain.languageVersion.set(JavaLanguageVersion.of(8)) + toolchain.languageVersion.set(JavaLanguageVersion.of(17)) } shadowJar { archiveClassifier = "all" relocate("net.kyori", "su.plo.slib.libs.adventure") + relocate("xyz.jpenilla.reflectionremapper", "su.plo.slib.libs.reflectionremapper") + relocate("net.fabricmc.mappingio", "su.plo.slib.libs.mappingio") + relocate("org.jspecify", "su.plo.slib.libs.jspecify") + relocate("org.semver4j", "su.plo.slib.libs.semver4j") } val finalJar = register("finalJar") { @@ -89,10 +99,9 @@ tasks { val mcSemVersion = Semver(mcVersion) val javaVersion = when { + mcSemVersion.satisfies(">=26.1") -> 25 mcSemVersion.satisfies(">=1.20.5") -> 21 - mcSemVersion.satisfies(">=1.18") -> 17 - mcSemVersion.satisfies(">=1.17") -> 16 - else -> 8 + else -> 17 } minecraftVersion(mcVersion) diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/SpigotServerLib.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/SpigotServerLib.kt index a841ad2..78ee889 100644 --- a/spigot/src/main/kotlin/su/plo/slib/spigot/SpigotServerLib.kt +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/SpigotServerLib.kt @@ -6,6 +6,7 @@ import net.kyori.adventure.platform.bukkit.BukkitAudiences import org.bukkit.Bukkit import org.bukkit.OfflinePlayer import org.bukkit.World +import org.bukkit.entity.Entity import org.bukkit.entity.LivingEntity import org.bukkit.entity.Player import org.bukkit.event.EventHandler @@ -38,7 +39,8 @@ import su.plo.slib.spigot.scheduler.SpigotServerScheduler import su.plo.slib.spigot.util.SchedulerUtil import su.plo.slib.spigot.world.SpigotServerWorld import java.io.File -import java.util.* +import java.util.Optional +import java.util.UUID class SpigotServerLib( private val loader: JavaPlugin @@ -54,6 +56,7 @@ class SpigotServerLib( .apply { parent = loader.logger.parent } } } + instance = this } private val worldByInstance: MutableMap = Maps.newConcurrentMap() @@ -155,7 +158,11 @@ class SpigotServerLib( McGameProfile(offlinePlayer.uniqueId, offlinePlayer.name ?: "", ImmutableList.of()) override fun getEntityByInstance(instance: Any): McServerEntity { - require(instance is LivingEntity) { "instance is not ${LivingEntity::class.java}" } + require(instance is Entity) { "instance is not ${Entity::class.java}" } + + if (instance is Player) { + return getPlayerByInstance(instance) + } return SpigotServerEntity( this, @@ -186,4 +193,8 @@ class SpigotServerLib( ) playerById.remove(event.player.uniqueId) } + + companion object { + lateinit var instance: SpigotServerLib + } } diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/chat/ComponentToMessageConverter.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/chat/ComponentToMessageConverter.kt new file mode 100644 index 0000000..84d9d04 --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/chat/ComponentToMessageConverter.kt @@ -0,0 +1,19 @@ +package su.plo.slib.spigot.chat + +import com.mojang.brigadier.Message +import net.kyori.adventure.platform.bukkit.MinecraftComponentSerializer +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer +import su.plo.slib.api.chat.component.McTextComponent +import su.plo.slib.api.chat.converter.MessageTextConverter +import su.plo.slib.chat.AdventureComponentTextConverter + +class ComponentToMessageConverter : MessageTextConverter { + private val textConverter = AdventureComponentTextConverter() + private val gson = GsonComponentSerializer.gson() + + override fun convert(text: McTextComponent): Message { + val json = textConverter.convertToJson(text) + val component = gson.deserialize(json) + return MinecraftComponentSerializer.get().serialize(component) as Message + } +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/command/SpigotCommandManager.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/command/SpigotCommandManager.kt index 20af439..55c33fe 100644 --- a/spigot/src/main/kotlin/su/plo/slib/spigot/command/SpigotCommandManager.kt +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/command/SpigotCommandManager.kt @@ -1,20 +1,26 @@ package su.plo.slib.spigot.command +import com.mojang.brigadier.context.CommandContext import org.bukkit.command.Command import org.bukkit.command.CommandSender import org.bukkit.command.SimpleCommandMap import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin import su.plo.slib.api.command.McCommand -import su.plo.slib.api.command.McCommandManager import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.command.brigadier.McBrigadierSource import su.plo.slib.api.logging.McLoggerFactory import su.plo.slib.api.server.event.command.McServerCommandsRegisterEvent +import su.plo.slib.command.AbstractCommandManager +import su.plo.slib.command.copyFor +import su.plo.slib.command.proxied import su.plo.slib.spigot.SpigotServerLib +import su.plo.slib.spigot.command.brigadier.SpigotBrigadierSource +import su.plo.slib.spigot.nms.getCommandDispatcher class SpigotCommandManager( private val minecraftServer: SpigotServerLib -) : McCommandManager() { +) : AbstractCommandManager() { private val logger = McLoggerFactory.createLogger("SpigotCommandManager") @@ -22,13 +28,27 @@ class SpigotCommandManager( fun registerCommands(loader: JavaPlugin) { McServerCommandsRegisterEvent.invoker.onCommandsRegister(this, minecraftServer) - commandByName.forEach { (name, command) -> + registerCommands { name, command -> val spigotCommand = SpigotCommand(minecraftServer, this, command, name) val commandMap = loader.commandMap() commandMap.register("plasmovoice", spigotCommand) } + try { + val dispatcher = loader.server.getCommandDispatcher() + registerBrigadierCommands { command -> + dispatcher.root.addChild( + command.proxied( + SpigotBrigadierSource::from, + { it.toMc() }, + ) + ) + } + } catch (e: Exception) { + logger.warn("Failed to get Brigadier dispatcher", e) + } + registered = true } @@ -71,3 +91,10 @@ class SpigotCommandManager( .also { it.isAccessible = true } .get(server) as SimpleCommandMap } + +fun CommandContext.toSourceStack(): CommandContext = + copyFor(source.getInstance()) + +fun CommandContext.toMc(): CommandContext = + copyFor(SpigotBrigadierSource.from(source)) + diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/command/brigadier/SpigotBrigadierArguments.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/command/brigadier/SpigotBrigadierArguments.kt new file mode 100644 index 0000000..a79cf2d --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/command/brigadier/SpigotBrigadierArguments.kt @@ -0,0 +1,141 @@ +package su.plo.slib.spigot.command.brigadier + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import su.plo.slib.api.command.brigadier.CustomArgumentType +import su.plo.slib.api.entity.player.McGameProfile +import su.plo.slib.api.server.command.brigadier.McArgumentTypes +import su.plo.slib.api.server.command.brigadier.McEntitiesArgumentResolver +import su.plo.slib.api.server.command.brigadier.McEntityArgumentResolver +import su.plo.slib.api.server.command.brigadier.McGameProfilesArgumentResolver +import su.plo.slib.api.server.command.brigadier.McPlayerArgumentResolver +import su.plo.slib.api.server.command.brigadier.McPlayersArgumentResolver +import su.plo.slib.api.server.command.brigadier.ServerPos3dResolver +import su.plo.slib.api.server.entity.McServerEntity +import su.plo.slib.api.server.position.ServerPos3d +import su.plo.slib.spigot.SpigotServerLib +import su.plo.slib.spigot.nms.ReflectionProxies +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.UndeclaredThrowableException +import java.util.UUID + +class SpigotBrigadierArguments: McArgumentTypes.Provider { + private val serverLib by lazy { SpigotServerLib.instance } + + override fun entity(): ArgumentType = + argumentResolver(ReflectionProxies.entityArgument.entity()) { selector -> + McEntityArgumentResolver { source -> + val entity = rethrowProxyException { + ReflectionProxies.entitySelector.findSingleEntity(selector, source.getInstance()) + } + val bukkitEntity = ReflectionProxies.entity.getBukkitEntity(entity) + + serverLib.getEntityByInstance(bukkitEntity) + } + } + + override fun entities(): ArgumentType = + argumentResolver(ReflectionProxies.entityArgument.entities()) { selector -> + McEntitiesArgumentResolver { source -> + val entities = rethrowProxyException { + ReflectionProxies.entitySelector.findEntities(selector, source.getInstance()) + } + entities.map { + val bukkitEntity = ReflectionProxies.entity.getBukkitEntity(it) + serverLib.getEntityByInstance(bukkitEntity) + } + } + } + + override fun player(): ArgumentType = + argumentResolver(ReflectionProxies.entityArgument.player()) { selector -> + McPlayerArgumentResolver { source -> + val player = rethrowProxyException { + ReflectionProxies.entitySelector.findSinglePlayer(selector, source.getInstance()) + } + val bukkitPlayer = ReflectionProxies.entity.getBukkitEntity(player) + serverLib.getPlayerByInstance(bukkitPlayer) + } + } + + override fun players(): ArgumentType = + argumentResolver(ReflectionProxies.entityArgument.players()) { selector -> + McPlayersArgumentResolver { source -> + val players = rethrowProxyException { + ReflectionProxies.entitySelector.findPlayers(selector, source.getInstance()) + } + players.map { + val bukkitPlayer = ReflectionProxies.entity.getBukkitEntity(it) + serverLib.getPlayerByInstance(bukkitPlayer) + } + } + } + + override fun gameProfiles(): ArgumentType = + argumentResolver(ReflectionProxies.gameProfileArgument.gameProfile()) { selector -> + McGameProfilesArgumentResolver { source -> + val getNamesMethod = selector.javaClass.declaredMethods + .first() + .also { it.isAccessible = true } + + @Suppress("UNCHECKED_CAST") + val names = rethrowProxyException { + getNamesMethod.invoke(selector, source.getInstance()) as Collection + } + + names.map { + // 1.21.9+ uses NameAndId, so we can't just use GameProfile here + val uuidMethod = it.javaClass.declaredMethods + .first { it.returnType == UUID::class.java } + .also { it.isAccessible = true } + val nameMethod = it.javaClass.declaredMethods + .first { it.returnType == String::class.java } + .also { it.isAccessible = true } + + val uuid = uuidMethod.invoke(it) as UUID + val name = nameMethod.invoke(it) as String + + McGameProfile(uuid, name, emptyList()) + } + } + } + + override fun position(): ArgumentType = + argumentResolver(ReflectionProxies.blockPosArgument.blockPos()) { coordinates -> + ServerPos3dResolver { source -> + val position = ReflectionProxies.coordinates.getPosition(coordinates, source.getInstance()) + val rotation = ReflectionProxies.coordinates.getRotation(coordinates, source.getInstance()) + + val world = (source.executor as? McServerEntity)?.world + + ServerPos3d( + world, + ReflectionProxies.vec3Proxy.x(position), + ReflectionProxies.vec3Proxy.y(position), + ReflectionProxies.vec3Proxy.z(position), + ReflectionProxies.vec2Proxy.y(rotation), + ReflectionProxies.vec2Proxy.x(rotation), + ) + } + } + + private fun argumentResolver( + nativeType: ArgumentType, + resolverFactory: (Any) -> T, + ): CustomArgumentType = + object : CustomArgumentType { + override val nativeType = nativeType + + override fun parse(reader: StringReader) = resolverFactory(nativeType.parse(reader)) + } + + private fun rethrowProxyException(block: () -> T): T { + try { + return block() + } catch (e: InvocationTargetException) { + throw e.targetException + } catch (e: UndeclaredThrowableException) { + throw e.undeclaredThrowable + } + } +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/command/brigadier/SpigotBrigadierSource.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/command/brigadier/SpigotBrigadierSource.kt new file mode 100644 index 0000000..8ec9cf4 --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/command/brigadier/SpigotBrigadierSource.kt @@ -0,0 +1,32 @@ +package su.plo.slib.spigot.command.brigadier + +import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.command.brigadier.McBrigadierSource +import su.plo.slib.api.entity.McEntity +import su.plo.slib.spigot.SpigotServerLib +import su.plo.slib.spigot.nms.ReflectionProxies + +data class SpigotBrigadierSource( + override val source: McCommandSource, + override val executor: McEntity?, + private val instance: Any, +): McBrigadierSource { + + @Suppress("UNCHECKED_CAST") + override fun getInstance(): T = + instance as T + + companion object { + private val minecraftServer by lazy { SpigotServerLib.instance } + + fun from(sourceStack: Any): SpigotBrigadierSource { + val source = ReflectionProxies.commandSourceStack.getBukkitSender(sourceStack) + .let { minecraftServer.commandManager.getCommandSource(it) } + val entity = ReflectionProxies.commandSourceStack.getEntity(sourceStack) + ?.let { ReflectionProxies.entity.getBukkitEntity(it) } + ?.let { minecraftServer.getEntityByInstance(it) } + + return SpigotBrigadierSource(source, entity, sourceStack) + } + } +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/entity/SpigotServerEntity.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/entity/SpigotServerEntity.kt index 802c770..6946a7e 100644 --- a/spigot/src/main/kotlin/su/plo/slib/spigot/entity/SpigotServerEntity.kt +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/entity/SpigotServerEntity.kt @@ -1,6 +1,7 @@ package su.plo.slib.spigot.entity import org.bukkit.Location +import org.bukkit.entity.Entity import org.bukkit.entity.LivingEntity import su.plo.slib.api.position.Pos3d import su.plo.slib.api.server.entity.McServerEntity @@ -9,7 +10,7 @@ import su.plo.slib.api.server.world.McServerWorld import su.plo.slib.spigot.SpigotServerLib import java.util.* -open class SpigotServerEntity( +open class SpigotServerEntity( protected val minecraftServer: SpigotServerLib, protected val instance: E ) : McServerEntity { @@ -26,7 +27,12 @@ open class SpigotServerEntity( get() = instance.uniqueId override val eyeHeight: Double - get() = instance.eyeHeight + get() = + if (instance is LivingEntity) { + instance.eyeHeight + } else { + 0.0 + } override val hitBoxWidth: Float get() = instance.boundingBox.widthX.toFloat() diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/nms/BlockPosArgumentProxy.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/BlockPosArgumentProxy.kt new file mode 100644 index 0000000..8a028f7 --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/BlockPosArgumentProxy.kt @@ -0,0 +1,13 @@ +package su.plo.slib.spigot.nms + +import com.mojang.brigadier.arguments.ArgumentType +import xyz.jpenilla.reflectionremapper.proxy.annotation.Proxies +import xyz.jpenilla.reflectionremapper.proxy.annotation.Static + +@Proxies( + className = "net.minecraft.commands.arguments.coordinates.BlockPosArgument" +) +interface BlockPosArgumentProxy { + @Static + fun blockPos(): ArgumentType +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/nms/CommandSourceStackProxy.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/CommandSourceStackProxy.kt new file mode 100644 index 0000000..71eb149 --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/CommandSourceStackProxy.kt @@ -0,0 +1,20 @@ +package su.plo.slib.spigot.nms + +import org.bukkit.command.CommandSender +import xyz.jpenilla.reflectionremapper.proxy.annotation.FieldGetter +import xyz.jpenilla.reflectionremapper.proxy.annotation.Proxies +import xyz.jpenilla.reflectionremapper.proxy.annotation.Type + +@Proxies( + className = "net.minecraft.commands.CommandSourceStack" +) +interface CommandSourceStackProxy { + fun getBukkitSender( + @Type(className = "net.minecraft.commands.CommandSourceStack") instance: Any, + ): CommandSender + + @FieldGetter("entity") + fun getEntity( + @Type(className = "net.minecraft.commands.CommandSourceStack") instance: Any, + ): Any? +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/nms/CoordinatesProxy.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/CoordinatesProxy.kt new file mode 100644 index 0000000..fc47cf3 --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/CoordinatesProxy.kt @@ -0,0 +1,19 @@ +package su.plo.slib.spigot.nms + +import xyz.jpenilla.reflectionremapper.proxy.annotation.Proxies +import xyz.jpenilla.reflectionremapper.proxy.annotation.Type + +@Proxies( + className = "net.minecraft.commands.arguments.coordinates.Coordinates" +) +interface CoordinatesProxy { + fun getPosition( + @Type(className = "net.minecraft.commands.arguments.selector.EntitySelector") instance: Any, + @Type(className = "net.minecraft.commands.CommandSourceStack") source: Any, + ): Any + + fun getRotation( + @Type(className = "net.minecraft.commands.arguments.selector.EntitySelector") instance: Any, + @Type(className = "net.minecraft.commands.CommandSourceStack") source: Any, + ): Any +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/nms/EntityArgumentProxy.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/EntityArgumentProxy.kt new file mode 100644 index 0000000..d5a9b37 --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/EntityArgumentProxy.kt @@ -0,0 +1,22 @@ +package su.plo.slib.spigot.nms + +import com.mojang.brigadier.arguments.ArgumentType +import xyz.jpenilla.reflectionremapper.proxy.annotation.Proxies +import xyz.jpenilla.reflectionremapper.proxy.annotation.Static + +@Proxies( + className = "net.minecraft.commands.arguments.EntityArgument" +) +interface EntityArgumentProxy { + @Static + fun entity(): ArgumentType + + @Static + fun entities(): ArgumentType + + @Static + fun player(): ArgumentType + + @Static + fun players(): ArgumentType +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/nms/EntityProxy.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/EntityProxy.kt new file mode 100644 index 0000000..7144e0b --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/EntityProxy.kt @@ -0,0 +1,14 @@ +package su.plo.slib.spigot.nms + +import org.bukkit.entity.Entity +import xyz.jpenilla.reflectionremapper.proxy.annotation.Proxies +import xyz.jpenilla.reflectionremapper.proxy.annotation.Type + +@Proxies( + className = "net.minecraft.world.entity.Entity" +) +interface EntityProxy { + fun getBukkitEntity( + @Type(className = "net.minecraft.world.entity.Entity") instance: Any, + ): Entity +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/nms/EntitySelectorProxy.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/EntitySelectorProxy.kt new file mode 100644 index 0000000..183cd51 --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/EntitySelectorProxy.kt @@ -0,0 +1,29 @@ +package su.plo.slib.spigot.nms + +import xyz.jpenilla.reflectionremapper.proxy.annotation.Proxies +import xyz.jpenilla.reflectionremapper.proxy.annotation.Type + +@Proxies( + className = "net.minecraft.commands.arguments.selector.EntitySelector" +) +interface EntitySelectorProxy { + fun findSingleEntity( + @Type(className = "net.minecraft.commands.arguments.selector.EntitySelector") instance: Any, + @Type(className = "net.minecraft.commands.CommandSourceStack") source: Any, + ): Any + + fun findEntities( + @Type(className = "net.minecraft.commands.arguments.selector.EntitySelector") instance: Any, + @Type(className = "net.minecraft.commands.CommandSourceStack") source: Any, + ): List + + fun findSinglePlayer( + @Type(className = "net.minecraft.commands.arguments.selector.EntitySelector") instance: Any, + @Type(className = "net.minecraft.commands.CommandSourceStack") source: Any, + ): Any + + fun findPlayers( + @Type(className = "net.minecraft.commands.arguments.selector.EntitySelector") instance: Any, + @Type(className = "net.minecraft.commands.CommandSourceStack") source: Any, + ): List +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/nms/GameProfileArgumentProxy.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/GameProfileArgumentProxy.kt new file mode 100644 index 0000000..b908e2d --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/GameProfileArgumentProxy.kt @@ -0,0 +1,13 @@ +package su.plo.slib.spigot.nms + +import com.mojang.brigadier.arguments.ArgumentType +import xyz.jpenilla.reflectionremapper.proxy.annotation.Proxies +import xyz.jpenilla.reflectionremapper.proxy.annotation.Static + +@Proxies( + className = "net.minecraft.commands.arguments.GameProfileArgument" +) +interface GameProfileArgumentProxy { + @Static + fun gameProfile(): ArgumentType +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/nms/Reflections.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/Reflections.kt new file mode 100644 index 0000000..70b6485 --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/Reflections.kt @@ -0,0 +1,95 @@ +package su.plo.slib.spigot.nms + +import com.mojang.brigadier.CommandDispatcher +import org.bukkit.Bukkit +import org.bukkit.Server +import org.semver4j.Semver +import su.plo.slib.api.logging.McLoggerFactory +import xyz.jpenilla.reflectionremapper.ReflectionRemapper +import xyz.jpenilla.reflectionremapper.proxy.ReflectionProxyFactory + +object ReflectionProxies { + private val logger = McLoggerFactory.createLogger("ReflectionProxies") + + val commandsClass: Class<*> + val commandSourceStack: CommandSourceStackProxy + val entity: EntityProxy + val entityArgument: EntityArgumentProxy + val entitySelector: EntitySelectorProxy + val blockPosArgument: BlockPosArgumentProxy + val coordinates: CoordinatesProxy + val vec3Proxy: Vec3Proxy + val vec2Proxy: Vec2Proxy + val gameProfileArgument: GameProfileArgumentProxy + + init { + val bukkitVersion = Bukkit.getVersion() + val minecraftVersionString = bukkitVersion.substring(bukkitVersion.lastIndexOf(" ") + 1, bukkitVersion.length - 1) + val minecraftVersion = Semver(minecraftVersionString) + + val remapper = + try { + ReflectionRemapper.forReobfMappingsInPaperJar() + .also { logger.info("Using mappings from paper jar") } + } catch (_: Throwable) { + val mappingsVersion = listOf( + "1.21.4", + "1.19.2", + "1.17.1", + "1.16.5", + ) + .firstOrNull { minecraftVersion.satisfies(">=$it") } + ?: throw IllegalStateException("$minecraftVersionString is not supported") + + ReflectionRemapper.forMappings( + javaClass.classLoader.getResourceAsStream("mappings/$mappingsVersion.tiny")!!, + "source", + "target", + ) + .also { + logger.info("Using mappings from resources: \"mappings/$mappingsVersion.tiny\"") + } + } + + val proxyFactory = ReflectionProxyFactory.create(remapper, javaClass.classLoader) + + commandsClass = Class.forName(remapper.remapClassName("net.minecraft.commands.Commands")) + commandSourceStack = proxyFactory.reflectionProxy() + entity = proxyFactory.reflectionProxy() + entityArgument = proxyFactory.reflectionProxy() + entitySelector = proxyFactory.reflectionProxy() + blockPosArgument = proxyFactory.reflectionProxy() + coordinates = proxyFactory.reflectionProxy() + vec3Proxy = proxyFactory.reflectionProxy() + vec2Proxy = proxyFactory.reflectionProxy() + gameProfileArgument = proxyFactory.reflectionProxy() + } + + private inline fun ReflectionProxyFactory.reflectionProxy() = + reflectionProxy(T::class.java) +} + +@Suppress("UNCHECKED_CAST") +fun Server.getCommandDispatcher(): CommandDispatcher { + val minecraftServer = getMinecraftServer() + + val getCommandsMethod = minecraftServer.javaClass.methods + .first { it.returnType == ReflectionProxies.commandsClass } + + val commands = getCommandsMethod.invoke(minecraftServer) + + val dispatcherField = commands.javaClass.declaredFields + .first { it.type == CommandDispatcher::class.java } + dispatcherField.isAccessible = true + + val dispatcher = dispatcherField.get(commands) + + return dispatcher as CommandDispatcher +} + +fun Server.getMinecraftServer(): Any { + val getServerMethod = javaClass.getMethod("getServer") + val minecraftServer = getServerMethod.invoke(this) + + return minecraftServer +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/nms/Vec2Proxy.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/Vec2Proxy.kt new file mode 100644 index 0000000..1a40777 --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/Vec2Proxy.kt @@ -0,0 +1,20 @@ +package su.plo.slib.spigot.nms + +import xyz.jpenilla.reflectionremapper.proxy.annotation.FieldGetter +import xyz.jpenilla.reflectionremapper.proxy.annotation.Proxies +import xyz.jpenilla.reflectionremapper.proxy.annotation.Type + +@Proxies( + className = "net.minecraft.world.phys.Vec2" +) +interface Vec2Proxy { + @FieldGetter("x") + fun x( + @Type(className = "net.minecraft.world.phys.Vec2") instance: Any, + ): Float + + @FieldGetter("y") + fun y( + @Type(className = "net.minecraft.world.phys.Vec2") instance: Any, + ): Float +} diff --git a/spigot/src/main/kotlin/su/plo/slib/spigot/nms/Vec3Proxy.kt b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/Vec3Proxy.kt new file mode 100644 index 0000000..f0647d2 --- /dev/null +++ b/spigot/src/main/kotlin/su/plo/slib/spigot/nms/Vec3Proxy.kt @@ -0,0 +1,21 @@ +package su.plo.slib.spigot.nms + +import xyz.jpenilla.reflectionremapper.proxy.annotation.Proxies +import xyz.jpenilla.reflectionremapper.proxy.annotation.Type + +@Proxies( + className = "net.minecraft.world.phys.Vec3" +) +interface Vec3Proxy { + fun x( + @Type(className = "net.minecraft.world.phys.Vec3") instance: Any, + ): Double + + fun y( + @Type(className = "net.minecraft.world.phys.Vec3") instance: Any, + ): Double + + fun z( + @Type(className = "net.minecraft.world.phys.Vec3") instance: Any, + ): Double +} diff --git a/spigot/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter b/spigot/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter new file mode 100644 index 0000000..3313f66 --- /dev/null +++ b/spigot/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter @@ -0,0 +1 @@ +su.plo.slib.spigot.chat.ComponentToMessageConverter diff --git a/spigot/src/main/resources/META-INF/services/su.plo.slib.api.server.command.brigadier.McArgumentTypes$Provider b/spigot/src/main/resources/META-INF/services/su.plo.slib.api.server.command.brigadier.McArgumentTypes$Provider new file mode 100644 index 0000000..6ea8415 --- /dev/null +++ b/spigot/src/main/resources/META-INF/services/su.plo.slib.api.server.command.brigadier.McArgumentTypes$Provider @@ -0,0 +1 @@ +su.plo.slib.spigot.command.brigadier.SpigotBrigadierArguments diff --git a/spigot/src/main/resources/mappings/1.16.5.tiny b/spigot/src/main/resources/mappings/1.16.5.tiny new file mode 100644 index 0000000..faa6b64 --- /dev/null +++ b/spigot/src/main/resources/mappings/1.16.5.tiny @@ -0,0 +1,29 @@ +tiny 2 0 source target +c net/minecraft/commands/CommandSourceStack net/minecraft/server/v1_16_R3/CommandListenerWrapper + f Lnet/minecraft/world/entity/Entity; entity k +c net/minecraft/commands/Commands net/minecraft/server/v1_16_R3/CommandDispatcher +c net/minecraft/commands/arguments/EntityArgument net/minecraft/server/v1_16_R3/ArgumentEntity + m ()Lnet/minecraft/commands/arguments/EntityArgument; entity a + m ()Lnet/minecraft/commands/arguments/EntityArgument; entities multipleEntities + m ()Lnet/minecraft/commands/arguments/EntityArgument; player c + m ()Lnet/minecraft/commands/arguments/EntityArgument; players d +c net/minecraft/commands/arguments/GameProfileArgument net/minecraft/server/v1_16_R3/ArgumentProfile + m ()Lnet/minecraft/commands/arguments/GameProfileArgument; gameProfile a +c net/minecraft/commands/arguments/coordinates/BlockPosArgument net/minecraft/server/v1_16_R3/ArgumentPosition + m ()Lnet/minecraft/commands/arguments/coordinates/BlockPosArgument; blockPos a +c net/minecraft/commands/arguments/coordinates/Coordinates net/minecraft/server/v1_16_R3/IVectorPosition + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec3; getPosition a + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec2; getRotation b +c net/minecraft/commands/arguments/selector/EntitySelector net/minecraft/server/v1_16_R3/EntitySelector + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/entity/Entity; findSingleEntity a + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findEntities getEntities + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/server/level/ServerPlayer; findSinglePlayer c + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findPlayers d +c net/minecraft/world/entity/Entity net/minecraft/server/v1_16_R3/Entity +c net/minecraft/world/phys/Vec2 net/minecraft/server/v1_16_R3/Vec2F + f F x i + f F y j +c net/minecraft/world/phys/Vec3 net/minecraft/server/v1_16_R3/Vec3D + m ()D x getX + m ()D y getY + m ()D z getZ diff --git a/spigot/src/main/resources/mappings/1.17.1.tiny b/spigot/src/main/resources/mappings/1.17.1.tiny new file mode 100644 index 0000000..2dc7a7c --- /dev/null +++ b/spigot/src/main/resources/mappings/1.17.1.tiny @@ -0,0 +1,29 @@ +tiny 2 0 source target +c net/minecraft/commands/CommandSourceStack net/minecraft/commands/CommandListenerWrapper + f Lnet/minecraft/world/entity/Entity; entity k +c net/minecraft/commands/Commands net/minecraft/commands/CommandDispatcher +c net/minecraft/commands/arguments/EntityArgument net/minecraft/commands/arguments/ArgumentEntity + m ()Lnet/minecraft/commands/arguments/EntityArgument; entity a + m ()Lnet/minecraft/commands/arguments/EntityArgument; entities multipleEntities + m ()Lnet/minecraft/commands/arguments/EntityArgument; player c + m ()Lnet/minecraft/commands/arguments/EntityArgument; players d +c net/minecraft/commands/arguments/GameProfileArgument net/minecraft/commands/arguments/ArgumentProfile + m ()Lnet/minecraft/commands/arguments/GameProfileArgument; gameProfile a +c net/minecraft/commands/arguments/coordinates/BlockPosArgument net/minecraft/commands/arguments/coordinates/ArgumentPosition + m ()Lnet/minecraft/commands/arguments/coordinates/BlockPosArgument; blockPos a +c net/minecraft/commands/arguments/coordinates/Coordinates net/minecraft/commands/arguments/coordinates/IVectorPosition + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec3; getPosition a + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec2; getRotation b +c net/minecraft/commands/arguments/selector/EntitySelector net/minecraft/commands/arguments/selector/EntitySelector + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/entity/Entity; findSingleEntity a + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findEntities getEntities + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/server/level/ServerPlayer; findSinglePlayer c + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findPlayers d +c net/minecraft/world/entity/Entity net/minecraft/world/entity/Entity +c net/minecraft/world/phys/Vec2 net/minecraft/world/phys/Vec2F + f F x i + f F y j +c net/minecraft/world/phys/Vec3 net/minecraft/world/phys/Vec3D + m ()D x getX + m ()D y getY + m ()D z getZ diff --git a/spigot/src/main/resources/mappings/1.18.2.tiny b/spigot/src/main/resources/mappings/1.18.2.tiny new file mode 100644 index 0000000..f66aee8 --- /dev/null +++ b/spigot/src/main/resources/mappings/1.18.2.tiny @@ -0,0 +1,29 @@ +tiny 2 0 source target +c net/minecraft/commands/CommandSourceStack net/minecraft/commands/CommandListenerWrapper + f Lnet/minecraft/world/entity/Entity; entity k +c net/minecraft/commands/Commands net/minecraft/commands/CommandDispatcher +c net/minecraft/commands/arguments/EntityArgument net/minecraft/commands/arguments/ArgumentEntity + m ()Lnet/minecraft/commands/arguments/EntityArgument; entity a + m ()Lnet/minecraft/commands/arguments/EntityArgument; entities multipleEntities + m ()Lnet/minecraft/commands/arguments/EntityArgument; player c + m ()Lnet/minecraft/commands/arguments/EntityArgument; players d +c net/minecraft/commands/arguments/GameProfileArgument net/minecraft/commands/arguments/ArgumentProfile + m ()Lnet/minecraft/commands/arguments/GameProfileArgument; gameProfile a +c net/minecraft/commands/arguments/coordinates/BlockPosArgument net/minecraft/commands/arguments/coordinates/ArgumentPosition + m ()Lnet/minecraft/commands/arguments/coordinates/BlockPosArgument; blockPos a +c net/minecraft/commands/arguments/coordinates/Coordinates net/minecraft/commands/arguments/coordinates/IVectorPosition + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec3; getPosition a + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec2; getRotation b +c net/minecraft/commands/arguments/selector/EntitySelector net/minecraft/commands/arguments/selector/EntitySelector + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/entity/Entity; findSingleEntity a + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findEntities getEntities + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/server/level/ServerPlayer; findSinglePlayer c + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findPlayers d +c net/minecraft/world/entity/Entity net/minecraft/world/entity/Entity +c net/minecraft/world/phys/Vec2 net/minecraft/world/phys/Vec2F + f F x i + f F y j +c net/minecraft/world/phys/Vec3 net/minecraft/world/phys/Vec3D + m ()D x a + m ()D y b + m ()D z c diff --git a/spigot/src/main/resources/mappings/1.19.2.tiny b/spigot/src/main/resources/mappings/1.19.2.tiny new file mode 100644 index 0000000..2cc64db --- /dev/null +++ b/spigot/src/main/resources/mappings/1.19.2.tiny @@ -0,0 +1,29 @@ +tiny 2 0 source target +c net/minecraft/commands/CommandSourceStack net/minecraft/commands/CommandListenerWrapper + f Lnet/minecraft/world/entity/Entity; entity k +c net/minecraft/commands/Commands net/minecraft/commands/CommandDispatcher +c net/minecraft/commands/arguments/EntityArgument net/minecraft/commands/arguments/ArgumentEntity + m ()Lnet/minecraft/commands/arguments/EntityArgument; entity a + m ()Lnet/minecraft/commands/arguments/EntityArgument; entities b + m ()Lnet/minecraft/commands/arguments/EntityArgument; player c + m ()Lnet/minecraft/commands/arguments/EntityArgument; players d +c net/minecraft/commands/arguments/GameProfileArgument net/minecraft/commands/arguments/ArgumentProfile + m ()Lnet/minecraft/commands/arguments/GameProfileArgument; gameProfile a +c net/minecraft/commands/arguments/coordinates/BlockPosArgument net/minecraft/commands/arguments/coordinates/ArgumentPosition + m ()Lnet/minecraft/commands/arguments/coordinates/BlockPosArgument; blockPos a +c net/minecraft/commands/arguments/coordinates/Coordinates net/minecraft/commands/arguments/coordinates/IVectorPosition + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec3; getPosition a + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec2; getRotation b +c net/minecraft/commands/arguments/selector/EntitySelector net/minecraft/commands/arguments/selector/EntitySelector + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/entity/Entity; findSingleEntity a + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findEntities b + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/server/level/ServerPlayer; findSinglePlayer c + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findPlayers d +c net/minecraft/world/entity/Entity net/minecraft/world/entity/Entity +c net/minecraft/world/phys/Vec2 net/minecraft/world/phys/Vec2F + f F x i + f F y j +c net/minecraft/world/phys/Vec3 net/minecraft/world/phys/Vec3D + m ()D x a + m ()D y b + m ()D z c diff --git a/spigot/src/main/resources/mappings/1.21.4.tiny b/spigot/src/main/resources/mappings/1.21.4.tiny new file mode 100644 index 0000000..8305cea --- /dev/null +++ b/spigot/src/main/resources/mappings/1.21.4.tiny @@ -0,0 +1,29 @@ +tiny 2 0 source target +c net/minecraft/commands/CommandSourceStack net/minecraft/commands/CommandListenerWrapper + f Lnet/minecraft/world/entity/Entity; entity l +c net/minecraft/commands/Commands net/minecraft/commands/CommandDispatcher +c net/minecraft/commands/arguments/EntityArgument net/minecraft/commands/arguments/ArgumentEntity + m ()Lnet/minecraft/commands/arguments/EntityArgument; entity a + m ()Lnet/minecraft/commands/arguments/EntityArgument; entities b + m ()Lnet/minecraft/commands/arguments/EntityArgument; player c + m ()Lnet/minecraft/commands/arguments/EntityArgument; players d +c net/minecraft/commands/arguments/GameProfileArgument net/minecraft/commands/arguments/ArgumentProfile + m ()Lnet/minecraft/commands/arguments/GameProfileArgument; gameProfile a +c net/minecraft/commands/arguments/coordinates/BlockPosArgument net/minecraft/commands/arguments/coordinates/ArgumentPosition + m ()Lnet/minecraft/commands/arguments/coordinates/BlockPosArgument; blockPos a +c net/minecraft/commands/arguments/coordinates/Coordinates net/minecraft/commands/arguments/coordinates/IVectorPosition + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec3; getPosition a + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec2; getRotation b +c net/minecraft/commands/arguments/selector/EntitySelector net/minecraft/commands/arguments/selector/EntitySelector + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/entity/Entity; findSingleEntity a + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findEntities b + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/server/level/ServerPlayer; findSinglePlayer c + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findPlayers d +c net/minecraft/world/entity/Entity net/minecraft/world/entity/Entity +c net/minecraft/world/phys/Vec2 net/minecraft/world/phys/Vec2F + f F x i + f F y j +c net/minecraft/world/phys/Vec3 net/minecraft/world/phys/Vec3D + m ()D x a + m ()D y b + m ()D z c diff --git a/spigot/src/main/resources/mappings/1.21.5.tiny b/spigot/src/main/resources/mappings/1.21.5.tiny new file mode 100644 index 0000000..c30e4e6 --- /dev/null +++ b/spigot/src/main/resources/mappings/1.21.5.tiny @@ -0,0 +1,29 @@ +tiny 2 0 source target +c net/minecraft/commands/CommandSourceStack net/minecraft/commands/CommandListenerWrapper + f Lnet/minecraft/world/entity/Entity; entity l +c net/minecraft/commands/Commands net/minecraft/commands/CommandDispatcher +c net/minecraft/commands/arguments/EntityArgument net/minecraft/commands/arguments/ArgumentEntity + m ()Lnet/minecraft/commands/arguments/EntityArgument; entity a + m ()Lnet/minecraft/commands/arguments/EntityArgument; entities b + m ()Lnet/minecraft/commands/arguments/EntityArgument; player c + m ()Lnet/minecraft/commands/arguments/EntityArgument; players d +c net/minecraft/commands/arguments/GameProfileArgument net/minecraft/commands/arguments/ArgumentProfile + m ()Lnet/minecraft/commands/arguments/GameProfileArgument; gameProfile a +c net/minecraft/commands/arguments/coordinates/BlockPosArgument net/minecraft/commands/arguments/coordinates/ArgumentPosition + m ()Lnet/minecraft/commands/arguments/coordinates/BlockPosArgument; blockPos a +c net/minecraft/commands/arguments/coordinates/Coordinates net/minecraft/commands/arguments/coordinates/IVectorPosition + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec3; getPosition a + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/phys/Vec2; getRotation b +c net/minecraft/commands/arguments/selector/EntitySelector net/minecraft/commands/arguments/selector/EntitySelector + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/world/entity/Entity; findSingleEntity a + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findEntities b + m (Lnet/minecraft/commands/CommandSourceStack;)Lnet/minecraft/server/level/ServerPlayer; findSinglePlayer c + m (Lnet/minecraft/commands/CommandSourceStack;)Ljava/util/List; findPlayers d +c net/minecraft/world/entity/Entity net/minecraft/world/entity/Entity +c net/minecraft/world/phys/Vec2 net/minecraft/world/phys/Vec2F + f F x j + f F y k +c net/minecraft/world/phys/Vec3 net/minecraft/world/phys/Vec3D + m ()D x a + m ()D y b + m ()D z c diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index f2a074f..a24d12d 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -1,11 +1,22 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + plugins { id("su.plo.slib.shadow-platform") + alias(libs.plugins.run.velocity) + kotlin("kapt") } +val testShadowBundle: Configuration by configurations.creating + dependencies { compileOnly(libs.velocity) testCompileOnly(libs.velocity) + testCompileOnly(testFixtures(project(":common-proxy"))) + testShadowBundle(testFixtures(project(":common-proxy"))) + + kaptTest(libs.velocity) + compileOnly(project(":common")) listOf( project(":api:api-common"), @@ -22,8 +33,27 @@ repositories { maven("https://repo.papermc.io/repository/maven-public/") } +runVelocityExtension { + disablePluginJarDetection() +} + tasks { java { - toolchain.languageVersion.set(JavaLanguageVersion.of(11)) + toolchain.languageVersion.set(JavaLanguageVersion.of(17)) + } + + val testJar = + register("testJar", ShadowJar::class) { + configurations = listOf(testShadowBundle) + + archiveClassifier.set("test") + + from(zipTree(shadowJar.get().archiveFile)) + from(sourceSets.test.get().output) + } + + runVelocity { + velocityVersion("3.4.0-SNAPSHOT") + pluginJars.from(testJar) } } diff --git a/velocity/src/main/kotlin/su/plo/slib/velocity/VelocityProxyLib.kt b/velocity/src/main/kotlin/su/plo/slib/velocity/VelocityProxyLib.kt index 8f567dd..11cca08 100644 --- a/velocity/src/main/kotlin/su/plo/slib/velocity/VelocityProxyLib.kt +++ b/velocity/src/main/kotlin/su/plo/slib/velocity/VelocityProxyLib.kt @@ -36,6 +36,7 @@ class VelocityProxyLib( init { McLoggerFactory.supplier = McLoggerFactory.Supplier { name -> Slf4jLogger(name) } + instance = this } private val playerById: MutableMap = Maps.newConcurrentMap() @@ -155,4 +156,8 @@ class VelocityProxyLib( getServerInfoByServerInstance(it) } } + + companion object { + lateinit var instance: VelocityProxyLib + } } diff --git a/velocity/src/main/kotlin/su/plo/slib/velocity/chat/ComponentToMessageConverter.kt b/velocity/src/main/kotlin/su/plo/slib/velocity/chat/ComponentToMessageConverter.kt new file mode 100644 index 0000000..328aa47 --- /dev/null +++ b/velocity/src/main/kotlin/su/plo/slib/velocity/chat/ComponentToMessageConverter.kt @@ -0,0 +1,18 @@ +package su.plo.slib.velocity.chat + +import com.mojang.brigadier.Message +import com.velocitypowered.api.command.VelocityBrigadierMessage +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer +import su.plo.slib.api.chat.component.McTextComponent +import su.plo.slib.api.chat.converter.MessageTextConverter +import su.plo.slib.chat.AdventureComponentTextConverter + +class ComponentToMessageConverter : MessageTextConverter { + private val textConverter = AdventureComponentTextConverter() + private val gson = GsonComponentSerializer.gson() + + override fun convert(text: McTextComponent): Message { + val json = textConverter.convertToJson(text) + return VelocityBrigadierMessage.tooltip(gson.deserialize(json)) + } +} diff --git a/velocity/src/main/kotlin/su/plo/slib/velocity/command/VelocityCommandManager.kt b/velocity/src/main/kotlin/su/plo/slib/velocity/command/VelocityCommandManager.kt index 5331870..3d4f553 100644 --- a/velocity/src/main/kotlin/su/plo/slib/velocity/command/VelocityCommandManager.kt +++ b/velocity/src/main/kotlin/su/plo/slib/velocity/command/VelocityCommandManager.kt @@ -1,20 +1,26 @@ package su.plo.slib.velocity.command +import com.mojang.brigadier.context.CommandContext +import com.velocitypowered.api.command.BrigadierCommand import com.velocitypowered.api.command.CommandSource import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.command.CommandExecuteEvent import com.velocitypowered.api.proxy.Player import com.velocitypowered.api.proxy.ProxyServer -import su.plo.slib.api.command.McCommandManager import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.command.brigadier.McBrigadierSource import su.plo.slib.api.proxy.McProxyLib import su.plo.slib.api.proxy.command.McProxyCommand import su.plo.slib.api.proxy.event.command.McProxyCommandExecuteEvent +import su.plo.slib.command.AbstractCommandManager +import su.plo.slib.command.copyFor +import su.plo.slib.command.proxied +import su.plo.slib.velocity.command.brigadier.VelocityBrigadierSource import su.plo.slib.velocity.extension.textConverter class VelocityCommandManager( private val minecraftProxy: McProxyLib -) : McCommandManager() { +) : AbstractCommandManager() { @Subscribe fun onCommandExecute(event: CommandExecuteEvent) { @@ -51,11 +57,26 @@ class VelocityCommandManager( @Synchronized fun registerCommands(proxyServer: ProxyServer) { - commandByName.forEach { (name, command) -> - // todo: group commands and use aliases? + registerCommands { name, command -> val velocityCommand = VelocityCommand(minecraftProxy, this, command) proxyServer.commandManager.register(name, velocityCommand) } + + registerBrigadierCommands { command -> + @Suppress("UNCHECKED_CAST") + val brigadierCommand = BrigadierCommand( + command.proxied( + VelocityBrigadierSource::from, + { it.toMc() }, + ) + ) + proxyServer.commandManager.register(brigadierCommand) + } + registered = true } } + +fun CommandContext.toMc(): CommandContext = + copyFor(VelocityBrigadierSource.from(source)) + diff --git a/velocity/src/main/kotlin/su/plo/slib/velocity/command/brigadier/VelocityBrigadierSource.kt b/velocity/src/main/kotlin/su/plo/slib/velocity/command/brigadier/VelocityBrigadierSource.kt new file mode 100644 index 0000000..6e13750 --- /dev/null +++ b/velocity/src/main/kotlin/su/plo/slib/velocity/command/brigadier/VelocityBrigadierSource.kt @@ -0,0 +1,24 @@ +package su.plo.slib.velocity.command.brigadier + +import com.velocitypowered.api.command.CommandSource +import su.plo.slib.api.command.McCommandSource +import su.plo.slib.api.command.brigadier.McBrigadierSource +import su.plo.slib.api.entity.McEntity +import su.plo.slib.velocity.VelocityProxyLib + +data class VelocityBrigadierSource( + override val source: McCommandSource, + override val executor: McEntity? = null, + private val instance: CommandSource, +) : McBrigadierSource { + @Suppress("UNCHECKED_CAST") + override fun getInstance(): T = + instance as T + + companion object { + private val minecraftProxy by lazy { VelocityProxyLib.instance } + + fun from(source: CommandSource): VelocityBrigadierSource = + VelocityBrigadierSource(minecraftProxy.commandManager.getCommandSource(source), instance = source) + } +} diff --git a/velocity/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter b/velocity/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter new file mode 100644 index 0000000..79ec4bc --- /dev/null +++ b/velocity/src/main/resources/META-INF/services/su.plo.slib.api.chat.converter.MessageTextConverter @@ -0,0 +1 @@ +su.plo.slib.velocity.chat.ComponentToMessageConverter diff --git a/velocity/src/test/kotlin/VelocityPlugin.kt b/velocity/src/test/kotlin/VelocityPlugin.kt deleted file mode 100644 index bd55f05..0000000 --- a/velocity/src/test/kotlin/VelocityPlugin.kt +++ /dev/null @@ -1,34 +0,0 @@ -import com.google.inject.Inject -import com.velocitypowered.api.event.Subscribe -import com.velocitypowered.api.event.proxy.ProxyInitializeEvent -import com.velocitypowered.api.plugin.Plugin -import com.velocitypowered.api.proxy.ProxyServer -import su.plo.slib.api.proxy.McProxyLib -import su.plo.slib.api.proxy.event.command.McProxyCommandsRegisterEvent -import su.plo.slib.velocity.VelocityProxyLib - -@Plugin( - id = "test", - name = "Test", - version = "0.0.1", - authors = ["Apehum"] -) -class VelocityPlugin @Inject constructor( - private val proxyServer: ProxyServer -) { - - init { - McProxyCommandsRegisterEvent.registerListener { commandManager, minecraftServer -> - // register commands here - // commandManager.register("pepega", PepegaCommand()) - } - } - - private lateinit var minecraftProxy: McProxyLib - - @Subscribe - fun onProxyInitialization(event: ProxyInitializeEvent) { - // you need to initialize lib here - minecraftProxy = VelocityProxyLib(proxyServer, this) - } -} diff --git a/velocity/src/test/kotlin/su/plo/slib/velocity/TestVelocityPlugin.kt b/velocity/src/test/kotlin/su/plo/slib/velocity/TestVelocityPlugin.kt new file mode 100644 index 0000000..7d0a2e2 --- /dev/null +++ b/velocity/src/test/kotlin/su/plo/slib/velocity/TestVelocityPlugin.kt @@ -0,0 +1,24 @@ +package su.plo.slib.velocity + +import com.google.inject.Inject +import com.velocitypowered.api.event.Subscribe +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent +import com.velocitypowered.api.plugin.Plugin +import com.velocitypowered.api.proxy.ProxyServer +import su.plo.slib.proxy.TestProxy + +@Plugin( + id = "slib-velocity-test", + version = "0.0.1", + authors = ["Apehum"] +) +class TestVelocityPlugin @Inject constructor( + private val proxyServer: ProxyServer +) { + private val testProxy = TestProxy() + + @Subscribe + fun onProxyInitialization(event: ProxyInitializeEvent) { + val minecraftProxy = VelocityProxyLib(proxyServer, this) + } +}