Skip to content

Commit 82df85b

Browse files
committed
feat: Register SingleConfig<T> when injecting single configs, auto reload configs when tick changes
docs: Add more KDoc for DI helpers, update command dsl docs
1 parent 3852758 commit 82df85b

7 files changed

Lines changed: 241 additions & 189 deletions

File tree

docs/command-dsl.md

Lines changed: 35 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,43 @@
22

33
[//]: # ([:simple-gradle: ![]&#40;https://img.shields.io/maven-metadata/v?label=idofront-commands&metadataUrl=https://repo.mineinabyss.com/releases/com/mineinabyss/idofront-commands/maven-metadata.xml&#41;{ style="vertical-align:middle" }]&#40;https://repo.mineinabyss.com/#/releases/com/mineinabyss/idofront-commands&#41;)
44

5-
Idofront provides a clean way of creating custom command structures through a DSL. This feature is currently very
6-
experimental.
5+
Idofront provides a commands DSL that wraps Minecraft's Brigadier system.
6+
We show an example implementation in the `examples` module on GitHub.
77

88
## Register your command executor
99

10-
Implement `IdofrontCommandExecutor` It is recommended to create a singleton object for managing your commands.
10+
In your plugin's `onEnable`, call the `commands` block.
11+
Brigadier only lets you register commands on server startup, so your structure can't be changed after this:
1112

1213
```kotlin
13-
object MyCommandExecutor: IdofrontCommandExecutor() {
14-
override val commands: CommandHolder = commands(MyPlugin) { //MyPlugin is a reference to your main plugin instance
15-
//Command DSL ishere
14+
plugin.commands {
15+
// Registers a top level /example command
16+
"example" {
17+
...
1618
}
1719
}
18-
19-
...
20-
21-
override fun onEnable(){
22-
MyCommandExecutor //instantiate the singleton
23-
}
2420
```
2521

26-
That's it! The command registration is done for you. You do however currently need to add your commands into
27-
`plugin.yml` like you normally would, but hopefully this won't be necessary in the future.
28-
2922
## Creating commands
3023

3124
Within your `commands` block, you may create new commands as follows:
3225

3326
```kotlin
34-
command("basicCommand"){
35-
}
36-
37-
command("anotherCommand", "alias", "anotherAlias", desc= "Short description") {
38-
}
39-
40-
//or alternatively, which will be the preferred format in this guide
41-
27+
// Command with description and permission set
4228
"basicCommand" {
29+
description = "Short description"
30+
permission = "my.plugin.perm"
4331
}
4432

45-
("anotherCommand" / "alias" / "anotherAlias")(desc= "Short description") {
33+
// Command with aliases
34+
("anotherCommand" / "alias" / "anotherAlias") {
35+
...
4636
}
4737
""
4838
```
4939

5040
This will allow players to use `/basicCommand`, and `/anotherCommand` (or `/alias`, `/anotherAlias`)
51-
A malformated command will display relevant help information, such as subcommands or missing arguments.
52-
`/basicCommand ?` will display the full help message with description, aliases, etc...
41+
Note that subcommands default to using permission `parentpermission.<commandName>`.
5342

5443
## Subcommands
5544

@@ -58,7 +47,8 @@ different things. We want to be able to do:
5847
`/myplugin version` - To get the plugin version
5948
`/myplugin giveApple <amount (default 1)> <player (default to self)>` - To give an apple to a certain player
6049

61-
We can simply nest subcommands within other commands (indefinitely) to create a structure:
50+
We can nest subcommands within other commands (indefinitely) to create a structure.
51+
Under the hood this chains together Brigadier's `literal`.
6252

6353
```kotlin
6454
"myplugin" {
@@ -67,83 +57,45 @@ We can simply nest subcommands within other commands (indefinitely) to create a
6757
}
6858
```
6959

70-
## Actions
71-
72-
To actually run things with a command, we must specify actions. Actions, like the `playerAction` may have conditions
73-
that need to be met before they are run. If any condition ever fails, the command will stop right there, and an error
74-
will be sent to the sender. Multiple actions may exist in a command, but upon failure, no other actions will be
75-
executed.
60+
## Executes blocks
7661

77-
If an action succeeds, it will execute the code inside, plus give some extra contextual information, such as the sender.
78-
79-
Let's use the default `action` for our version, which gives us access to a sender, and `playerAction` which ensures the
80-
sender is a player, and gives us a player to work with:
62+
We specify what a command does with an `executes` block. Idofront provides some helpers on this to ensure
63+
a sender is a player and to pass command arguments.
8164

8265
```kotlin
8366
"myplugin" {
8467
"version" {
85-
action {
86-
sender.info("Plugin version: ${MyPlugin.version}")
68+
executes {
69+
sender.sendMessage("Plugin version: ${MyPlugin.version}")
8770
}
8871
}
8972
"giveApple" {
90-
playerAction {
73+
executes.asPlayer {
9174
player.inventory.addItem(ItemStack(Material.APPLE, 1))
9275
}
9376
}
9477
}
9578
```
9679

80+
Note that we can also `fail("Message")` inside executes blocks to stop the command at any point and send the sender a message.
81+
9782
## Arguments
9883

9984
Now we need to let players pass an amount for the item.
100-
101-
Arguments are done through delegates, there are some methods for primitives, but you may also create custom arguments:
85+
Arguments take normal Brigadier argument types, with our own DSL around them for providing suggestions or
86+
restricting options. We provide a singleton `Args` you can use to see a list of argument options, as well as `ArgsMinecraft`
87+
which is an alias for Paper's custom argumetns for things like Player, Location, etc...
10288

10389
```kotlin
10490
"giveApple" {
105-
val amount by intArg { default = 1 }
106-
playerAction {
107-
player.inventory.addItem(ItemStack(Material.APPLE, amount))
91+
executes.asPlayer().args(
92+
"amount" to Args.integer(min = 1).default { 1 },
93+
"player" to Args.otherPlayer() // A built-in helper in Idofront for an optional target player
94+
) { amount, other ->
95+
other.inventory.addItem(ItemStack(Material.APPLE, amount))
10896
}
10997
}
11098
```
11199

112-
And that's it! If you have multiple arguments, they will be required in that order. You may also customize some other
113-
things, like the error message within `intArg { }`. You may even use mutable properties.
114-
115-
Default just means the argument may be omitted. It doesn't do much if a default argument is followed by a mandated one.
116-
117-
## Limiting argument visibility
118-
119-
The general rule is, if you have access to an argument, it will be required in that command. This means, while you may
120-
share the same arguments between several subcommands, any commands below will also require that argument to be passed (
121-
the only limitation is that there cannot be any root level arguments yet).
122-
123-
To fix this, a `commandGroup` block exists, which does nothing but limit argument visibility:
124-
125-
```kotlin
126-
commandGroup {
127-
val amount by intArg()
128-
//both commands below will require the "amount" to be passed
129-
"one" { ... }
130-
"two" { ... }
131-
}
132-
//no arguments will be required here, because we can't access any in our code!
133-
"noArgs" { ... }
134-
```
135-
136-
## Conclusion
137-
138-
There are some more features sprinkled into the plugin, but many might disappear. I'm more confident the DSL will remain
139-
more or less similar going into the future, but be warned that some refactors may happen, and some things may change.
140-
141-
## Future plans
142-
143-
The current system also doesn't build any structure at startup, so it's unaware of things like arguments of subcommands,
144-
or subcommands of subcommands. Everything is evaluated on-the-go, except root level commands, since those need to be
145-
registered upon startup (this is why we can't currently have root level arguments). The biggest thing stopping a
146-
structure from being generated upon startup is arguments. While it is possible to get a reference to the object the
147-
delegate is being called from (and thus have separate instances of each argument in a map), this does not work for
148-
lambdas, which return null as the reference. Perhaps it is possible to sidestep this with `inline` functions, thought I
149-
have not tried it yet.
100+
Please be sure to look at the [ExampleCommands](https://github.com/MineInAbyss/Idofront/blob/master/examples/src/main/kotlin/com/mineinabyss/idofront/examples/commands/ExampleCommands.kt)
101+
implementation for more tips around using Minecraft arguments, providing suggestions, and more!

idofront-commands/src/main/kotlin/com/mineinabyss/idofront/commands/brigadier/Args.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ object Args {
3535
.map { Bukkit.getOfflinePlayer(it) }
3636

3737
/**
38-
* An argument for as singple player that defaults to the sender.
38+
* An argument for a single player that defaults to the sender.
3939
* Useful for commands that can optionally run as other players.
40+
*
41+
* Requires `<permission>.others` to run as other players.
4042
*/
4143
fun otherPlayer() = ArgsMinecraft
4244
.player()
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.mineinabyss.idofront.features
2+
3+
import com.mineinabyss.dependencies.DI
4+
import com.mineinabyss.dependencies.DIScope
5+
import com.mineinabyss.dependencies.get
6+
import com.mineinabyss.dependencies.module
7+
import com.mineinabyss.idofront.commands.brigadier.*
8+
import com.mineinabyss.idofront.messaging.error
9+
import com.mineinabyss.idofront.messaging.success
10+
import org.bukkit.plugin.Plugin
11+
12+
/**
13+
* Lets other features register subcommands under this plugin's [MainCommand].
14+
*
15+
* @see mainCommand
16+
*/
17+
val MainCommandFeature = module("Main Command") {
18+
onServerStartup {
19+
plugin.commands {
20+
val main = get<MainCommand>()
21+
val manager = get<DIScope>()
22+
main.names.invoke {
23+
description = main.description
24+
permission = main.permission
25+
main.subcommands.forEach { subcommand ->
26+
// val feature = manager.getModule(subcommand.module) ?: error("Feature name not found: ${subcommand.module}")
27+
val context = DICommandContext(manager, subcommand.module)
28+
subcommand.create(context, this)
29+
}
30+
31+
if (main.reloadCommandName != null) {
32+
main.reloadCommandName {
33+
permission = main.reloadCommandPermission
34+
35+
executes {
36+
if (main.reloadableFeatures == null)
37+
manager.reloadAll()
38+
else manager.reload(*main.reloadableFeatures.toTypedArray())
39+
}
40+
41+
executes.args(
42+
"feature" to Args.string().oneOf {
43+
if (main.reloadableFeatures == null)
44+
manager.loaded.map { it.name }.toList()
45+
else main.reloadableFeatures.map { it.name }
46+
}
47+
) { featureName ->
48+
val feat = manager.loaded.find { it.name == featureName } ?: fail("Feature $featureName not found")
49+
if (runCatching { manager.reload(feat) }.onFailure { it.printStackTrace() }.isSuccess) {
50+
sender.success("Reloaded feature $featureName")
51+
} else {
52+
sender.error("Failed to reload feature $featureName")
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}
61+
62+
/**
63+
* Registers a subcommand under this plugin's [MainCommand].
64+
*
65+
* Plugins using this must first inject a [MainCommand] into their DI container, then load [MainCommandFeature] *after* all other features are loaded:
66+
*
67+
* ```kotlin
68+
* val di = DI {
69+
* single { MainCommand(names = listOf("mycommand"), ...) }
70+
* }
71+
*
72+
* val FeatureA = module("Feature A") { ... }.mainCommand {
73+
* "subcommand" { ... }
74+
* }
75+
*
76+
* di.scope.loadAllCatching(FeatureA, FeatureB, ...)
77+
* di.scope.load(MainCommandFeature)
78+
*/
79+
fun DI.Module.mainCommand(block: context(DICommandContext) IdoRootCommand.() -> Unit): DI.Module {
80+
return override {
81+
onServerStartup {
82+
get<MainCommand>().subcommand(this@mainCommand, block)
83+
}
84+
}
85+
}
86+
87+
/**
88+
* Registers new commands under the [plugin] in this context, unlike [mainCommand], these are top-level commands.
89+
*
90+
* The entire block is only evaluated once on startup, use [DICommandContext]'s `get` to get dependencies in commands.
91+
*/
92+
fun DI.Module.commands(block: context(DICommandContext) RootIdoCommands.() -> Unit) = override {
93+
onServerStartup {
94+
val context = DICommandContext(get(), get())
95+
get<Plugin>().commands {
96+
block(context, this)
97+
}
98+
}
99+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.mineinabyss.idofront.features
2+
3+
import co.touchlab.kermit.Logger
4+
import co.touchlab.kermit.Severity
5+
import com.mineinabyss.dependencies.*
6+
import com.mineinabyss.idofront.config.ConfigBuilder
7+
import com.mineinabyss.idofront.config.SingleConfig
8+
import com.mineinabyss.idofront.config.config
9+
import com.mineinabyss.idofront.messaging.ComponentLogger
10+
import org.bukkit.Bukkit
11+
import org.bukkit.plugin.Plugin
12+
import kotlin.io.path.div
13+
14+
/**
15+
* Injects a single serializable config of type [T], located at [path] relative to the plugin's data folder.
16+
*
17+
* The config will be automatically re-read on a new server tick using a factory binding.
18+
* Also registers a `SingleConfig<T>` binding which can be used to read/write the config manually.
19+
*
20+
* For more complicated config use-cases (ex. reading a directory), use [ConfigBuilder] and manually inject via a context class.
21+
*
22+
* ### Example usage:
23+
*
24+
* ```kotlin
25+
* // Inject, overriding format to Yaml
26+
* singleConfig<MyConfig>("config.yml") { format = Yaml() }
27+
*
28+
* // Gets updated config if requesting on a new server tick
29+
* val config = get<MyConfig>()
30+
*
31+
* // Write newConfig to disk
32+
* get<SingleConfig<MyConfig>>().write(newConfig)
33+
* ```
34+
*/
35+
inline fun <reified T : Any> MutableDI.singleConfig(
36+
path: String,
37+
crossinline configure: ConfigBuilder<T>.() -> Unit = {},
38+
) {
39+
val configHolder by single<SingleConfig<T>> {
40+
val plugin = get<Plugin>()
41+
config<T> { configure() }.single(plugin.dataPath / path)
42+
}
43+
var cache = configHolder.read() to Bukkit.getCurrentTick()
44+
factory<T> {
45+
val currentTick = Bukkit.getCurrentTick()
46+
val (data, lastRead) = cache
47+
if (currentTick == lastRead) return@factory data
48+
else {
49+
val read = configHolder.read()
50+
cache = read to currentTick
51+
return@factory read
52+
}
53+
}
54+
}
55+
56+
/**
57+
* Injects a [Logger] and [ComponentLogger] for the given [plugin].
58+
*
59+
* In the future might be updated to allow re-reading severity from a config after reloads, for now [minSeverity] is evaluated once.
60+
*
61+
* @param minSeverity Provider for the minimum severity to log, can read DI values.
62+
*/
63+
fun MutableDI.singlePluginLogger(
64+
plugin: Plugin,
65+
minSeverity: DI.() -> Severity = { Severity.Info },
66+
) = single { ComponentLogger.forPlugin(plugin, minSeverity()) }.and<Logger>()

0 commit comments

Comments
 (0)