Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
76a64b2
feat: brigadier commands
apehum Dec 23, 2025
a52f67e
fix(spigot): remove reflection proxies for minecraft server and comma…
apehum Dec 23, 2025
0ddea58
feat: McBrigadierSource#from using service loaders
apehum Dec 24, 2025
fef36ef
fix(mod): use command source from CommandSourceStack as command source
apehum Dec 24, 2025
6a15cf4
fix(server): return getPlayerByInstance if entity is player in getEnt…
apehum Dec 24, 2025
386eefa
fix: convert McCommandManager back to interface for backwards-compat
apehum Dec 24, 2025
bcc09b4
feat: add method to resolve directly from source instance in McBrigad…
apehum Dec 24, 2025
580153a
refactor: use McBrigadierSource as brigadier builder source
apehum Dec 26, 2025
e65135a
feat: custom brigadier argument types
apehum Dec 28, 2025
ef984e3
feat(server): ServerPos3d brigadier argument
apehum Jan 28, 2026
f827267
feat(server): game profiles brigadier argument
apehum Jan 29, 2026
d33fc34
fix: proxy customSuggestions in brigadier commands
apehum Jan 30, 2026
c5363f6
fix(mod): disable refmap in mixin file on >=26.1
apehum Apr 20, 2026
ccca826
ci: add 26.1.2 to paper matrix
apehum Apr 20, 2026
e1b9082
fix(mod): add missing mapping for ServerPlayer$3
apehum Apr 20, 2026
a7970f3
build: bump bungee java version to 11
apehum Apr 22, 2026
ea75355
refactor: use LiteralCommandNode to register brigadier comamnds in Mc…
apehum Apr 22, 2026
8695018
feat: add MessageTextConverter to support McTextComponent inside Comm…
apehum Apr 22, 2026
1c87c56
build: update adventure-platform
apehum Apr 23, 2026
8e8c466
build: add commands to smoke tests
apehum Apr 23, 2026
ca1e1c4
build: remove brigadier-game-profiles-selector from tests, it's flaky
apehum Apr 23, 2026
fe7a03f
ci: skip command checks in 1.19.4 and 1.20.1 paper
apehum Apr 23, 2026
5b0ec54
fix: clear brigadier commands on AbstractCommandManager#clear
apehum Apr 24, 2026
fafe0ff
chore(mod): remove wildcard import from ComponentTextConverter
apehum Apr 27, 2026
2c565e7
fix(minestom): correct error message in getEntityByInstance
apehum Apr 27, 2026
f8bd0e3
fix(minestom): unwrap CustomArgumentType when emitting StringArgument…
apehum Apr 27, 2026
45a2328
fix: reject null parse result in CustomArgumentCommandNode
apehum Apr 27, 2026
3065088
chore(proxy): align UuidArgumentType with server fixture
apehum Apr 27, 2026
7c1ae21
docs: fix stale McCommandManager kdoc
apehum Apr 27, 2026
69e1969
fix(minestom): preserve argument chains when converting brigadier tree
apehum Apr 27, 2026
73265cd
fix(proxy): move test translation key registration to common-proxy Te…
apehum Apr 27, 2026
06c428a
fix(mod): use sendSuccess in ModDefaultCommandSource#sendMessage
apehum Apr 29, 2026
41de25e
chore(spigot): remove unnecessary stack trace print
apehum May 9, 2026
ac9b5ce
build: move deps to gradle catalog
apehum May 10, 2026
cf0d149
build: group catalog entries
apehum May 10, 2026
d2e1597
ci: add concurrency for build-pr
apehum May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 80 additions & 8 deletions .github/workflows/build-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -13,61 +16,78 @@ import com.google.common.collect.Maps
*/
abstract class McCommandManager<T : McCommand> {

protected val commandByName: MutableMap<String, T> = 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<String, McCommand>

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<LiteralCommandNode<McBrigadierSource>>

/**
* 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<McBrigadierSource>) {
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<McBrigadierSource>)

/**
* 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<String, McCommand>
get() = ImmutableMap.copyOf<String, McCommand>(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<McBrigadierSource> =
LiteralArgumentBuilder.literal<McBrigadierSource>(name)

@JvmStatic
fun <T> argument(name: String, argument: ArgumentType<T>): RequiredArgumentBuilder<McBrigadierSource, T> =
RequiredArgumentBuilder.argument<McBrigadierSource, T>(name, argument)
}
}
Original file line number Diff line number Diff line change
@@ -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<T> {
/**
* Resolves the argument using the command [source].
*
* @param source the command source
* @return the resolved value
*/
fun resolve(source: McBrigadierSource): T
}
Original file line number Diff line number Diff line change
@@ -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<PARSED, NATIVE> : ArgumentType<PARSED> {
/**
* The native argument type sent to the client.
*/
val nativeType: ArgumentType<NATIVE>

/**
* 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<String> =
nativeType.examples
}
Original file line number Diff line number Diff line change
@@ -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 <T> getInstance(): T
}
14 changes: 14 additions & 0 deletions api/common/src/main/kotlin/su/plo/slib/api/service/LazyService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package su.plo.slib.api.service

import java.util.ServiceLoader

inline fun <reified T> lazyService(): Lazy<T> =
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")
}

Original file line number Diff line number Diff line change
@@ -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 {

Expand Down Expand Up @@ -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.
Expand Down
Loading